]> git.ipfire.org Git - thirdparty/postgresql.git/commitdiff
Tighten nullingrels checks for outer joins
authorRichard Guo <rguo@postgresql.org>
Wed, 8 Jul 2026 03:02:21 +0000 (12:02 +0900)
committerRichard Guo <rguo@postgresql.org>
Wed, 8 Jul 2026 03:02:21 +0000 (12:02 +0900)
When fixing up the targetlist and qpqual of an outer join, we must
account for the effects of the outer join.  Vars and PHVs appearing
there are logically above the join, so they should have nullingrels
equal to the input Vars/PHVs' nullingrels plus the bit added by the
outer join.

Determining the effects of the outer join can be tricky when the join
has been commuted with another one per outer join identity 3.  In this
case, the Vars/PHVs in the join's targetlist and qpqual should have
the same nullingrels that they would if the two joins had been done in
syntactic order.  Unfortunately, in setrefs.c, we don't have enough
information to identify what that should be, so we have to use
superset nullingrels matches instead of exact ones.

However, we can tighten the check somewhat.  Currently, we check
whether the jointype is JOIN_INNER and use NRM_SUPERSET if it is not.
We can improve this by checking whether the Join node has non-empty
ojrelids and using NRM_SUPERSET only in that case.  This allows us to
perform exact matches in more situations.

To support this, we record the outer-join relids in Join plan nodes.
This information can also improve EXPLAIN (RANGE_TABLE) output by
showing which outer-join relids are completed by each Join plan node.
We may discover additional uses for this information in the future.

Author: Richard Guo <guofenglinux@gmail.com>
Discussion: https://postgr.es/m/CAMbWs482_DFHzQ079ZPp6c8UvmFdz3Jj+4K8tVRu9g2Bw34NPA@mail.gmail.com

contrib/pg_overexplain/expected/pg_overexplain.out
contrib/pg_overexplain/pg_overexplain.c
contrib/pg_overexplain/sql/pg_overexplain.sql
src/backend/optimizer/plan/createplan.c
src/backend/optimizer/plan/setrefs.c
src/include/nodes/plannodes.h

index 12ab92629ab2303e9783da14d16f5541a5f182bf..dfe530f6f3f8554db44823f34e990cc89d108416 100644 (file)
@@ -497,14 +497,15 @@ $$);
 (15 rows)
 
 -- Create an index, and then attempt to force a nested loop with inner index
--- scan so that we can see parameter-related information. Also, let's try
--- actually running the query, but try to suppress potentially variable output.
+-- scan so that we can see parameter-related information.
 CREATE INDEX ON vegetables (id);
 ANALYZE vegetables;
 SET enable_hashjoin = false;
 SET enable_material = false;
 SET enable_mergejoin = false;
 SET enable_seqscan = false;
+-- Let's try actually running the query, but try to suppress potentially
+-- variable output.
 SELECT explain_filter($$
 EXPLAIN (BUFFERS OFF, COSTS OFF, SUMMARY OFF, TIMING OFF, ANALYZE, DEBUG)
 SELECT * FROM vegetables v1, vegetables v2 WHERE v1.id = v2.id;
@@ -560,6 +561,41 @@ $$);
    Parse Location: 0 to end
 (47 rows)
 
+-- Test the RANGE_TABLE option with a case that involves an outer join.
+SELECT explain_filter($$
+EXPLAIN (RANGE_TABLE, COSTS OFF)
+SELECT * FROM daucus d LEFT JOIN brassica b ON d.id = b.id;
+$$);
+                     explain_filter                      
+---------------------------------------------------------
+ Nested Loop Left Join
+   Outer Join RTIs: 3
+   ->  Index Scan using daucus_id_idx on daucus d
+         Scan RTI: 1
+   ->  Index Scan using brassica_id_idx on brassica b
+         Index Cond: (id = d.id)
+         Scan RTI: 2
+ RTI 1 (relation, in-from-clause):
+   Alias: d ()
+   Eref: d (id, name, genus)
+   Relation: daucus
+   Relation Kind: relation
+   Relation Lock Mode: AccessShareLock
+   Permission Info Index: 1
+ RTI 2 (relation, in-from-clause):
+   Alias: b ()
+   Eref: b (id, name, genus)
+   Relation: brassica
+   Relation Kind: relation
+   Relation Lock Mode: AccessShareLock
+   Permission Info Index: 2
+ RTI 3 (join, in-from-clause):
+   Eref: unnamed_join (id, name, genus, id, name, genus)
+   Join Type: Left
+ Unprunable RTIs: 1 2
+(25 rows)
+
+-- Restore default settings.
 RESET enable_hashjoin;
 RESET enable_material;
 RESET enable_mergejoin;
