From 9b82f32cc8797390d660f39c63dadfebebbcd04a Mon Sep 17 00:00:00 2001 From: "Andrei V. Lepikhov" Date: Tue, 26 May 2026 15:53:04 +0200 Subject: [PATCH v1 1/2] Gate the NestLoop inner side with outer-only join clauses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In some cases, a LEFT JOIN’s NestLoop might have a clause that depends only on the outer side of the join. In such a case, if the outer tuple evaluates this clause as false, the result of the inner side scan operation will not impact the join’s output: inner side will always be replaced with NULL values. Treat such an outer-side clause as a ‘gating’ clause and push it into the inner side as a Result node. It will reduce the number of rescan operations to a bare minimum when the the gating clause evaluates to a true. This operation effectively forces parameterisation of the NestLoop JOIN, even if it was originally plain. To identify such cases, the NestLoop struct was supplemented with a flag that allows the executor to distinguish a ‘gated’ join from the parameterised NestLoop and to set the REWIND flag correctly. No costing change is made: the gating Result inherits its child's cost fields, and final_cost_nestloop() does not model the skipped inner rescans. --- .../postgres_fdw/expected/postgres_fdw.out | 14 +- src/backend/executor/nodeNestloop.c | 12 +- src/backend/optimizer/plan/createplan.c | 93 +++- src/include/nodes/plannodes.h | 2 + src/test/regress/expected/create_index.out | 21 +- src/test/regress/expected/join.out | 412 ++++++++++-------- src/test/regress/expected/predicate.out | 113 ++--- src/test/regress/expected/subselect.out | 17 +- src/test/regress/sql/join.sql | 27 ++ 9 files changed, 459 insertions(+), 252 deletions(-) diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out index e90289e4ab1..7aa86c057d5 100644 --- a/contrib/postgres_fdw/expected/postgres_fdw.out +++ b/contrib/postgres_fdw/expected/postgres_fdw.out @@ -2457,16 +2457,18 @@ SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 O --------------------------------------------------------------------------------------------------------------------------- Nested Loop Left Join Output: (13), ft2.c1 - Join Filter: (13 = ft2.c1) -> Foreign Scan on public.ft2 Output: ft2.c1 Remote SQL: SELECT "C 1" FROM "S 1"."T 1" WHERE (("C 1" >= 10)) AND (("C 1" <= 15)) ORDER BY "C 1" ASC NULLS LAST - -> Materialize + -> Result Output: (13) - -> Foreign Scan on public.ft1 - Output: 13 - Remote SQL: SELECT NULL FROM "S 1"."T 1" WHERE (("C 1" = 13)) -(11 rows) + One-Time Filter: (13 = ft2.c1) + -> Materialize + Output: (13) + -> Foreign Scan on public.ft1 + Output: 13 + Remote SQL: SELECT NULL FROM "S 1"."T 1" WHERE (("C 1" = 13)) +(13 rows) SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15; a | c1 diff --git a/src/backend/executor/nodeNestloop.c b/src/backend/executor/nodeNestloop.c index 809311ab513..b49aa8190ea 100644 --- a/src/backend/executor/nodeNestloop.c +++ b/src/backend/executor/nodeNestloop.c @@ -290,12 +290,16 @@ ExecInitNestLoop(NestLoop *node, EState *estate, int eflags) * * If we have no parameters to pass into the inner rel from the outer, * tell the inner child that cheap rescans would be good. If we do have - * such parameters, then there is no point in REWIND support at all in the - * inner child, because it will always be rescanned with fresh parameter - * values. + * such parameters, there is normally no point in REWIND support in the + * inner child, because it will be rescanned with fresh parameter values. + * + * The exception is a 'gated' nested loop join, where the only parameters + * feed a gating Result that caps the inner subtree and is re-checked once + * per outer tuple. The plan below that gate is parameter-independent, so + * we keep the REWIND flag. */ outerPlanState(nlstate) = ExecInitNode(outerPlan(node), estate, eflags); - if (node->nestParams == NIL) + if (node->nestParams == NIL || node->keep_inner_rewind) eflags |= EXEC_FLAG_REWIND; else eflags &= ~EXEC_FLAG_REWIND; diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c index de6a183da79..3d09c32d084 100644 --- a/src/backend/optimizer/plan/createplan.c +++ b/src/backend/optimizer/plan/createplan.c @@ -4191,8 +4191,11 @@ create_nestloop_plan(PlannerInfo *root, Plan *outer_plan; Plan *inner_plan; Relids outerrelids; + Relids joinrelids = best_path->jpath.path.parent->relids; List *tlist = build_path_tlist(root, &best_path->jpath.path); List *joinrestrictclauses = best_path->jpath.joinrestrictinfo; + List *gating_clauses = NIL; + bool keep_inner_rewind = false; List *joinclauses; List *otherclauses; List *nestParams; @@ -4238,8 +4241,24 @@ create_nestloop_plan(PlannerInfo *root, if (IS_OUTER_JOIN(best_path->jpath.jointype)) { extract_actual_join_clauses(joinrestrictclauses, - best_path->jpath.path.parent->relids, + joinrelids, &joinclauses, &otherclauses); + + /* + * Collect the join clauses that reference only the outer rel: they are + * constant for a given outer tuple, so the loop further down can gate + * the inner side with them instead of re-checking them per inner row. + */ + foreach_node(RestrictInfo, rinfo, joinrestrictclauses) + { + if (bms_is_empty(rinfo->clause_relids) || + !bms_is_subset(rinfo->clause_relids, outerrelids)) + continue; + + Assert(!RINFO_IS_PUSHED_DOWN(rinfo, joinrelids)); + + gating_clauses = lappend(gating_clauses, rinfo->clause); + } } else { @@ -4248,6 +4267,14 @@ create_nestloop_plan(PlannerInfo *root, otherclauses = NIL; } + /* + * Pull the outer-only clauses out of joinclauses; they become a gating + * qual on the inner side below. Do this before parameterization, while + * the expressions are still un-parameterized. + */ + if (gating_clauses != NIL) + joinclauses = list_difference_ptr(joinclauses, gating_clauses); + /* Replace any outer-relation variables with nestloop params */ if (best_path->jpath.path.param_info) { @@ -4257,6 +4284,68 @@ create_nestloop_plan(PlannerInfo *root, replace_nestloop_params(root, (Node *) otherclauses); } + /* + * The gating clauses must be parameterized so the gating Result can + * evaluate them against the current outer tuple. + */ + if (gating_clauses != NIL) + { + Relids tmpOuterRels = root->curOuterRels; + Plan *subplan = inner_plan; + + /* + * Keep the inner side rewindable across rescans, but only when this + * gate is the sole source of nestloop parameters. + */ + keep_inner_rewind = + !bms_overlap(PATH_REQ_OUTER(best_path->jpath.innerjoinpath), + outerrelids); + + Assert(bms_is_subset(pull_varnos(root, (Node *) gating_clauses), + outerrelids)); + + /* + * replace_nestloop_params only converts Vars in curOuterRels, which + * was restored above and no longer covers this join's outer relids, + * so re-add them across the call. + */ + root->curOuterRels = bms_union(root->curOuterRels, outerrelids); + gating_clauses = (List *) + replace_nestloop_params(root, (Node *) gating_clauses); + bms_free(root->curOuterRels); + root->curOuterRels = tmpOuterRels; + + /* + * Avoid stacking Result nodes. If the inner plan is already a Result, + * merge our parameterized clauses into its resconstantqual. This + * mirrors create_gating_plan()'s logic. + */ + if (IsA(subplan, Result)) + { + Result *existing = (Result *) subplan; + List *clauses = (List *) existing->resconstantqual; + + Assert(clauses == NULL || IsA(clauses, List)); + + clauses = list_concat(gating_clauses, clauses); + clauses = order_qual_clauses(root, clauses); + existing->resconstantqual = (Node *) clauses; + + /* Gating quals could be unsafe, so use the Path's safety flag */ + existing->plan.parallel_safe = best_path->jpath.path.parallel_safe; + } + else + { + inner_plan = (Plan *) make_gating_result(subplan->targetlist, + (Node *) gating_clauses, + subplan); + copy_plan_costsize(inner_plan, subplan); + + /* Gating quals could be unsafe, so use the Path's safety flag */ + inner_plan->parallel_safe = best_path->jpath.path.parallel_safe; + } + } + /* * Identify any nestloop parameters that should be supplied by this join * node, and remove them from root->curOuterParams. @@ -4330,6 +4419,8 @@ create_nestloop_plan(PlannerInfo *root, best_path->jpath.jointype, best_path->jpath.inner_unique); + join_plan->keep_inner_rewind = keep_inner_rewind; + copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path); return join_plan; diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h index 14a1dfed2b9..5a3fe27ca35 100644 --- a/src/include/nodes/plannodes.h +++ b/src/include/nodes/plannodes.h @@ -1008,6 +1008,8 @@ typedef struct NestLoop Join join; /* list of NestLoopParam nodes */ List *nestParams; + /* keep inner side rewindable: gate is the sole source of nestloop params */ + bool keep_inner_rewind; } NestLoop; typedef struct NestLoopParam diff --git a/src/test/regress/expected/create_index.out b/src/test/regress/expected/create_index.out index 55538c4c41e..71fef89354a 100644 --- a/src/test/regress/expected/create_index.out +++ b/src/test/regress/expected/create_index.out @@ -2250,19 +2250,20 @@ EXPLAIN (COSTS OFF) SELECT count(*) FROM tenk1 LEFT JOIN tenk2 ON tenk1.hundred = 42 AND (tenk2.thousand = 42 OR tenk2.thousand = 41 OR tenk2.tenthous = 2) AND tenk2.hundred = tenk1.hundred; - QUERY PLAN ------------------------------------------------------------------------------------- + QUERY PLAN +------------------------------------------------------------------------------------------ Aggregate -> Nested Loop Left Join - Join Filter: (tenk1.hundred = 42) -> Index Only Scan using tenk1_hundred on tenk1 - -> Memoize - Cache Key: tenk1.hundred - Cache Mode: logical - -> Index Scan using tenk2_hundred on tenk2 - Index Cond: (hundred = tenk1.hundred) - Filter: ((thousand = 42) OR (thousand = 41) OR (tenthous = 2)) -(10 rows) + -> Result + One-Time Filter: (tenk1.hundred = 42) + -> Memoize + Cache Key: tenk1.hundred + Cache Mode: logical + -> Index Scan using tenk2_hundred on tenk2 + Index Cond: (hundred = tenk1.hundred) + Filter: ((thousand = 42) OR (thousand = 41) OR (tenthous = 2)) +(11 rows) -- -- Check behavior with duplicate index column contents diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out index 78bf022f7b4..640ac750d51 100644 --- a/src/test/regress/expected/join.out +++ b/src/test/regress/expected/join.out @@ -2424,21 +2424,22 @@ from int4_tbl t1, int4_tbl t2 left join int4_tbl t3 on t3.f1 > 0 left join int4_tbl t4 on t3.f1 > 1 where t4.f1 is null; - QUERY PLAN -------------------------------------------------------- + QUERY PLAN +------------------------------------------------------------- Nested Loop -> Nested Loop Left Join Filter: (t4.f1 IS NULL) -> Seq Scan on int4_tbl t2 -> Materialize -> Nested Loop Left Join - Join Filter: (t3.f1 > 1) -> Seq Scan on int4_tbl t3 Filter: (f1 > 0) - -> Materialize - -> Seq Scan on int4_tbl t4 + -> Result + One-Time Filter: (t3.f1 > 1) + -> Materialize + -> Seq Scan on int4_tbl t4 -> Seq Scan on int4_tbl t1 -(12 rows) +(13 rows) select t1.f1 from int4_tbl t1, int4_tbl t2 @@ -2454,21 +2455,23 @@ select * from int4_tbl t1 left join int4_tbl t2 on true left join int4_tbl t3 on t2.f1 > 0 left join int4_tbl t4 on t3.f1 > 0; - QUERY PLAN -------------------------------------------------------- + QUERY PLAN +------------------------------------------------------------- Nested Loop Left Join -> Seq Scan on int4_tbl t1 -> Materialize -> Nested Loop Left Join - Join Filter: (t3.f1 > 0) -> Nested Loop Left Join - Join Filter: (t2.f1 > 0) -> Seq Scan on int4_tbl t2 + -> Result + One-Time Filter: (t2.f1 > 0) + -> Materialize + -> Seq Scan on int4_tbl t3 + -> Result + One-Time Filter: (t3.f1 > 0) -> Materialize - -> Seq Scan on int4_tbl t3 - -> Materialize - -> Seq Scan on int4_tbl t4 -(12 rows) + -> Seq Scan on int4_tbl t4 +(14 rows) explain (costs off) select * from onek t1 @@ -2525,7 +2528,6 @@ select * from int4_tbl t1 QUERY PLAN ------------------------------------------------- Nested Loop Left Join - Join Filter: (t2.f1 = t3.f1) -> Nested Loop Left Join -> Nested Loop Left Join -> Seq Scan on int4_tbl t1 @@ -2533,9 +2535,11 @@ select * from int4_tbl t1 -> Seq Scan on int4_tbl t2 -> Materialize -> Seq Scan on int4_tbl t3 - -> Materialize - -> Seq Scan on int4_tbl t4 -(11 rows) + -> Result + One-Time Filter: (t2.f1 = t3.f1) + -> Materialize + -> Seq Scan on int4_tbl t4 +(12 rows) explain (costs off) select * from int4_tbl t1 @@ -2563,23 +2567,25 @@ select * from int4_tbl t1 left join (int4_tbl t2 left join int4_tbl t3 on t2.f1 > 0) on t2.f1 > 1 left join int4_tbl t4 on t2.f1 > 2 and t3.f1 > 3 where t1.f1 = coalesce(t2.f1, 1); - QUERY PLAN ----------------------------------------------------- + QUERY PLAN +-------------------------------------------------------- Nested Loop Left Join - Join Filter: ((t2.f1 > 2) AND (t3.f1 > 3)) -> Nested Loop Left Join - Join Filter: (t2.f1 > 0) -> Nested Loop Left Join Filter: (t1.f1 = COALESCE(t2.f1, 1)) -> Seq Scan on int4_tbl t1 -> Materialize -> Seq Scan on int4_tbl t2 Filter: (f1 > 1) + -> Result + One-Time Filter: (t2.f1 > 0) + -> Materialize + -> Seq Scan on int4_tbl t3 + -> Result + One-Time Filter: ((t2.f1 > 2) AND (t3.f1 > 3)) -> Materialize - -> Seq Scan on int4_tbl t3 - -> Materialize - -> Seq Scan on int4_tbl t4 -(14 rows) + -> Seq Scan on int4_tbl t4 +(16 rows) explain (costs off) select * from int4_tbl t1 @@ -2588,22 +2594,24 @@ select * from int4_tbl t1 where t3.f1 is null) s left join tenk1 t4 on s.f1 > 1) on s.f1 = t1.f1; - QUERY PLAN -------------------------------------------------- + QUERY PLAN +------------------------------------------------------- Hash Right Join Hash Cond: (t2.f1 = t1.f1) -> Nested Loop Left Join - Join Filter: (t2.f1 > 1) -> Nested Loop Left Join - Join Filter: (t2.f1 > 0) Filter: (t3.f1 IS NULL) -> Seq Scan on int4_tbl t2 - -> Materialize - -> Seq Scan on int4_tbl t3 - -> Seq Scan on tenk1 t4 + -> Result + One-Time Filter: (t2.f1 > 0) + -> Materialize + -> Seq Scan on int4_tbl t3 + -> Result + One-Time Filter: (t2.f1 > 1) + -> Seq Scan on tenk1 t4 -> Hash -> Seq Scan on int4_tbl t1 -(13 rows) +(15 rows) explain (costs off) select * from int4_tbl t1 @@ -2615,20 +2623,22 @@ select * from int4_tbl t1 QUERY PLAN ----------------------------------------------------------------- Nested Loop Left Join - Join Filter: (t2.f1 > 1) -> Hash Right Join Hash Cond: (t2.f1 = t1.f1) -> Nested Loop Left Join - Join Filter: (t2.f1 > 0) Filter: (t2.f1 <> COALESCE(t3.f1, '-1'::integer)) -> Seq Scan on int4_tbl t2 - -> Materialize - -> Seq Scan on int4_tbl t3 + -> Result + One-Time Filter: (t2.f1 > 0) + -> Materialize + -> Seq Scan on int4_tbl t3 -> Hash -> Seq Scan on int4_tbl t1 - -> Materialize - -> Seq Scan on tenk1 t4 -(14 rows) + -> Result + One-Time Filter: (t2.f1 > 1) + -> Materialize + -> Seq Scan on tenk1 t4 +(16 rows) explain (costs off) select * from onek t1 @@ -2680,17 +2690,18 @@ select * from int8_tbl t1 left join lateral (select * from int8_tbl t3 where t3.q1 = t2.q1 offset 0) s on t2.q1 = 1; - QUERY PLAN -------------------------------------------- + QUERY PLAN +-------------------------------------------------- Nested Loop Left Join -> Seq Scan on int8_tbl t1 -> Materialize -> Nested Loop Left Join - Join Filter: (t2.q1 = 1) -> Seq Scan on int8_tbl t2 - -> Seq Scan on int8_tbl t3 - Filter: (q1 = t2.q1) -(8 rows) + -> Result + One-Time Filter: (t2.q1 = 1) + -> Seq Scan on int8_tbl t3 + Filter: (q1 = t2.q1) +(9 rows) explain (costs off) select * from int8_tbl t1 @@ -2698,16 +2709,17 @@ select * from int8_tbl t1 left join lateral (select * from generate_series(t2.q1, 100)) s on t2.q1 = 1; - QUERY PLAN ----------------------------------------------------- + QUERY PLAN +---------------------------------------------------------- Nested Loop Left Join -> Seq Scan on int8_tbl t1 -> Materialize -> Nested Loop Left Join - Join Filter: (t2.q1 = 1) -> Seq Scan on int8_tbl t2 - -> Function Scan on generate_series -(7 rows) + -> Result + One-Time Filter: (t2.q1 = 1) + -> Function Scan on generate_series +(8 rows) explain (costs off) select * from int8_tbl t1 @@ -2715,16 +2727,17 @@ select * from int8_tbl t1 left join lateral (select t2.q1 from int8_tbl t3) s on t2.q1 = 1; - QUERY PLAN -------------------------------------------- + QUERY PLAN +-------------------------------------------------- Nested Loop Left Join -> Seq Scan on int8_tbl t1 -> Materialize -> Nested Loop Left Join - Join Filter: (t2.q1 = 1) -> Seq Scan on int8_tbl t2 - -> Seq Scan on int8_tbl t3 -(7 rows) + -> Result + One-Time Filter: (t2.q1 = 1) + -> Seq Scan on int8_tbl t3 +(8 rows) explain (costs off) select * from onek t1 @@ -2732,20 +2745,21 @@ select * from onek t1 left join lateral (select * from onek t3 where t3.two = t2.two offset 0) s on t2.unique1 = 1; - QUERY PLAN --------------------------------------------------- + QUERY PLAN +-------------------------------------------------------- Nested Loop Left Join -> Seq Scan on onek t1 -> Materialize -> Nested Loop Left Join - Join Filter: (t2.unique1 = 1) -> Seq Scan on onek t2 - -> Memoize - Cache Key: t2.two - Cache Mode: binary - -> Seq Scan on onek t3 - Filter: (two = t2.two) -(11 rows) + -> Result + One-Time Filter: (t2.unique1 = 1) + -> Memoize + Cache Key: t2.two + Cache Mode: binary + -> Seq Scan on onek t3 + Filter: (two = t2.two) +(12 rows) -- -- check a case where we formerly got confused by conflicting sort orders @@ -4630,11 +4644,12 @@ select unique1, x from tenk1 left join f_immutable_int4(1) x on unique1 = x; QUERY PLAN ---------------------------------------------------- Nested Loop Left Join - Join Filter: (tenk1.unique1 = 1) -> Index Only Scan using tenk1_unique1 on tenk1 - -> Materialize - -> Result -(5 rows) + -> Result + One-Time Filter: (tenk1.unique1 = 1) + -> Materialize + -> Result +(6 rows) explain (costs off) select unique1, x from tenk1 right join f_immutable_int4(1) x on unique1 = x; @@ -4681,17 +4696,17 @@ from nt3 as nt3 ) as ss2 on ss2.id = nt3.nt2_id where nt3.id = 1 and ss2.b3; - QUERY PLAN ----------------------------------------------- + QUERY PLAN +------------------------------------------------- Nested Loop Left Join Filter: ((nt2.b1 OR ((0) = 42))) -> Index Scan using nt3_pkey on nt3 Index Cond: (id = 1) -> Nested Loop Left Join - Join Filter: (0 = nt2.nt1_id) -> Index Scan using nt2_pkey on nt2 Index Cond: (id = nt3.nt2_id) -> Result + One-Time Filter: (0 = nt2.nt1_id) (9 rows) drop function f_immutable_int4(int); @@ -4931,7 +4946,6 @@ select count(*) from ------------------------------------------------------------------------- Aggregate -> Nested Loop Left Join - Join Filter: (a.unique2 = b.unique1) -> Nested Loop -> Nested Loop -> Seq Scan on int4_tbl @@ -4941,9 +4955,11 @@ select count(*) from Index Cond: (thousand = int4_tbl.f1) -> Index Scan using tenk1_unique1 on tenk1 a Index Cond: (unique1 = b.unique2) - -> Index Only Scan using tenk1_thous_tenthous on tenk1 c - Index Cond: (thousand = a.thousand) -(14 rows) + -> Result + One-Time Filter: (a.unique2 = b.unique1) + -> Index Only Scan using tenk1_thous_tenthous on tenk1 c + Index Cond: (thousand = a.thousand) +(15 rows) select count(*) from tenk1 a join tenk1 b on a.unique1 = b.unique2 @@ -4968,7 +4984,6 @@ select b.unique1 from -> Nested Loop Left Join -> Seq Scan on int4_tbl i2 -> Nested Loop Left Join - Join Filter: (b.unique1 = 42) -> Nested Loop -> Nested Loop -> Seq Scan on int4_tbl i1 @@ -4976,9 +4991,11 @@ select b.unique1 from Index Cond: ((thousand = i1.f1) AND (tenthous = i2.f1)) -> Index Scan using tenk1_unique1 on tenk1 a Index Cond: (unique1 = b.unique2) - -> Index Only Scan using tenk1_thous_tenthous on tenk1 c - Index Cond: (thousand = a.thousand) -(15 rows) + -> Result + One-Time Filter: (b.unique1 = 42) + -> Index Only Scan using tenk1_thous_tenthous on tenk1 c + Index Cond: (thousand = a.thousand) +(16 rows) select b.unique1 from tenk1 a join tenk1 b on a.unique1 = b.unique2 @@ -5277,39 +5294,41 @@ select t1.* from on (t1.f1 = b1.d1) left join int4_tbl i4 on (i8.q2 = i4.f1); - QUERY PLAN ----------------------------------------------------------------------- + QUERY PLAN +---------------------------------------------------------------------------- Hash Left Join Output: t1.f1 Hash Cond: (i8.q2 = i4.f1) -> Nested Loop Left Join Output: t1.f1, i8.q2 - Join Filter: (t1.f1 = '***'::text) -> Seq Scan on public.text_tbl t1 Output: t1.f1 - -> Materialize + -> Result Output: i8.q2 - -> Hash Right Join + One-Time Filter: (t1.f1 = '***'::text) + -> Materialize Output: i8.q2 - Hash Cond: ((NULL::integer) = i8b1.q2) - -> Hash Join - Output: i8.q2, (NULL::integer) - Hash Cond: (i8.q1 = i8b2.q1) - -> Seq Scan on public.int8_tbl i8 - Output: i8.q1, i8.q2 + -> Hash Right Join + Output: i8.q2 + Hash Cond: ((NULL::integer) = i8b1.q2) + -> Hash Join + Output: i8.q2, (NULL::integer) + Hash Cond: (i8.q1 = i8b2.q1) + -> Seq Scan on public.int8_tbl i8 + Output: i8.q1, i8.q2 + -> Hash + Output: i8b2.q1, (NULL::integer) + -> Seq Scan on public.int8_tbl i8b2 + Output: i8b2.q1, NULL::integer -> Hash - Output: i8b2.q1, (NULL::integer) - -> Seq Scan on public.int8_tbl i8b2 - Output: i8b2.q1, NULL::integer - -> Hash - Output: i8b1.q2 - -> Seq Scan on public.int8_tbl i8b1 Output: i8b1.q2 + -> Seq Scan on public.int8_tbl i8b1 + Output: i8b1.q2 -> Hash Output: i4.f1 -> Seq Scan on public.int4_tbl i4 Output: i4.f1 -(30 rows) +(32 rows) select t1.* from text_tbl t1 @@ -5338,43 +5357,45 @@ select t1.* from on (t1.f1 = b1.d1) left join int4_tbl i4 on (i8.q2 = i4.f1); - QUERY PLAN ----------------------------------------------------------------------------- + QUERY PLAN +---------------------------------------------------------------------------------- Hash Left Join Output: t1.f1 Hash Cond: (i8.q2 = i4.f1) -> Nested Loop Left Join Output: t1.f1, i8.q2 - Join Filter: (t1.f1 = '***'::text) -> Seq Scan on public.text_tbl t1 Output: t1.f1 - -> Materialize + -> Result Output: i8.q2 - -> Hash Right Join + One-Time Filter: (t1.f1 = '***'::text) + -> Materialize Output: i8.q2 - Hash Cond: ((NULL::integer) = i8b1.q2) -> Hash Right Join - Output: i8.q2, (NULL::integer) - Hash Cond: (i8b2.q1 = i8.q1) - -> Nested Loop - Output: i8b2.q1, NULL::integer - -> Seq Scan on public.int8_tbl i8b2 - Output: i8b2.q1, i8b2.q2 - -> Materialize - -> Seq Scan on public.int4_tbl i4b2 - -> Hash - Output: i8.q1, i8.q2 - -> Seq Scan on public.int8_tbl i8 + Output: i8.q2 + Hash Cond: ((NULL::integer) = i8b1.q2) + -> Hash Right Join + Output: i8.q2, (NULL::integer) + Hash Cond: (i8b2.q1 = i8.q1) + -> Nested Loop + Output: i8b2.q1, NULL::integer + -> Seq Scan on public.int8_tbl i8b2 + Output: i8b2.q1, i8b2.q2 + -> Materialize + -> Seq Scan on public.int4_tbl i4b2 + -> Hash Output: i8.q1, i8.q2 - -> Hash - Output: i8b1.q2 - -> Seq Scan on public.int8_tbl i8b1 + -> Seq Scan on public.int8_tbl i8 + Output: i8.q1, i8.q2 + -> Hash Output: i8b1.q2 + -> Seq Scan on public.int8_tbl i8b1 + Output: i8b1.q2 -> Hash Output: i4.f1 -> Seq Scan on public.int4_tbl i4 Output: i4.f1 -(34 rows) +(36 rows) select t1.* from text_tbl t1 @@ -5404,46 +5425,48 @@ select t1.* from on (t1.f1 = b1.d1) left join int4_tbl i4 on (i8.q2 = i4.f1); - QUERY PLAN ----------------------------------------------------------------------------- + QUERY PLAN +---------------------------------------------------------------------------------- Hash Left Join Output: t1.f1 Hash Cond: (i8.q2 = i4.f1) -> Nested Loop Left Join Output: t1.f1, i8.q2 - Join Filter: (t1.f1 = '***'::text) -> Seq Scan on public.text_tbl t1 Output: t1.f1 - -> Materialize + -> Result Output: i8.q2 - -> Hash Right Join + One-Time Filter: (t1.f1 = '***'::text) + -> Materialize Output: i8.q2 - Hash Cond: ((NULL::integer) = i8b1.q2) -> Hash Right Join - Output: i8.q2, (NULL::integer) - Hash Cond: (i8b2.q1 = i8.q1) - -> Hash Join - Output: i8b2.q1, NULL::integer - Hash Cond: (i8b2.q1 = i4b2.f1) - -> Seq Scan on public.int8_tbl i8b2 - Output: i8b2.q1, i8b2.q2 - -> Hash - Output: i4b2.f1 - -> Seq Scan on public.int4_tbl i4b2 + Output: i8.q2 + Hash Cond: ((NULL::integer) = i8b1.q2) + -> Hash Right Join + Output: i8.q2, (NULL::integer) + Hash Cond: (i8b2.q1 = i8.q1) + -> Hash Join + Output: i8b2.q1, NULL::integer + Hash Cond: (i8b2.q1 = i4b2.f1) + -> Seq Scan on public.int8_tbl i8b2 + Output: i8b2.q1, i8b2.q2 + -> Hash Output: i4b2.f1 - -> Hash - Output: i8.q1, i8.q2 - -> Seq Scan on public.int8_tbl i8 + -> Seq Scan on public.int4_tbl i4b2 + Output: i4b2.f1 + -> Hash Output: i8.q1, i8.q2 - -> Hash - Output: i8b1.q2 - -> Seq Scan on public.int8_tbl i8b1 + -> Seq Scan on public.int8_tbl i8 + Output: i8.q1, i8.q2 + -> Hash Output: i8b1.q2 + -> Seq Scan on public.int8_tbl i8b1 + Output: i8b1.q2 -> Hash Output: i4.f1 -> Seq Scan on public.int4_tbl i4 Output: i4.f1 -(37 rows) +(39 rows) select t1.* from text_tbl t1 @@ -5537,8 +5560,8 @@ select 1 from join int4_tbl i42 on ss1.a is null or i8.q1 <> i8.q2 right join (select 2 as b) ss2 on ss2.b < i4.f1; - QUERY PLAN ------------------------------------------------------------ + QUERY PLAN +---------------------------------------------------------------- Nested Loop Left Join -> Result -> Nested Loop @@ -5546,16 +5569,17 @@ select 1 from Join Filter: NULL::boolean Filter: (((1) IS NULL) OR (i8.q1 <> i8.q2)) -> Nested Loop Left Join - Join Filter: (i4.f1 IS NOT NULL) -> Seq Scan on int4_tbl i4 Filter: (2 < f1) - -> Materialize - -> Seq Scan on int8_tbl i8 + -> Result + One-Time Filter: (i4.f1 IS NOT NULL) + -> Materialize + -> Seq Scan on int8_tbl i8 -> Result One-Time Filter: false -> Materialize -> Seq Scan on int4_tbl i42 -(16 rows) +(17 rows) -- -- test for appropriate join order in the presence of lateral references @@ -6216,7 +6240,6 @@ from int8_tbl t1 QUERY PLAN ------------------------------------------------- Nested Loop Left Join - Join Filter: (t2.q2 < t3.unique2) -> Nested Loop Left Join Join Filter: (t2.q1 > t3.unique1) -> Hash Left Join @@ -6226,9 +6249,11 @@ from int8_tbl t1 -> Seq Scan on int8_tbl t2 -> Materialize -> Seq Scan on onek t3 - -> Materialize - -> Seq Scan on onek t4 -(13 rows) + -> Result + One-Time Filter: (t2.q2 < t3.unique2) + -> Materialize + -> Seq Scan on onek t4 +(14 rows) -- bug #19460: we need to clean up RestrictInfos more than we had been doing explain (costs off) @@ -6480,11 +6505,12 @@ select 1 from a t1 -> Seq Scan on a t1 -> Materialize -> Nested Loop Left Join - Join Filter: (t2.id = 1) -> Index Only Scan using a_pkey on a t2 Index Cond: (id = 1) - -> Seq Scan on a t3 -(8 rows) + -> Result + One-Time Filter: (t2.id = 1) + -> Seq Scan on a t3 +(9 rows) -- check join removal works when uniqueness of the join condition is enforced -- by a UNION @@ -7931,14 +7957,15 @@ explain (costs off) select * from emp1 t1 inner join emp1 t2 on t1.id = t2.id left join emp1 t3 on t1.id > 1 and t1.id < 2; - QUERY PLAN ----------------------------------------------- + QUERY PLAN +-------------------------------------------------------- Nested Loop Left Join - Join Filter: ((t2.id > 1) AND (t2.id < 2)) -> Seq Scan on emp1 t2 - -> Materialize - -> Seq Scan on emp1 t3 -(5 rows) + -> Result + One-Time Filter: ((t2.id > 1) AND (t2.id < 2)) + -> Materialize + -> Seq Scan on emp1 t3 +(6 rows) -- Check that SJE doesn't replace the target relation EXPLAIN (COSTS OFF) @@ -7959,14 +7986,16 @@ EXPLAIN (COSTS OFF) SELECT * FROM emp1 t1 INNER JOIN emp1 t2 ON t1.id = t2.id LEFT JOIN emp1 t3 ON t1.code = 1 AND (t2.code = t3.code OR t2.code = 1); - QUERY PLAN ---------------------------------------------------------------------------- + QUERY PLAN +------------------------------------------------------- Nested Loop Left Join - Join Filter: ((t2.code = 1) AND ((t2.code = t3.code) OR (t2.code = 1))) + Join Filter: ((t2.code = t3.code) OR (t2.code = 1)) -> Seq Scan on emp1 t2 - -> Materialize - -> Seq Scan on emp1 t3 -(5 rows) + -> Result + One-Time Filter: (t2.code = 1) + -> Materialize + -> Seq Scan on emp1 t3 +(7 rows) INSERT INTO emp1 VALUES (1, 1), (2, 1); WITH t1 AS (SELECT * FROM emp1) @@ -8141,11 +8170,12 @@ SELECT 1 AS c1 FROM sl sl1 LEFT JOIN (sl AS sl2 NATURAL JOIN sl AS sl3) Nested Loop Left Join -> Seq Scan on sl sl1 -> Nested Loop Left Join - Join Filter: sl3.bool_col -> Seq Scan on sl sl3 Filter: (bool_col AND (a IS NOT NULL) AND (b IS NOT NULL) AND (c IS NOT NULL) AND (bool_col IS NOT NULL)) - -> Seq Scan on sl sl4 -(7 rows) + -> Result + One-Time Filter: sl3.bool_col + -> Seq Scan on sl sl4 +(8 rows) -- Check optimization disabling if it will violate special join conditions. -- Two identical joined relations satisfies self join removal conditions but @@ -10143,3 +10173,43 @@ SELECT COUNT(*) FROM onek t1 LEFT JOIN tenk1 t2 19000 (1 row) +-- Outer-only ON-clauses become a gating Result on the inner side; for a +-- parameter-independent inner, NestLoop.keep_inner_rewind retains REWIND so +-- a Materialize below the gate replays its buffer across rescans rather than +-- rebuilding it for every outer tuple that passes the gate. +create function platform_independent_explain(query text) returns setof text +language plpgsql as +$$ +declare + ln text; +begin + for ln in + execute format('explain (analyze, costs off, summary off, timing off, buffers off) %s', query) + loop + ln := regexp_replace(ln, 'Maximum Storage: \d+', 'Maximum Storage: N'); + return next ln; + end loop; +end; +$$; +set enable_hashjoin = off; +set enable_mergejoin = off; +select platform_independent_explain(' + select count(*) from onek t1 left join int4_tbl t2 + on (t1.unique1 = t2.f1 and t1.hundred in (1, 2))'); + platform_independent_explain +---------------------------------------------------------------------------- + Aggregate (actual rows=1.00 loops=1) + -> Nested Loop Left Join (actual rows=1000.00 loops=1) + Join Filter: (t1.unique1 = t2.f1) + Rows Removed by Join Filter: 1000 + -> Seq Scan on onek t1 (actual rows=1000.00 loops=1) + -> Result (actual rows=1.00 loops=1000) + One-Time Filter: (t1.hundred = ANY ('{1,2}'::integer[])) + -> Materialize (actual rows=5.00 loops=200) + Storage: Memory Maximum Storage: NkB + -> Seq Scan on int4_tbl t2 (actual rows=5.00 loops=1) +(10 rows) + +reset enable_hashjoin; +reset enable_mergejoin; +drop function platform_independent_explain(text); diff --git a/src/test/regress/expected/predicate.out b/src/test/regress/expected/predicate.out index feae77cb840..357c1ad1e08 100644 --- a/src/test/regress/expected/predicate.out +++ b/src/test/regress/expected/predicate.out @@ -114,10 +114,9 @@ EXPLAIN (COSTS OFF) SELECT * FROM pred_tab t1 FULL JOIN pred_tab t2 ON t1.a = t2.a LEFT JOIN pred_tab t3 ON t2.a IS NOT NULL; - QUERY PLAN -------------------------------------------- + QUERY PLAN +--------------------------------------------- Nested Loop Left Join - Join Filter: (t2.a IS NOT NULL) -> Merge Full Join Merge Cond: (t1.a = t2.a) -> Sort @@ -126,9 +125,11 @@ SELECT * FROM pred_tab t1 -> Sort Sort Key: t2.a -> Seq Scan on pred_tab t2 - -> Materialize - -> Seq Scan on pred_tab t3 -(12 rows) + -> Result + One-Time Filter: (t2.a IS NOT NULL) + -> Materialize + -> Seq Scan on pred_tab t3 +(13 rows) -- Ensure the IS_NULL qual is reduced to constant-FALSE, since a) it's on a NOT -- NULL column, and b) its Var is not nullable by any outer joins @@ -151,18 +152,20 @@ EXPLAIN (COSTS OFF) SELECT * FROM pred_tab t1 LEFT JOIN pred_tab t2 ON t1.a = 1 LEFT JOIN pred_tab t3 ON t2.a IS NULL; - QUERY PLAN -------------------------------------------- + QUERY PLAN +------------------------------------------------- Nested Loop Left Join - Join Filter: (t2.a IS NULL) -> Nested Loop Left Join - Join Filter: (t1.a = 1) -> Seq Scan on pred_tab t1 + -> Result + One-Time Filter: (t1.a = 1) + -> Materialize + -> Seq Scan on pred_tab t2 + -> Result + One-Time Filter: (t2.a IS NULL) -> Materialize - -> Seq Scan on pred_tab t2 - -> Materialize - -> Seq Scan on pred_tab t3 -(9 rows) + -> Seq Scan on pred_tab t3 +(11 rows) -- -- Tests for OR clauses in join clauses @@ -185,10 +188,9 @@ EXPLAIN (COSTS OFF) SELECT * FROM pred_tab t1 FULL JOIN pred_tab t2 ON t1.a = t2.a LEFT JOIN pred_tab t3 ON t2.a IS NOT NULL OR t2.b = 1; - QUERY PLAN ---------------------------------------------------- + QUERY PLAN +------------------------------------------------------------- Nested Loop Left Join - Join Filter: ((t2.a IS NOT NULL) OR (t2.b = 1)) -> Merge Full Join Merge Cond: (t1.a = t2.a) -> Sort @@ -197,9 +199,11 @@ SELECT * FROM pred_tab t1 -> Sort Sort Key: t2.a -> Seq Scan on pred_tab t2 - -> Materialize - -> Seq Scan on pred_tab t3 -(12 rows) + -> Result + One-Time Filter: ((t2.a IS NOT NULL) OR (t2.b = 1)) + -> Materialize + -> Seq Scan on pred_tab t3 +(13 rows) -- Ensure the OR clause is reduced to constant-FALSE when all OR branches are -- provably false @@ -222,18 +226,20 @@ EXPLAIN (COSTS OFF) SELECT * FROM pred_tab t1 LEFT JOIN pred_tab t2 ON t1.a = 1 LEFT JOIN pred_tab t3 ON t2.a IS NULL OR t2.c IS NULL; - QUERY PLAN ---------------------------------------------------- + QUERY PLAN +------------------------------------------------------------- Nested Loop Left Join - Join Filter: ((t2.a IS NULL) OR (t2.c IS NULL)) -> Nested Loop Left Join - Join Filter: (t1.a = 1) -> Seq Scan on pred_tab t1 + -> Result + One-Time Filter: (t1.a = 1) + -> Materialize + -> Seq Scan on pred_tab t2 + -> Result + One-Time Filter: ((t2.a IS NULL) OR (t2.c IS NULL)) -> Materialize - -> Seq Scan on pred_tab t2 - -> Materialize - -> Seq Scan on pred_tab t3 -(9 rows) + -> Seq Scan on pred_tab t3 +(11 rows) -- -- Tests for NullTest reduction in EXISTS sublink @@ -244,26 +250,27 @@ SELECT * FROM pred_tab t1 LEFT JOIN pred_tab t2 ON EXISTS (SELECT 1 FROM pred_tab t3, pred_tab t4, pred_tab t5, pred_tab t6 WHERE t1.a = t3.a AND t6.a IS NOT NULL); - QUERY PLAN ---------------------------------------------------------- + QUERY PLAN +--------------------------------------------------------------- Nested Loop Left Join - Join Filter: EXISTS(SubPlan exists_1) -> Seq Scan on pred_tab t1 - -> Materialize - -> Seq Scan on pred_tab t2 - SubPlan exists_1 - -> Nested Loop + -> Result + One-Time Filter: EXISTS(SubPlan exists_1) + -> Materialize + -> Seq Scan on pred_tab t2 + SubPlan exists_1 -> Nested Loop -> Nested Loop - -> Seq Scan on pred_tab t4 + -> Nested Loop + -> Seq Scan on pred_tab t4 + -> Materialize + -> Seq Scan on pred_tab t3 + Filter: (t1.a = a) -> Materialize - -> Seq Scan on pred_tab t3 - Filter: (t1.a = a) + -> Seq Scan on pred_tab t5 -> Materialize - -> Seq Scan on pred_tab t5 - -> Materialize - -> Seq Scan on pred_tab t6 -(17 rows) + -> Seq Scan on pred_tab t6 +(18 rows) -- Ensure the IS_NULL qual is reduced to constant-FALSE EXPLAIN (COSTS OFF) @@ -354,15 +361,16 @@ SELECT * FROM pred_tab t1 -> Seq Scan on pred_tab t1 -> Materialize -> Nested Loop Left Join - Join Filter: (t3.b IS NOT NULL) -> Nested Loop Left Join Join Filter: (t2.a = t3.a) -> Seq Scan on pred_tab t2 -> Materialize -> Seq Scan on pred_tab_notnull t3 - -> Materialize - -> Seq Scan on pred_tab t4 -(12 rows) + -> Result + One-Time Filter: (t3.b IS NOT NULL) + -> Materialize + -> Seq Scan on pred_tab t4 +(13 rows) SELECT * FROM pred_tab t1 LEFT JOIN pred_tab t2 ON TRUE @@ -384,21 +392,22 @@ SELECT * FROM pred_tab t1 LEFT JOIN pred_tab t2 ON TRUE LEFT JOIN pred_tab_notnull t3 ON t2.a = t3.a LEFT JOIN pred_tab t4 ON t3.b IS NULL AND t3.a IS NOT NULL; - QUERY PLAN --------------------------------------------------------------------- + QUERY PLAN +------------------------------------------------------------------------------ Nested Loop Left Join -> Seq Scan on pred_tab t1 -> Materialize -> Nested Loop Left Join - Join Filter: ((t3.b IS NULL) AND (t3.a IS NOT NULL)) -> Nested Loop Left Join Join Filter: (t2.a = t3.a) -> Seq Scan on pred_tab t2 -> Materialize -> Seq Scan on pred_tab_notnull t3 - -> Materialize - -> Seq Scan on pred_tab t4 -(12 rows) + -> Result + One-Time Filter: ((t3.b IS NULL) AND (t3.a IS NOT NULL)) + -> Materialize + -> Seq Scan on pred_tab t4 +(13 rows) SELECT * FROM pred_tab t1 LEFT JOIN pred_tab t2 ON TRUE diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out index a3778c23c34..06013614868 100644 --- a/src/test/regress/expected/subselect.out +++ b/src/test/regress/expected/subselect.out @@ -3575,16 +3575,17 @@ EXPLAIN (COSTS OFF) SELECT * FROM not_null_tab t1 LEFT JOIN not_null_tab t2 ON t1.id NOT IN (SELECT id FROM not_null_tab); - QUERY PLAN ------------------------------------------------------------------- + QUERY PLAN +---------------------------------------------------------------------------- Nested Loop Left Join - Join Filter: (NOT (ANY (t1.id = (hashed SubPlan any_1).col1))) -> Seq Scan on not_null_tab t1 - -> Materialize - -> Seq Scan on not_null_tab t2 - SubPlan any_1 - -> Seq Scan on not_null_tab -(7 rows) + -> Result + One-Time Filter: (NOT (ANY (t1.id = (hashed SubPlan any_1).col1))) + -> Materialize + -> Seq Scan on not_null_tab t2 + SubPlan any_1 + -> Seq Scan on not_null_tab +(8 rows) -- ANTI JOIN: outer side is defined NOT NULL and is not nulled by outer join, -- inner side is defined NOT NULL diff --git a/src/test/regress/sql/join.sql b/src/test/regress/sql/join.sql index fae19113cef..0e58c2f0c48 100644 --- a/src/test/regress/sql/join.sql +++ b/src/test/regress/sql/join.sql @@ -3891,3 +3891,30 @@ SELECT COUNT(*) FROM onek t1 LEFT JOIN tenk1 t2 ON (t2.thousand = t1.tenthous OR t2.thousand = t1.thousand); SELECT COUNT(*) FROM onek t1 LEFT JOIN tenk1 t2 ON (t2.thousand = t1.tenthous OR t2.thousand = t1.thousand); + +-- Outer-only ON-clauses become a gating Result on the inner side; for a +-- parameter-independent inner, NestLoop.keep_inner_rewind retains REWIND so +-- a Materialize below the gate replays its buffer across rescans rather than +-- rebuilding it for every outer tuple that passes the gate. +create function platform_independent_explain(query text) returns setof text +language plpgsql as +$$ +declare + ln text; +begin + for ln in + execute format('explain (analyze, costs off, summary off, timing off, buffers off) %s', query) + loop + ln := regexp_replace(ln, 'Maximum Storage: \d+', 'Maximum Storage: N'); + return next ln; + end loop; +end; +$$; +set enable_hashjoin = off; +set enable_mergejoin = off; +select platform_independent_explain(' + select count(*) from onek t1 left join int4_tbl t2 + on (t1.unique1 = t2.f1 and t1.hundred in (1, 2))'); +reset enable_hashjoin; +reset enable_mergejoin; +drop function platform_independent_explain(text); -- 2.54.0