1
0
mirror of https://github.com/postgres/postgres.git synced 2026-10-29 15:58:33 +03:00

Emit null-keyed hash join outer tuples immediately when feasible.

Commit 1811f1af9 put outer tuples with null join keys into a
tuplestore and emitted them at the end of the batch, which risks
expensive spills to disk if there are a lot of them.  It's difficult
to avoid the tuplestore in a multi-batch parallel join.  But in the
non-parallel and single-batch cases we can just emit such tuples
immediately, at the tiny cost of propagating the "is-null" result
flag one call level further up.  This avoids a performance regression
from v18 for such cases.

Author: shihao zhong <zhong950419@gmail.com>
Co-authored-by: Tom Lane <tgl@sss.pgh.pa.us>
Discussion: https://postgr.es/m/CAGRkXqTgugmYrtnP_11NjL_GCXwAfzVfm5WKUNFsfZOzxDF2rA@mail.gmail.com
Backpatch-through: 19
This commit is contained in:
Tom Lane
2026-10-23 11:35:42 -04:00
parent 896246ea10
commit 126976a8ff
3 changed files with 47 additions and 29 deletions

View File

@@ -195,10 +195,12 @@
static TupleTableSlot *ExecHashJoinOuterGetTuple(PlanState *outerNode,
HashJoinState *hjstate,
uint32 *hashvalue);
uint32 *hashvalue,
bool *is_null);
static TupleTableSlot *ExecParallelHashJoinOuterGetTuple(PlanState *outerNode,
HashJoinState *hjstate,
uint32 *hashvalue);
uint32 *hashvalue,
bool *is_null);
static TupleTableSlot *ExecHashJoinGetSavedTuple(HashJoinState *hjstate,
BufFile *file,
uint32 *hashvalue,
@@ -233,6 +235,7 @@ ExecHashJoinImpl(PlanState *pstate, bool parallel)
HashJoinTable hashtable;
TupleTableSlot *outerTupleSlot;
uint32 hashvalue;
bool is_null;
int batchno;
ParallelHashJoinState *parallel_state;
@@ -440,10 +443,13 @@ ExecHashJoinImpl(PlanState *pstate, bool parallel)
if (parallel)
outerTupleSlot =
ExecParallelHashJoinOuterGetTuple(outerNode, node,
&hashvalue);
&hashvalue,
&is_null);
else
outerTupleSlot =
ExecHashJoinOuterGetTuple(outerNode, node, &hashvalue);
ExecHashJoinOuterGetTuple(outerNode, node,
&hashvalue,
&is_null);
if (TupIsNull(outerTupleSlot))
{
@@ -489,6 +495,16 @@ ExecHashJoinImpl(PlanState *pstate, bool parallel)
econtext->ecxt_outertuple = outerTupleSlot;
node->hj_MatchedOuter = false;
/*
* If the tuple can't match anything, skip all the tedium and
* go directly to emitting it in null-extended form.
*/
if (is_null)
{
node->hj_JoinState = HJ_FILL_OUTER_TUPLE;
continue;
}
/*
* Find the corresponding bucket for this tuple in the main
* hash table or skew hash table.
@@ -1104,12 +1120,15 @@ ExecEndHashJoin(HashJoinState *node)
* Returns a null slot if no more outer tuples (within the current batch).
*
* On success, the tuple's hash value is stored at *hashvalue --- this is
* either originally computed, or re-read from the temp file.
* either originally computed, or re-read from the temp file. Also, we
* set *is_null to true if the tuple cannot have any matches due to
* containing a null join key (in which case the hashvalue is meaningless).
*/
static TupleTableSlot *
ExecHashJoinOuterGetTuple(PlanState *outerNode,
HashJoinState *hjstate,
uint32 *hashvalue)
uint32 *hashvalue,
bool *is_null)
{
HashJoinTable hashtable = hjstate->hj_HashTable;
int curbatch = hashtable->curbatch;
@@ -1129,8 +1148,6 @@ ExecHashJoinOuterGetTuple(PlanState *outerNode,
while (!TupIsNull(slot))
{
bool isnull;
/*
* We have to compute the tuple's hash value.
*/
@@ -1142,9 +1159,9 @@ ExecHashJoinOuterGetTuple(PlanState *outerNode,
*hashvalue = DatumGetUInt32(ExecEvalExprSwitchContext(hjstate->hj_OuterHash,
econtext,
&isnull));
is_null));
if (!isnull)
if (!*is_null)
{
/* normal case with a non-null join key */
/* remember outer relation is not empty for possible rescan */
@@ -1154,10 +1171,8 @@ 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, but we must return tuple to be emitted */
return slot;
}
/*
@@ -1183,7 +1198,10 @@ ExecHashJoinOuterGetTuple(PlanState *outerNode,
hashvalue,
hjstate->hj_OuterTupleSlot);
if (!TupIsNull(slot))
{
*is_null = false; /* else it'd not be in the batch file */
return slot;
}
}
/* End of this batch */
@@ -1196,7 +1214,8 @@ ExecHashJoinOuterGetTuple(PlanState *outerNode,
static TupleTableSlot *
ExecParallelHashJoinOuterGetTuple(PlanState *outerNode,
HashJoinState *hjstate,
uint32 *hashvalue)
uint32 *hashvalue,
bool *is_null)
{
HashJoinTable hashtable = hjstate->hj_HashTable;
int curbatch = hashtable->curbatch;
@@ -1213,8 +1232,6 @@ ExecParallelHashJoinOuterGetTuple(PlanState *outerNode,
while (!TupIsNull(slot))
{
bool isnull;
ExprContext *econtext = hjstate->js.ps.ps_ExprContext;
econtext->ecxt_outertuple = slot;
@@ -1223,19 +1240,17 @@ ExecParallelHashJoinOuterGetTuple(PlanState *outerNode,
*hashvalue = DatumGetUInt32(ExecEvalExprSwitchContext(hjstate->hj_OuterHash,
econtext,
&isnull));
is_null));
if (!isnull)
if (!*is_null)
{
/* normal case with a non-null join key */
return slot;
}
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, but we must return tuple to be emitted */
return slot;
}
/*
@@ -1256,6 +1271,7 @@ ExecParallelHashJoinOuterGetTuple(PlanState *outerNode,
ExecForceStoreMinimalTuple(tuple,
hjstate->hj_OuterTupleSlot,
false);
*is_null = false; /* else it'd not be in the batch file */
slot = hjstate->hj_OuterTupleSlot;
return slot;
}

View File

@@ -74,11 +74,12 @@
* 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
* supported case of a non-strict join operator, we treat null keys as normal
* data.)
* must emit it with null-extension of the other side. For an outer tuple
* it's possible to emit it immediately and then discard it. But 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.)
* ----------------------------------------------------------------
*/

View File

@@ -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