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
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);
/*
(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
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)