diff options
author | Tom Lane <tgl@sss.pgh.pa.us> | 2023-02-20 15:18:22 -0500 |
---|---|---|
committer | Tom Lane <tgl@sss.pgh.pa.us> | 2023-02-20 15:18:32 -0500 |
commit | e6d8639cf25ccfffe12695768a4f7a60130c426f (patch) | |
tree | 20a23fbd0743fda124829443ce7d9afb0070b9ed /src | |
parent | da32a99df1f519622eee0d5c3ea61226468272a7 (diff) | |
download | postgresql-e6d8639cf25ccfffe12695768a4f7a60130c426f.tar.gz postgresql-e6d8639cf25ccfffe12695768a4f7a60130c426f.zip |
Prevent join removal from removing the query's result relation.
This was not something that required consideration before MERGE
was invented; but MERGE builds a join tree that left-joins to the
result relation, meaning that remove_useless_joins will consider
removing it. That should generally be stopped by the query's use
of output variables from the result relation. However, if the
result relation is inherited (e.g. a partitioned table) then
we don't add any row identity variables to the query until
expand_inherited_rtentry, which happens after join removal.
This was exposed as of commit 3c569049b, which made it possible
to deduce that a partitioned table could contain at most one row
matching a join key, enabling removal of the not-yet-expanded
result relation. Ooops.
To fix, let's just teach join_is_removable that the query result
rel is never removable. It's a cheap enough test in any case,
and it'll save some cycles that we'd otherwise expend in proving
that it's not removable, even in the cases we got right.
Back-patch to v15 where MERGE was added. Although I think the
case cannot be reached in v15, this seems like cheap insurance.
Per investigation of a report from Alexander Lakhin.
Discussion: https://postgr.es/m/36bee393-b351-16ac-93b2-d46d83637e45@gmail.com
Diffstat (limited to 'src')
-rw-r--r-- | src/backend/optimizer/plan/analyzejoins.c | 8 | ||||
-rw-r--r-- | src/test/regress/expected/merge.out | 32 | ||||
-rw-r--r-- | src/test/regress/sql/merge.sql | 20 |
3 files changed, 60 insertions, 0 deletions
diff --git a/src/backend/optimizer/plan/analyzejoins.c b/src/backend/optimizer/plan/analyzejoins.c index 337f470d583..7ea6e26fecd 100644 --- a/src/backend/optimizer/plan/analyzejoins.c +++ b/src/backend/optimizer/plan/analyzejoins.c @@ -176,6 +176,14 @@ join_is_removable(PlannerInfo *root, SpecialJoinInfo *sjinfo) if (!bms_get_singleton_member(sjinfo->min_righthand, &innerrelid)) return false; + /* + * Never try to eliminate a left join to the query result rel. Although + * the case is syntactically impossible in standard SQL, MERGE will build + * a join tree that looks exactly like that. + */ + if (innerrelid == root->parse->resultRelation) + return false; + innerrel = find_base_rel(root, innerrelid); /* diff --git a/src/test/regress/expected/merge.out b/src/test/regress/expected/merge.out index 21a82eefbcd..564ff686943 100644 --- a/src/test/regress/expected/merge.out +++ b/src/test/regress/expected/merge.out @@ -1773,6 +1773,38 @@ SELECT * FROM pa_target ORDER BY tid; ROLLBACK; DROP TABLE pa_source; DROP TABLE pa_target CASCADE; +-- Partitioned table with primary key +CREATE TABLE pa_target (tid integer PRIMARY KEY) PARTITION BY LIST (tid); +CREATE TABLE pa_targetp PARTITION OF pa_target DEFAULT; +CREATE TABLE pa_source (sid integer); +INSERT INTO pa_source VALUES (1), (2); +EXPLAIN (VERBOSE, COSTS OFF) +MERGE INTO pa_target t USING pa_source s ON t.tid = s.sid + WHEN NOT MATCHED THEN INSERT VALUES (s.sid); + QUERY PLAN +----------------------------------------------------------------------- + Merge on public.pa_target t + Merge on public.pa_targetp t_1 + -> Nested Loop Left Join + Output: s.sid, t_1.tableoid, t_1.ctid + -> Seq Scan on public.pa_source s + Output: s.sid + -> Index Scan using pa_targetp_pkey on public.pa_targetp t_1 + Output: t_1.tid, t_1.tableoid, t_1.ctid + Index Cond: (t_1.tid = s.sid) +(9 rows) + +MERGE INTO pa_target t USING pa_source s ON t.tid = s.sid + WHEN NOT MATCHED THEN INSERT VALUES (s.sid); +TABLE pa_target; + tid +----- + 1 + 2 +(2 rows) + +DROP TABLE pa_source; +DROP TABLE pa_target CASCADE; -- some complex joins on the source side CREATE TABLE cj_target (tid integer, balance float, val text) WITH (autovacuum_enabled=off); diff --git a/src/test/regress/sql/merge.sql b/src/test/regress/sql/merge.sql index fdbcd708823..cef37e57d92 100644 --- a/src/test/regress/sql/merge.sql +++ b/src/test/regress/sql/merge.sql @@ -1116,6 +1116,26 @@ ROLLBACK; DROP TABLE pa_source; DROP TABLE pa_target CASCADE; +-- Partitioned table with primary key + +CREATE TABLE pa_target (tid integer PRIMARY KEY) PARTITION BY LIST (tid); +CREATE TABLE pa_targetp PARTITION OF pa_target DEFAULT; +CREATE TABLE pa_source (sid integer); + +INSERT INTO pa_source VALUES (1), (2); + +EXPLAIN (VERBOSE, COSTS OFF) +MERGE INTO pa_target t USING pa_source s ON t.tid = s.sid + WHEN NOT MATCHED THEN INSERT VALUES (s.sid); + +MERGE INTO pa_target t USING pa_source s ON t.tid = s.sid + WHEN NOT MATCHED THEN INSERT VALUES (s.sid); + +TABLE pa_target; + +DROP TABLE pa_source; +DROP TABLE pa_target CASCADE; + -- some complex joins on the source side CREATE TABLE cj_target (tid integer, balance float, val text) |