index a93a4dcfed6ef2bb461cbf407741e0079d99b051..b9933eb395c80551dc082a3748a63f6c871d4132 100644 (file)
@@ -261,6 +261,20 @@ overexplain_per_node_hook(PlanState *planstate, List *ancestors,
                                                                                  ((Result *) plan)->relids,
                                                                                  es);
                                break;
+                       case T_MergeJoin:
+                       case T_NestLoop:
+                       case T_HashJoin:
+
+                               /*
+                                * 'ojrelids' is only meaningful for non-inner joins, but if
+                                * it somehow ends up set for an inner join, print it anyway.
+                                */
+                               if (((Join *) plan)->jointype != JOIN_INNER ||
+                                       ((Join *) plan)->ojrelids != NULL)
+                                       overexplain_bitmapset("Outer Join RTIs",
+                                                                                 ((Join *) plan)->ojrelids,
+                                                                                 es);
+                               break;
                        default:
                                break;
                }
index 3f17b61a2dafd2836424fe3530b610915323a6ba..80e9acda8de6e6d84695fc182471e8f2c6a3e324 100644 (file)
@@ -92,18 +92,28 @@ INSERT INTO vegetables (name, genus)
 $$);
 
 -- Create an index, and then attempt to force a nested loop with inner index
--- scan so that we can see parameter-related information. Also, let's try
--- actually running the query, but try to suppress potentially variable output.
+-- scan so that we can see parameter-related information.
 CREATE INDEX ON vegetables (id);
 ANALYZE vegetables;
 SET enable_hashjoin = false;
 SET enable_material = false;
 SET enable_mergejoin = false;
 SET enable_seqscan = false;
+
+-- Let's try actually running the query, but try to suppress potentially
+-- variable output.
 SELECT explain_filter($$
 EXPLAIN (BUFFERS OFF, COSTS OFF, SUMMARY OFF, TIMING OFF, ANALYZE, DEBUG)
 SELECT * FROM vegetables v1, vegetables v2 WHERE v1.id = v2.id;
 $$);
+
+-- Test the RANGE_TABLE option with a case that involves an outer join.
+SELECT explain_filter($$
+EXPLAIN (RANGE_TABLE, COSTS OFF)
+SELECT * FROM daucus d LEFT JOIN brassica b ON d.id = b.id;
+$$);
+
+-- Restore default settings.
 RESET enable_hashjoin;
 RESET enable_material;
 RESET enable_mergejoin;
