postgresql/contrib/pg_plan_advice/sql/alternatives.sql
Robert Haas 6455e55b0d pg_plan_advice: Invent DO_NOT_SCAN(relation_identifier).
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>
2026-03-26 17:09:57 -04:00

58 lines
2.3 KiB
PL/PgSQL

LOAD 'pg_plan_advice';
SET max_parallel_workers_per_gather = 0;
CREATE TABLE alt_t1 (a int) WITH (autovacuum_enabled = false);
CREATE TABLE alt_t2 (a int) WITH (autovacuum_enabled = false);
CREATE INDEX ON alt_t2(a);
INSERT INTO alt_t1 SELECT generate_series(1, 1000);
INSERT INTO alt_t2 SELECT generate_series(1, 100000);
VACUUM ANALYZE alt_t1;
VACUUM ANALYZE alt_t2;
-- This query uses an OR to prevent the EXISTS from being converted to a
-- semi-join, forcing the planner through the AlternativeSubPlan path.
EXPLAIN (COSTS OFF, PLAN_ADVICE)
SELECT * FROM alt_t1
WHERE EXISTS (SELECT 1 FROM alt_t2 WHERE alt_t2.a = alt_t1.a) OR alt_t1.a < 0;
-- We should be able to force either AlternativeSubPlan by advising against
-- scanning the other relation.
BEGIN;
SET LOCAL pg_plan_advice.advice = 'DO_NOT_SCAN(alt_t2@exists_1)';
EXPLAIN (COSTS OFF, PLAN_ADVICE)
SELECT * FROM alt_t1
WHERE EXISTS (SELECT 1 FROM alt_t2 WHERE alt_t2.a = alt_t1.a) OR alt_t1.a < 0;
SET LOCAL pg_plan_advice.advice = 'DO_NOT_SCAN(alt_t2@exists_2)';
EXPLAIN (COSTS OFF, PLAN_ADVICE)
SELECT * FROM alt_t1
WHERE EXISTS (SELECT 1 FROM alt_t2 WHERE alt_t2.a = alt_t1.a) OR alt_t1.a < 0;
COMMIT;
-- Now let's test a case involving MinMaxAggPath, which we treat similarly
-- to the AlternativeSubPlan case.
CREATE TABLE alt_minmax (a int) WITH (autovacuum_enabled = false);
CREATE INDEX ON alt_minmax(a);
INSERT INTO alt_minmax SELECT generate_series(1, 10000);
VACUUM ANALYZE alt_minmax;
-- Using an Index Scan inside of an InitPlan should win over a full table
-- scan.
EXPLAIN (COSTS OFF, PLAN_ADVICE)
SELECT min(a), max(a) FROM alt_minmax;
-- Advising against the scan of alt_minmax at the root query level should
-- change nothing, but if we say we don't want either of or both of the
-- minmax-variant scans, the plan should switch to a full table scan.
BEGIN;
SET LOCAL pg_plan_advice.advice = 'DO_NOT_SCAN(alt_minmax)';
EXPLAIN (COSTS OFF, PLAN_ADVICE)
SELECT min(a), max(a) FROM alt_minmax;
SET LOCAL pg_plan_advice.advice = 'DO_NOT_SCAN(alt_minmax@minmax_1)';
EXPLAIN (COSTS OFF, PLAN_ADVICE)
SELECT min(a), max(a) FROM alt_minmax;
SET LOCAL pg_plan_advice.advice = 'DO_NOT_SCAN(alt_minmax@minmax_1) DO_NOT_SCAN(alt_minmax@minmax_2)';
EXPLAIN (COSTS OFF, PLAN_ADVICE)
SELECT min(a), max(a) FROM alt_minmax;
COMMIT;
DROP TABLE alt_t1, alt_t2, alt_minmax;