From dd51073a86aafb8d94d0b3b1c232fe8a6bca05fd Mon Sep 17 00:00:00 2001 From: "Andrei V. Lepikhov" Date: Thu, 2 Apr 2026 15:35:16 +0200 Subject: [PATCH v1 1/3] Extend the ExecSetTupleBound to LEFT JOIN outer side --- src/backend/executor/execProcnode.c | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/backend/executor/execProcnode.c b/src/backend/executor/execProcnode.c index 7c4c66e323f..06d2be90478 100644 --- a/src/backend/executor/execProcnode.c +++ b/src/backend/executor/execProcnode.c @@ -958,6 +958,22 @@ ExecSetTupleBound(int64 tuples_needed, PlanState *child_node) ExecSetTupleBound(tuples_needed, outerPlanState(child_node)); } + else if (IsA(child_node, NestLoopState)) + { + /* + * A nestloop left join returns at least one row for every outer row: + * if no inner row passes the joinqual, the outer row is emitted + * null-extended instead of being dropped. So the bound carries over + * unchanged to the outer input. + * This logic works unless otherqual can discard an outer tuple during + * the join. Hence, check it before propagating boundaries downstairs. + */ + NestLoopState *nlstate = (NestLoopState *) child_node; + JoinType jointype = nlstate->js.jointype; + + if (jointype == JOIN_LEFT && nlstate->js.ps.qual == NULL) + ExecSetTupleBound(tuples_needed, outerPlanState(child_node)); + } /* * In principle we could descend through any plan node type that is -- 2.55.0