index de6a183da7996307cdd038e5c22c1f675b4b3ff3..2c696ea02688e26d7df6f37bab7a60f5048bd0a9 100644 (file)
@@ -231,14 +231,18 @@ static BitmapOr *make_bitmap_or(List *bitmapplans);
 static NestLoop *make_nestloop(List *tlist,
                                                           List *joinclauses, List *otherclauses, List *nestParams,
                                                           Plan *lefttree, Plan *righttree,
-                                                          JoinType jointype, bool inner_unique);
+                                                          JoinType jointype,
+                                                          Relids ojrelids,
+                                                          bool inner_unique);
 static HashJoin *make_hashjoin(List *tlist,
                                                           List *joinclauses, List *otherclauses,
                                                           List *hashclauses,
                                                           List *hashoperators, List *hashcollations,
                                                           List *hashkeys,
                                                           Plan *lefttree, Plan *righttree,
-                                                          JoinType jointype, bool inner_unique);
+                                                          JoinType jointype,
+                                                          Relids ojrelids,
+                                                          bool inner_unique);
 static Hash *make_hash(Plan *lefttree,
                                           List *hashkeys,
                                           Oid skewTable,
@@ -252,7 +256,9 @@ static MergeJoin *make_mergejoin(List *tlist,
                                                                 bool *mergereversals,
                                                                 bool *mergenullsfirst,
                                                                 Plan *lefttree, Plan *righttree,
-                                                                JoinType jointype, bool inner_unique,
+                                                                JoinType jointype,
+                                                                Relids ojrelids,
+                                                                bool inner_unique,
                                                                 bool skip_mark_restore);
 static Sort *make_sort(Plan *lefttree, int numCols,
                                           AttrNumber *sortColIdx, Oid *sortOperators,
@@ -4191,6 +4197,7 @@ create_nestloop_plan(PlannerInfo *root,
        Plan       *outer_plan;
        Plan       *inner_plan;
        Relids          outerrelids;
+       Relids          ojrelids;
        List       *tlist = build_path_tlist(root, &best_path->jpath.path);
        List       *joinrestrictclauses = best_path->jpath.joinrestrictinfo;
        List       *joinclauses;
@@ -4257,6 +4264,11 @@ create_nestloop_plan(PlannerInfo *root,
                        replace_nestloop_params(root, (Node *) otherclauses);
        }
 
+       /* Identify any outer joins computed at this level */
+       ojrelids = bms_difference(best_path->jpath.path.parent->relids,
+                                                         bms_union(best_path->jpath.outerjoinpath->parent->relids,
+                                                                               best_path->jpath.innerjoinpath->parent->relids));
+
        /*
         * Identify any nestloop parameters that should be supplied by this join
         * node, and remove them from root->curOuterParams.
@@ -4328,6 +4340,7 @@ create_nestloop_plan(PlannerInfo *root,
                                                          outer_plan,
                                                          inner_plan,
                                                          best_path->jpath.jointype,
+                                                         ojrelids,
                                                          best_path->jpath.inner_unique);
 
        copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
@@ -4342,6 +4355,7 @@ create_mergejoin_plan(PlannerInfo *root,
        MergeJoin  *join_plan;
        Plan       *outer_plan;
        Plan       *inner_plan;
+       Relids          ojrelids;
        List       *tlist = build_path_tlist(root, &best_path->jpath.path);
        List       *joinclauses;
        List       *otherclauses;
@@ -4420,6 +4434,11 @@ create_mergejoin_plan(PlannerInfo *root,
        mergeclauses = get_switched_clauses(best_path->path_mergeclauses,
                                                                                best_path->jpath.outerjoinpath->parent->relids);
 
+       /* Identify any outer joins computed at this level */
+       ojrelids = bms_difference(best_path->jpath.path.parent->relids,
+                                                         bms_union(outer_path->parent->relids,
+                                                                               inner_path->parent->relids));
+
        /*
         * Create explicit sort nodes for the outer and inner paths if necessary.
         */
@@ -4680,6 +4699,7 @@ create_mergejoin_plan(PlannerInfo *root,
                                                           outer_plan,
                                                           inner_plan,
                                                           best_path->jpath.jointype,
+                                                          ojrelids,
                                                           best_path->jpath.inner_unique,
                                                           best_path->skip_mark_restore);
 
@@ -4697,6 +4717,7 @@ create_hashjoin_plan(PlannerInfo *root,
        Hash       *hash_plan;
        Plan       *outer_plan;
        Plan       *inner_plan;
+       Relids          ojrelids;
        List       *tlist = build_path_tlist(root, &best_path->jpath.path);
        List       *joinclauses;
        List       *otherclauses;
@@ -4845,6 +4866,11 @@ create_hashjoin_plan(PlannerInfo *root,
                hash_plan->rows_total = best_path->inner_rows_total;
        }
 
+       /* Identify any outer joins computed at this level */
+       ojrelids = bms_difference(best_path->jpath.path.parent->relids,
+                                                         bms_union(best_path->jpath.outerjoinpath->parent->relids,
+                                                                               best_path->jpath.innerjoinpath->parent->relids));
+
        join_plan = make_hashjoin(tlist,
                                                          joinclauses,
                                                          otherclauses,
@@ -4855,6 +4881,7 @@ create_hashjoin_plan(PlannerInfo *root,
                                                          outer_plan,
                                                          (Plan *) hash_plan,
                                                          best_path->jpath.jointype,
+                                                         ojrelids,
                                                          best_path->jpath.inner_unique);
 
        copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
@@ -5933,6 +5960,7 @@ make_nestloop(List *tlist,
                          Plan *lefttree,
                          Plan *righttree,
                          JoinType jointype,
+                         Relids ojrelids,
                          bool inner_unique)
 {
        NestLoop   *node = makeNode(NestLoop);
@@ -5945,6 +5973,7 @@ make_nestloop(List *tlist,
        node->join.jointype = jointype;
        node->join.inner_unique = inner_unique;
        node->join.joinqual = joinclauses;
+       node->join.ojrelids = ojrelids;
        node->nestParams = nestParams;
 
        return node;
@@ -5961,6 +5990,7 @@ make_hashjoin(List *tlist,
                          Plan *lefttree,
                          Plan *righttree,
                          JoinType jointype,
+                         Relids ojrelids,
                          bool inner_unique)
 {
        HashJoin   *node = makeNode(HashJoin);
@@ -5977,6 +6007,7 @@ make_hashjoin(List *tlist,
        node->join.jointype = jointype;
        node->join.inner_unique = inner_unique;
        node->join.joinqual = joinclauses;
+       node->join.ojrelids = ojrelids;
 
        return node;
 }
@@ -6016,6 +6047,7 @@ make_mergejoin(List *tlist,
                           Plan *lefttree,
                           Plan *righttree,
                           JoinType jointype,
+                          Relids ojrelids,
                           bool inner_unique,
                           bool skip_mark_restore)
 {
@@ -6035,6 +6067,7 @@ make_mergejoin(List *tlist,
        node->join.jointype = jointype;
        node->join.inner_unique = inner_unique;
        node->join.joinqual = joinclauses;
+       node->join.ojrelids = ojrelids;
 
        return node;
 }
index c95aaa6d85811019c17cd69a97d29e3fa395929f..fa65a6435ded9bf8162a765b1c32b9795b4d3542 100644 (file)
@@ -2489,13 +2489,13 @@ set_join_references(PlannerInfo *root, Join *join, int rtoffset)
 
        /*
         * Now we need to fix up the targetlist and qpqual, which are logically
-        * above the join.  This means that, if it's not an inner join, any Vars
-        * and PHVs appearing here should have nullingrels that include the
-        * effects of the outer join, ie they will have nullingrels equal to the
-        * input Vars' nullingrels plus the bit added by the outer join.  We don't
-        * currently have enough info available here to identify what that should
-        * be, so we just tell fix_join_expr to accept superset nullingrels
-        * matches instead of exact ones.
+        * above the join.  This means that, if it's an outer join with non-empty
+        * ojrelids, any Vars and PHVs appearing here should have nullingrels that
+        * include the effects of the outer join, ie they will have nullingrels
+        * equal to the input Vars' nullingrels plus the bit added by the outer
+        * join.  We don't currently have enough info available here to identify
+        * what that should be, so we just tell fix_join_expr to accept superset
+        * nullingrels matches instead of exact ones.
         */
        join->plan.targetlist = fix_join_expr(root,
                                                                                  join->plan.targetlist,
@@ -2503,7 +2503,7 @@ set_join_references(PlannerInfo *root, Join *join, int rtoffset)
                                                                                  inner_itlist,
                                                                                  (Index) 0,
                                                                                  rtoffset,
-                                                                                 (join->jointype == JOIN_INNER ? NRM_EQUAL : NRM_SUPERSET),
+                                                                                 (bms_is_empty(join->ojrelids) ? NRM_EQUAL : NRM_SUPERSET),
                                                                                  NUM_EXEC_TLIST((Plan *) join));
        join->plan.qual = fix_join_expr(root,
                                                                        join->plan.qual,
@@ -2511,7 +2511,7 @@ set_join_references(PlannerInfo *root, Join *join, int rtoffset)
                                                                        inner_itlist,
                                                                        (Index) 0,
                                                                        rtoffset,
-                                                                       (join->jointype == JOIN_INNER ? NRM_EQUAL : NRM_SUPERSET),
+                                                                       (bms_is_empty(join->ojrelids) ? NRM_EQUAL : NRM_SUPERSET),
                                                                        NUM_EXEC_QUAL((Plan *) join));
 
        pfree(outer_itlist);
index 14a1dfed2b99772f5895b949bed6cea58cfaafb5..b880ce0f4bee244118d8bf1311139bcdf8a0a7d2 100644 (file)
@@ -966,6 +966,7 @@ typedef struct CustomScan
  * inner_unique each outer tuple can match to no more than one inner tuple
  * joinqual:   qual conditions that came from JOIN/ON or JOIN/USING
  *                             (plan.qual contains conditions that came from WHERE)
+ * ojrelids:   outer joins completed at this level
  *
  * When jointype is INNER, joinqual and plan.qual are semantically
  * interchangeable.  For OUTER jointypes, the two are *not* interchangeable;
@@ -990,6 +991,7 @@ typedef struct Join
        bool            inner_unique;
        /* JOIN quals (in addition to plan.qual) */
        List       *joinqual;
+       Bitmapset  *ojrelids;
 } Join;
 
 /* ----------------