mirror of
https://github.com/postgres/postgres.git
synced 2026-03-30 14:26:23 -04:00
The premise of src/test/modules/test_plan_advice is that if we plan a query once, generate plan advice, and then replan it using that same advice, all of that advice should apply cleanly, since the settings and everything else are the same. Unfortunately, that's not the case: the test suite is the main regression tests, and concurrent activity can change the statistics on tables involved in the query, especially system catalogs. That's OK as long as it only affects costing, but in a few cases, it affects which relations appear in the final plan at all. In the buildfarm failures observed to date, this happens because we consider alternative subplans for the same portion of the query; in theory, MinMaxAggPath is vulnerable to a similar hazard. In both cases, the planner clones an entire subquery, and the clone has a different plan name, and therefore different range table identifiers, than the original. If a cost change results in flipping between one of these plans and the other, the test_plan_advice tests will fail, because the range table identifiers to which advice was applied won't even be present in the output of the second planning cycle. To fix, invent a new DO_NOT_SCAN advice tag. When generating advice, emit it for relations that should not appear in the final plan at all, because some alternative version of that relation was used instead. When DO_NOT_SCAN is supplied, disable all scan methods for that relation. To make this work, we reuse a bunch of the machinery that previously existed for the purpose of ensuring that we build the same set of relation identifiers during planning as we do from the final PlannedStmt. In the process, this commit slightly weakens the cross-check mechanism: before this commit, it would fire whenever the pg_plan_advice module was loaded, even if pg_plan_advice wasn't actually doing anything; now, it will only engage when we have some other reason to create a pgpa_planner_state. The old way was complex and didn't add much useful test coverage, so this seems like an acceptable sacrifice. Discussion: http://postgr.es/m/CA+TgmoYuWmN-00Ec5pY7zAcpSFQUQLbgAdVWGR9kOR-HM-fHrA@mail.gmail.com Reviewed-by: Lukas Fittl <lukas@fittl.com>
198 lines
9.1 KiB
PL/PgSQL
198 lines
9.1 KiB
PL/PgSQL
LOAD 'pg_plan_advice';
|
|
SET max_parallel_workers_per_gather = 0;
|
|
SET seq_page_cost = 0.1;
|
|
SET random_page_cost = 0.1;
|
|
SET cpu_tuple_cost = 0;
|
|
SET cpu_index_tuple_cost = 0;
|
|
|
|
CREATE TABLE scan_table (a int primary key, b text)
|
|
WITH (autovacuum_enabled = false);
|
|
INSERT INTO scan_table
|
|
SELECT g, 'some text ' || g FROM generate_series(1, 100000) g;
|
|
CREATE INDEX scan_table_b ON scan_table USING brin (b);
|
|
VACUUM ANALYZE scan_table;
|
|
|
|
-- Sequential scan
|
|
EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table;
|
|
|
|
-- Index scan
|
|
EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table WHERE a = 1;
|
|
|
|
-- Index-only scan
|
|
EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT a FROM scan_table WHERE a = 1;
|
|
|
|
-- Bitmap heap scan
|
|
EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table
|
|
WHERE b > 'some text 8';
|
|
|
|
-- TID scan
|
|
EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table WHERE ctid = '(0,1)';
|
|
|
|
-- TID range scan
|
|
EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table
|
|
WHERE ctid > '(1,1)' AND ctid < '(2,1)';
|
|
|
|
-- Try forcing each of our test queries to use the scan type they
|
|
-- wanted to use anyway. This should succeed.
|
|
BEGIN;
|
|
SET LOCAL pg_plan_advice.advice = 'SEQ_SCAN(scan_table)';
|
|
EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table;
|
|
SET LOCAL pg_plan_advice.advice = 'INDEX_SCAN(scan_table scan_table_pkey)';
|
|
EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table WHERE a = 1;
|
|
SET LOCAL pg_plan_advice.advice = 'INDEX_ONLY_SCAN(scan_table scan_table_pkey)';
|
|
EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT a FROM scan_table WHERE a = 1;
|
|
SET LOCAL pg_plan_advice.advice = 'BITMAP_HEAP_SCAN(scan_table)';
|
|
EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table
|
|
WHERE b > 'some text 8';
|
|
SET LOCAL pg_plan_advice.advice = 'TID_SCAN(scan_table)';
|
|
EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table WHERE ctid = '(0,1)';
|
|
EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table
|
|
WHERE ctid > '(1,1)' AND ctid < '(2,1)';
|
|
COMMIT;
|
|
|
|
-- Try to force a full scan of the table to use some other scan type. All
|
|
-- of these will fail. An index scan or bitmap heap scan could potentially
|
|
-- generate the correct answer, but the planner does not even consider these
|
|
-- possibilities due to the lack of a WHERE clause.
|
|
BEGIN;
|
|
SET LOCAL pg_plan_advice.advice = 'INDEX_SCAN(scan_table scan_table_pkey)';
|
|
EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table;
|
|
SET LOCAL pg_plan_advice.advice = 'INDEX_ONLY_SCAN(scan_table scan_table_pkey)';
|
|
EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table;
|
|
SET LOCAL pg_plan_advice.advice = 'BITMAP_HEAP_SCAN(scan_table)';
|
|
EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table;
|
|
SET LOCAL pg_plan_advice.advice = 'TID_SCAN(scan_table)';
|
|
EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table;
|
|
COMMIT;
|
|
|
|
-- Try again to force index use. This should now succeed for the INDEX_SCAN
|
|
-- and BITMAP_HEAP_SCAN, but the INDEX_ONLY_SCAN can't be forced because the
|
|
-- query fetches columns not included in the index.
|
|
BEGIN;
|
|
SET LOCAL pg_plan_advice.advice = 'INDEX_SCAN(scan_table scan_table_pkey)';
|
|
EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table WHERE a > 0;
|
|
SET LOCAL pg_plan_advice.advice = 'INDEX_ONLY_SCAN(scan_table scan_table_pkey)';
|
|
EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table WHERE a > 0;
|
|
SET LOCAL pg_plan_advice.advice = 'BITMAP_HEAP_SCAN(scan_table)';
|
|
EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table WHERE a > 0;
|
|
COMMIT;
|
|
|
|
-- We can force a primary key lookup to use a sequential scan, but we
|
|
-- can't force it to use an index-only scan (due to the column list)
|
|
-- or a TID scan (due to the absence of a TID qual). If we apply DO_NOT_SCAN
|
|
-- here, we should get a valid plan anyway, but with the scan disabled.
|
|
BEGIN;
|
|
SET LOCAL pg_plan_advice.advice = 'SEQ_SCAN(scan_table)';
|
|
EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table WHERE a = 1;
|
|
SET LOCAL pg_plan_advice.advice = 'INDEX_ONLY_SCAN(scan_table scan_table_pkey)';
|
|
EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table WHERE a = 1;
|
|
SET LOCAL pg_plan_advice.advice = 'TID_SCAN(scan_table)';
|
|
EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table WHERE a = 1;
|
|
SET LOCAL pg_plan_advice.advice = 'DO_NOT_SCAN(scan_table)';
|
|
EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table WHERE a = 1;
|
|
COMMIT;
|
|
|
|
-- We can forcibly downgrade an index-only scan to an index scan, but we can't
|
|
-- force the use of an index that the planner thinks is inapplicable.
|
|
BEGIN;
|
|
SET LOCAL pg_plan_advice.advice = 'INDEX_SCAN(scan_table scan_table_pkey)';
|
|
EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table WHERE a = 1;
|
|
SET LOCAL pg_plan_advice.advice = 'INDEX_SCAN(scan_table public.scan_table_pkey)';
|
|
EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table WHERE a = 1;
|
|
SET LOCAL pg_plan_advice.advice = 'INDEX_SCAN(scan_table scan_table_b)';
|
|
EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table WHERE a = 1;
|
|
COMMIT;
|
|
|
|
-- We can force the use of a sequential scan in place of a bitmap heap scan,
|
|
-- but a plain index scan on a BRIN index is not possible.
|
|
BEGIN;
|
|
SET LOCAL pg_plan_advice.advice = 'SEQ_SCAN(scan_table)';
|
|
EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table
|
|
WHERE b > 'some text 8';
|
|
SET LOCAL pg_plan_advice.advice = 'INDEX_SCAN(scan_table scan_table_b)';
|
|
EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table WHERE a = 1;
|
|
COMMIT;
|
|
|
|
-- We can force the use of a sequential scan rather than a TID scan or
|
|
-- TID range scan.
|
|
BEGIN;
|
|
SET LOCAL pg_plan_advice.advice = 'SEQ_SCAN(scan_table)';
|
|
EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table WHERE ctid = '(0,1)';
|
|
EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table
|
|
WHERE ctid > '(1,1)' AND ctid < '(2,1)';
|
|
COMMIT;
|
|
|
|
-- Test more complex scenarios with index scans.
|
|
BEGIN;
|
|
-- Should still work if we mention the schema.
|
|
SET LOCAL pg_plan_advice.advice = 'INDEX_SCAN(scan_table public.scan_table_pkey)';
|
|
EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table WHERE a = 1;
|
|
-- But not if we mention the wrong schema.
|
|
SET LOCAL pg_plan_advice.advice = 'INDEX_SCAN(scan_table cilbup.scan_table_pkey)';
|
|
EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table WHERE a = 1;
|
|
-- It's OK to repeat the same advice.
|
|
SET LOCAL pg_plan_advice.advice = 'INDEX_SCAN(scan_table scan_table_pkey scan_table scan_table_pkey)';
|
|
EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table WHERE a = 1;
|
|
-- But it doesn't work if the index target is even notionally different.
|
|
SET LOCAL pg_plan_advice.advice = 'INDEX_SCAN(scan_table scan_table_pkey scan_table public.scan_table_pkey)';
|
|
EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT * FROM scan_table WHERE a = 1;
|
|
COMMIT;
|
|
|
|
-- Test assorted incorrect advice.
|
|
BEGIN;
|
|
SET LOCAL pg_plan_advice.advice = 'SEQ_SCAN(nothing)';
|
|
EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT a FROM scan_table WHERE a = 1;
|
|
SET LOCAL pg_plan_advice.advice = 'INDEX_SCAN(nothing whatsoever)';
|
|
EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT a FROM scan_table WHERE a = 1;
|
|
SET LOCAL pg_plan_advice.advice = 'INDEX_SCAN(scan_table bogus)';
|
|
EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT a FROM scan_table WHERE a = 1;
|
|
SET LOCAL pg_plan_advice.advice = 'INDEX_ONLY_SCAN(nothing whatsoever)';
|
|
EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT a FROM scan_table WHERE a = 1;
|
|
SET LOCAL pg_plan_advice.advice = 'INDEX_ONLY_SCAN(scan_table bogus)';
|
|
EXPLAIN (COSTS OFF, PLAN_ADVICE) SELECT a FROM scan_table WHERE a = 1;
|
|
COMMIT;
|
|
|
|
-- Test our ability to refer to multiple instances of the same alias.
|
|
BEGIN;
|
|
EXPLAIN (COSTS OFF, PLAN_ADVICE)
|
|
SELECT * FROM (generate_series(1,10) g LEFT JOIN scan_table s ON g = s.a) x
|
|
LEFT JOIN scan_table s ON g = s.a;
|
|
SET LOCAL pg_plan_advice.advice = 'SEQ_SCAN(s)';
|
|
EXPLAIN (COSTS OFF, PLAN_ADVICE)
|
|
SELECT * FROM (generate_series(1,10) g LEFT JOIN scan_table s ON g = s.a) x
|
|
LEFT JOIN scan_table s ON g = s.a;
|
|
SET LOCAL pg_plan_advice.advice = 'SEQ_SCAN(s#2)';
|
|
EXPLAIN (COSTS OFF, PLAN_ADVICE)
|
|
SELECT * FROM (generate_series(1,10) g LEFT JOIN scan_table s ON g = s.a) x
|
|
LEFT JOIN scan_table s ON g = s.a;
|
|
SET LOCAL pg_plan_advice.advice = 'SEQ_SCAN(s) SEQ_SCAN(s#2)';
|
|
EXPLAIN (COSTS OFF, PLAN_ADVICE)
|
|
SELECT * FROM (generate_series(1,10) g LEFT JOIN scan_table s ON g = s.a) x
|
|
LEFT JOIN scan_table s ON g = s.a;
|
|
COMMIT;
|
|
|
|
-- Test our ability to refer to scans within a subquery.
|
|
EXPLAIN (COSTS OFF, PLAN_ADVICE)
|
|
SELECT * FROM (SELECT * FROM scan_table s WHERE a = 1 OFFSET 0) x;
|
|
EXPLAIN (COSTS OFF, PLAN_ADVICE)
|
|
SELECT * FROM (SELECT * FROM scan_table s WHERE a = 1 OFFSET 0);
|
|
BEGIN;
|
|
-- Should not match.
|
|
SET LOCAL pg_plan_advice.advice = 'SEQ_SCAN(s)';
|
|
EXPLAIN (COSTS OFF, PLAN_ADVICE)
|
|
SELECT * FROM (SELECT * FROM scan_table s WHERE a = 1 OFFSET 0) x;
|
|
EXPLAIN (COSTS OFF, PLAN_ADVICE)
|
|
SELECT * FROM (SELECT * FROM scan_table s WHERE a = 1 OFFSET 0);
|
|
-- Should match first query only.
|
|
SET LOCAL pg_plan_advice.advice = 'SEQ_SCAN(s@x)';
|
|
EXPLAIN (COSTS OFF, PLAN_ADVICE)
|
|
SELECT * FROM (SELECT * FROM scan_table s WHERE a = 1 OFFSET 0) x;
|
|
EXPLAIN (COSTS OFF, PLAN_ADVICE)
|
|
SELECT * FROM (SELECT * FROM scan_table s WHERE a = 1 OFFSET 0);
|
|
-- Should match second query only.
|
|
SET LOCAL pg_plan_advice.advice = 'SEQ_SCAN(s@unnamed_subquery)';
|
|
EXPLAIN (COSTS OFF, PLAN_ADVICE)
|
|
SELECT * FROM (SELECT * FROM scan_table s WHERE a = 1 OFFSET 0) x;
|
|
EXPLAIN (COSTS OFF, PLAN_ADVICE)
|
|
SELECT * FROM (SELECT * FROM scan_table s WHERE a = 1 OFFSET 0);
|
|
COMMIT;
|