From 489896e07d3ad3b14b214129d407f67f36ece5a5 Mon Sep 17 00:00:00 2001 From: Tender Wang Date: Mon, 12 Oct 2026 09:02:07 +0800 Subject: [PATCH] Fix self-join elimination for inheritance scans Self-join elimination groups candidate relations by their relation OID, but references to the same relation can have different scan scopes when one includes inheritance children and another uses ONLY. A unique index on the parent does not prove that matching rows from these scans are the same physical row. Eliminating the join can therefore produce incorrect results. Exclude RTEs with inh set from self-join elimination candidates. Add regression coverage for joins between inheritance scans and ONLY scans in both orders, and verify that joins between two ONLY scans can still be eliminated. --- src/backend/optimizer/plan/analyzejoins.c | 14 ++++--- src/test/regress/expected/join.out | 46 +++++++++++++++++++++++ src/test/regress/sql/join.sql | 30 +++++++++++++++ 3 files changed, 85 insertions(+), 5 deletions(-) diff --git a/src/backend/optimizer/plan/analyzejoins.c b/src/backend/optimizer/plan/analyzejoins.c index 1fafca57004..0946ace1b6d 100644 --- a/src/backend/optimizer/plan/analyzejoins.c +++ b/src/backend/optimizer/plan/analyzejoins.c @@ -1981,15 +1981,19 @@ remove_self_joins_recurse(PlannerInfo *root, List *joinlist) RangeTblEntry *rte = root->simple_rte_array[varno]; /* - * We only consider ordinary relations as candidates to be - * removed, and these relations should not have TABLESAMPLE - * clauses specified. Removing a relation with TABLESAMPLE clause - * could potentially change the semantics of the query. Because of - * UPDATE/DELETE EPQ mechanism, currently Query->resultRelation or + * We only consider ordinary relations without inheritance scans as + * candidates to be removed. A unique index on an inheritance parent + * does not establish uniqueness across its children. + * + * These relations should not have TABLESAMPLE clauses specified. + * Removing a relation with a TABLESAMPLE clause could potentially + * change the semantics of the query. Because of the UPDATE/DELETE + * EPQ mechanism, currently Query->resultRelation or * Query->mergeTargetRelation associated rel cannot be eliminated. */ if (rte->rtekind == RTE_RELATION && rte->relkind == RELKIND_RELATION && + !rte->inh && rte->tablesample == NULL && varno != root->parse->resultRelation && varno != root->parse->mergeTargetRelation) diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out index 4ee94d00ade..424f7b33d6f 100644 --- a/src/test/regress/expected/join.out +++ b/src/test/regress/expected/join.out @@ -10017,6 +10017,52 @@ SELECT a1.a FROM sj a1,sj a2 WHERE (a1.a=a2.a) FOR UPDATE; Filter: (a IS NOT NULL) (3 rows) +-- Self-join elimination must not merge an inheritance scan with an ONLY scan. +CREATE TABLE sje_inh (id int, v int); +CREATE TABLE sje_inh_child () INHERITS (sje_inh); +CREATE UNIQUE INDEX sje_inh_id_idx ON sje_inh (id); +INSERT INTO sje_inh VALUES (1, 1); +INSERT INTO sje_inh_child VALUES (1, 100); +ANALYZE sje_inh; +ANALYZE sje_inh_child; +SELECT p1.id, p1.v, p2.id, p2.v +FROM sje_inh p1 JOIN ONLY sje_inh p2 ON p1.id = p2.id +ORDER BY p1.v, p2.v; + id | v | id | v +----+-----+----+--- + 1 | 1 | 1 | 1 + 1 | 100 | 1 | 1 +(2 rows) + +-- Check the opposite order as well. +SELECT p1.id, p1.v, p2.id, p2.v +FROM ONLY sje_inh p1 JOIN sje_inh p2 ON p1.id = p2.id +ORDER BY p1.v, p2.v; + id | v | id | v +----+---+----+----- + 1 | 1 | 1 | 1 + 1 | 1 | 1 | 100 +(2 rows) + +-- Self-join elimination is still valid when both references use ONLY. +EXPLAIN (COSTS OFF) +SELECT p1.id, p1.v, p2.id, p2.v +FROM ONLY sje_inh p1 JOIN ONLY sje_inh p2 ON p1.id = p2.id; + QUERY PLAN +---------------------------- + Seq Scan on sje_inh p2 + Filter: (id IS NOT NULL) +(2 rows) + +SELECT p1.id, p1.v, p2.id, p2.v +FROM ONLY sje_inh p1 JOIN ONLY sje_inh p2 ON p1.id = p2.id; + id | v | id | v +----+---+----+--- + 1 | 1 | 1 | 1 +(1 row) + +DROP TABLE sje_inh_child; +DROP TABLE sje_inh; reset enable_hashjoin; reset enable_mergejoin; -- diff --git a/src/test/regress/sql/join.sql b/src/test/regress/sql/join.sql index bf8153afbcd..50c65089df0 100644 --- a/src/test/regress/sql/join.sql +++ b/src/test/regress/sql/join.sql @@ -3906,6 +3906,36 @@ ON sj_t1.id = _t2t3t4.id; EXPLAIN (COSTS OFF) SELECT a1.a FROM sj a1,sj a2 WHERE (a1.a=a2.a) FOR UPDATE; +-- Self-join elimination must not merge an inheritance scan with an ONLY scan. +CREATE TABLE sje_inh (id int, v int); +CREATE TABLE sje_inh_child () INHERITS (sje_inh); +CREATE UNIQUE INDEX sje_inh_id_idx ON sje_inh (id); + +INSERT INTO sje_inh VALUES (1, 1); +INSERT INTO sje_inh_child VALUES (1, 100); +ANALYZE sje_inh; +ANALYZE sje_inh_child; + +SELECT p1.id, p1.v, p2.id, p2.v +FROM sje_inh p1 JOIN ONLY sje_inh p2 ON p1.id = p2.id +ORDER BY p1.v, p2.v; + +-- Check the opposite order as well. +SELECT p1.id, p1.v, p2.id, p2.v +FROM ONLY sje_inh p1 JOIN sje_inh p2 ON p1.id = p2.id +ORDER BY p1.v, p2.v; + +-- Self-join elimination is still valid when both references use ONLY. +EXPLAIN (COSTS OFF) +SELECT p1.id, p1.v, p2.id, p2.v +FROM ONLY sje_inh p1 JOIN ONLY sje_inh p2 ON p1.id = p2.id; + +SELECT p1.id, p1.v, p2.id, p2.v +FROM ONLY sje_inh p1 JOIN ONLY sje_inh p2 ON p1.id = p2.id; + +DROP TABLE sje_inh_child; +DROP TABLE sje_inh; + reset enable_hashjoin; reset enable_mergejoin; -- 2.43.0