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
This commit is contained in:
Tom Lane 2023-02-20 15:18:22 -05:00
parent 69e8c7cf1d
commit f6db76c555
3 changed files with 63 additions and 0 deletions

View File

@ -183,6 +183,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);
/*

View File

@ -1771,6 +1771,41 @@ 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
-> Hash Left Join
Output: s.sid, t_1.tableoid, t_1.ctid
Inner Unique: true
Hash Cond: (s.sid = t_1.tid)
-> Seq Scan on public.pa_source s
Output: s.sid
-> Hash
Output: t_1.tid, t_1.tableoid, t_1.ctid
-> Seq Scan on public.pa_targetp t_1
Output: t_1.tid, t_1.tableoid, t_1.ctid
(12 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

View 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)