From 26864c5e9946fb975f54295d65095b5ead231169 Mon Sep 17 00:00:00 2001 From: shihao zhong Date: Sun, 18 Oct 2026 21:12:15 -0400 Subject: [PATCH v1] Emit null-keyed outer hash join tuples immediately Commit 1811f1af98f put outer tuples with null join keys into a tuplestore and emitted them at the end of the batch, which spills to disk past work_mem/16. Let them probe with a dummy hash value instead, so they are emitted right away. Multi-batch parallel joins still use the tuplestore. --- src/backend/executor/nodeHashjoin.c | 20 ++++++++++++-------- src/include/executor/hashjoin.h | 8 +++++--- src/include/nodes/execnodes.h | 3 ++- 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/src/backend/executor/nodeHashjoin.c b/src/backend/executor/nodeHashjoin.c index 202dd866251..c49a8d87422 100644 --- a/src/backend/executor/nodeHashjoin.c +++ b/src/backend/executor/nodeHashjoin.c @@ -1154,10 +1154,12 @@ ExecHashJoinOuterGetTuple(PlanState *outerNode, } else if (hjstate->hj_KeepNullTuples) { - /* null join key, but we must save tuple to be emitted later */ - if (hjstate->hj_NullOuterTupleStore == NULL) - hjstate->hj_NullOuterTupleStore = ExecHashBuildNullTupleStore(hashtable); - tuplestore_puttupleslot(hjstate->hj_NullOuterTupleStore, slot); + /* + * null join key; probe with a dummy hash value, which goes to + * the current batch and cannot match + */ + *hashvalue = 0; + return slot; } /* @@ -1232,10 +1234,12 @@ ExecParallelHashJoinOuterGetTuple(PlanState *outerNode, } else if (hjstate->hj_KeepNullTuples) { - /* null join key, but we must save tuple to be emitted later */ - if (hjstate->hj_NullOuterTupleStore == NULL) - hjstate->hj_NullOuterTupleStore = ExecHashBuildNullTupleStore(hashtable); - tuplestore_puttupleslot(hjstate->hj_NullOuterTupleStore, slot); + /* + * null join key; probe with a dummy hash value, which goes to + * the current batch and cannot match + */ + *hashvalue = 0; + return slot; } /* diff --git a/src/include/executor/hashjoin.h b/src/include/executor/hashjoin.h index 4d342174b9a..f15691b4569 100644 --- a/src/include/executor/hashjoin.h +++ b/src/include/executor/hashjoin.h @@ -74,9 +74,11 @@ * If an input tuple has a null join key, then it cannot match anything from * the other side of the join. Normally we can just discard such a tuple * immediately, but if it comes from the outer side of an outer join then we - * must emit it with null-extension of the other side. For various reasons - * it's not convenient to do that immediately on seeing the tuple, so we dump - * the tuple into a tuplestore and emit it later. (In the unlikely but + * must emit it with null-extension of the other side. An outer tuple just + * probes the hash table with a dummy hash value, finds no match and is + * emitted. For an inner tuple, or an outer tuple of a multi-batch parallel + * join, that is not convenient, so we dump the tuple into a tuplestore and + * emit it later. (In the unlikely but * supported case of a non-strict join operator, we treat null keys as normal * data.) * ---------------------------------------------------------------- diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index e3cea52e7df..7fbccaea519 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -2203,7 +2203,8 @@ typedef struct MergeJoinState * outer joins * hj_NullInnerTupleSlot prepared null tuple for left/full outer joins * hj_NullOuterTupleStore tuplestore holding outer tuples that have - * null join keys (but must be emitted anyway) + * null join keys (but must be emitted anyway), + * used only in multi-batch parallel joins * hj_FirstOuterTupleSlot first tuple retrieved from outer plan * hj_JoinState current state of ExecHashJoin state machine * hj_KeepNullTuples true to keep outer tuples with null join keys -- 2.37.1 (Apple Git-137.1)