Prevent join removal from removing the query's result relation.
authorTom Lane
Mon, 20 Feb 2023 20:18:22 +0000 (15:18 -0500)
committerTom Lane
Mon, 20 Feb 2023 20:18:32 +0000 (15:18 -0500)
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

src/backend/optimizer/plan/analyzejoins.c
src/test/regress/expected/merge.out
src/test/regress/sql/merge.sql

index 337f470d58346f572a5f6a165cafc315178315be..7ea6e26fecde6dd244a85e1c96f879a8419f039c 100644 (file)
@@ -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);
 
    /*
index 21a82eefbcdaa6b27a1a6c5bb0aa3872d2738c5c..564ff68694357e08bbaa884c6e1d909f049e87c0 100644 (file)
@@ -1771,6 +1771,38 @@ SELECT * FROM pa_target ORDER BY tid;
 (9 rows)
 
 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
index fdbcd708823100c3ff7c789f0273e107247f0893..cef37e57d92aa18a93c97f35da7847bb09db4910 100644 (file)
@@ -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)