]> git.ipfire.org Git - thirdparty/postgresql.git/commitdiff
Improve hash join's handling of tuples with null join keys.
authorTom Lane <tgl@sss.pgh.pa.us>
Thu, 19 Mar 2026 19:21:36 +0000 (15:21 -0400)
committerTom Lane <tgl@sss.pgh.pa.us>
Thu, 19 Mar 2026 19:21:36 +0000 (15:21 -0400)
In a plain join, we can just summarily discard an input tuple
with null join key(s), since it cannot match anything from
the other side of the join (assuming a strict join operator).
However, if the tuple comes from the outer side of an outer join
then we have to emit it with null-extension of the other side.

Up to now, hash joins did that by inserting the tuple into the hash
table as though it were a normal tuple.  This is unnecessarily
inefficient though, since the required processing is far simpler than
for a potentially-matchable tuple.  Worse, if there are a lot of such
tuples they will bloat the hash bucket they go into, possibly causing
useless repeated attempts to split that bucket or increase the number
of batches.  We have a report of a large join vainly creating many
thousands of batches when faced with such input.

This patch improves the situation by keeping such tuples out of the
hash table altogether, instead pushing them into a separate tuplestore
from which we return them later.  (One might consider trying to return
them immediately; but that would require substantial refactoring, and
it doesn't work anyway for cases where we rescan an unmodified hash
table.)  This works even in parallel hash joins, because whichever
worker reads a null-keyed tuple can just return it; there's no need
for consultation with other workers.  Thus the tuplestores are local
storage even in a parallel join.

A pre-existing buglet that I noticed while analyzing the code's
behavior is that ExecHashRemoveNextSkewBucket fails to decrement
hashtable->skewTuples for tuples moved into the main hash table
from the skew hash table.  This invalidates ExecHashTableInsert's
calculation of the number of main-hash-table tuples, though probably
not by a lot since we expect the skew table to be small relative
to the main one.  Nonetheless, let's fix that too while we're here.

Bug: #18909
Reported-by: Sergey Koposov <Sergey.Koposov@ed.ac.uk>
Author: Tom Lane <tgl@sss.pgh.pa.us>
Reviewed-by: Chao Li <li.evan.chao@gmail.com>
Discussion: https://postgr.es/m/3061845.1746486714@sss.pgh.pa.us

13 files changed:
src/backend/executor/execExpr.c
src/backend/executor/nodeHash.c
src/backend/executor/nodeHashjoin.c
src/backend/utils/sort/tuplestore.c
src/include/executor/executor.h
src/include/executor/hashjoin.h
src/include/executor/nodeHash.h
src/include/nodes/execnodes.h
src/include/utils/tuplestore.h
src/test/regress/expected/join.out
src/test/regress/expected/join_hash.out
src/test/regress/sql/join.sql
src/test/regress/sql/join_hash.sql

index bd46b75e498772d3fb0674e976951699049fbe33..77229141b380df3a9fe8950534eef74af82ce1c8 100644 (file)
@@ -4309,25 +4309,27 @@ ExecBuildHash32FromAttrs(TupleDesc desc, const TupleTableSlotOps *ops,
  * 'hash_exprs'.  When multiple expressions are present, the hash values
  * returned by each hash function are combined to produce a single hash value.
  *
+ * If any hash_expr yields NULL and the corresponding hash operator is strict,
+ * the created ExprState will return NULL.  (If the operator is not strict,
+ * we treat NULL values as having a hash value of zero.  The hash functions
+ * themselves are always treated as strict.)
+ *
  * desc: tuple descriptor for the to-be-hashed expressions
  * ops: TupleTableSlotOps for the TupleDesc
  * hashfunc_oids: Oid for each hash function to call, one for each 'hash_expr'
- * collations: collation to use when calling the hash function.
- * hash_expr: list of expressions to hash the value of
- * opstrict: array corresponding to the 'hashfunc_oids' to store op_strict()
+ * collations: collation to use when calling the hash function
+ * hash_exprs: list of expressions to hash the value of
+ * opstrict: strictness flag for each hash function's comparison operator
  * parent: PlanState node that the 'hash_exprs' will be evaluated at
  * init_value: Normally 0, but can be set to other values to seed the hash
  * with some other value.  Using non-zero is slightly less efficient but can
  * be useful.
- * keep_nulls: if true, evaluation of the returned ExprState will abort early
- * returning NULL if the given hash function is strict and the Datum to hash
- * is null.  When set to false, any NULL input Datums are skipped.
  */
 ExprState *
 ExecBuildHash32Expr(TupleDesc desc, const TupleTableSlotOps *ops,
                                        const Oid *hashfunc_oids, const List *collations,
                                        const List *hash_exprs, const bool *opstrict,
-                                       PlanState *parent, uint32 init_value, bool keep_nulls)
+                                       PlanState *parent, uint32 init_value)
 {
        ExprState  *state = makeNode(ExprState);
        ExprEvalStep scratch = {0};
@@ -4404,8 +4406,8 @@ ExecBuildHash32Expr(TupleDesc desc, const TupleTableSlotOps *ops,
                fmgr_info(funcid, finfo);
 
                /*
-                * Build the steps to evaluate the hash function's argument have it so
-                * the value of that is stored in the 0th argument of the hash func.
+                * Build the steps to evaluate the hash function's argument, placing
+                * the value in the 0th argument of the hash func.
                 */
                ExecInitExprRec(expr,
                                                state,
@@ -4440,7 +4442,7 @@ ExecBuildHash32Expr(TupleDesc desc, const TupleTableSlotOps *ops,
                scratch.d.hashdatum.fcinfo_data = fcinfo;
                scratch.d.hashdatum.fn_addr = finfo->fn_addr;
 
-               scratch.opcode = opstrict[i] && !keep_nulls ? strict_opcode : opcode;
+               scratch.opcode = opstrict[i] ? strict_opcode : opcode;
                scratch.d.hashdatum.jumpdone = -1;
 
                ExprEvalPushStep(state, &scratch);
index f7fd6474db80f7a08eb5b45885dacc97226181ce..8825bb6fa23fe8022186fbd234736fad8bf54e4e 100644 (file)
@@ -40,6 +40,7 @@
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/syscache.h"
+#include "utils/tuplestore.h"
 #include "utils/wait_event.h"
 
 static void ExecHashIncreaseNumBatches(HashJoinTable hashtable);
@@ -115,7 +116,7 @@ MultiExecHash(HashState *node)
 
        /* must provide our own instrumentation support */
        if (node->ps.instrument)
-               InstrStopNode(node->ps.instrument, node->hashtable->partialTuples);
+               InstrStopNode(node->ps.instrument, node->hashtable->reportTuples);
 
        /*
         * We do not return the hash table directly because it's not a subtype of
@@ -141,6 +142,7 @@ MultiExecPrivateHash(HashState *node)
        HashJoinTable hashtable;
        TupleTableSlot *slot;
        ExprContext *econtext;
+       double          nullTuples = 0;
 
        /*
         * get state info from node
@@ -154,8 +156,11 @@ MultiExecPrivateHash(HashState *node)
        econtext = node->ps.ps_ExprContext;
 
        /*
-        * Get all tuples from the node below the Hash node and insert into the
-        * hash table (or temp files).
+        * Get all tuples from the node below the Hash node and insert the
+        * potentially-matchable ones into the hash table (or temp files).  Tuples
+        * that can't possibly match because they have null join keys are dumped
+        * into a separate tuplestore, or just summarily discarded if we don't
+        * need to emit them with null-extension.
         */
        for (;;)
        {
@@ -175,6 +180,7 @@ MultiExecPrivateHash(HashState *node)
 
                if (!isnull)
                {
+                       /* normal case with a non-null join key */
                        uint32          hashvalue = DatumGetUInt32(hashdatum);
                        int                     bucketNumber;
 
@@ -184,7 +190,6 @@ MultiExecPrivateHash(HashState *node)
                                /* It's a skew tuple, so put it into that hash table */
                                ExecHashSkewTableInsert(hashtable, slot, hashvalue,
                                                                                bucketNumber);
-                               hashtable->skewTuples += 1;
                        }
                        else
                        {
@@ -193,6 +198,15 @@ MultiExecPrivateHash(HashState *node)
                        }
                        hashtable->totalTuples += 1;
                }
+               else if (node->keep_null_tuples)
+               {
+                       /* null join key, but we must save tuple to be emitted later */
+                       if (node->null_tuple_store == NULL)
+                               node->null_tuple_store = ExecHashBuildNullTupleStore(hashtable);
+                       tuplestore_puttupleslot(node->null_tuple_store, slot);
+                       nullTuples += 1;
+               }
+               /* else we can discard the tuple immediately */
        }
 
        /* resize the hash table if needed (NTUP_PER_BUCKET exceeded) */
@@ -204,7 +218,8 @@ MultiExecPrivateHash(HashState *node)
        if (hashtable->spaceUsed > hashtable->spacePeak)
                hashtable->spacePeak = hashtable->spaceUsed;
 
-       hashtable->partialTuples = hashtable->totalTuples;
+       /* Report total number of tuples output (but not those discarded) */
+       hashtable->reportTuples = hashtable->totalTuples + nullTuples;
 }
 
 /* ----------------------------------------------------------------
@@ -223,7 +238,6 @@ MultiExecParallelHash(HashState *node)
        HashJoinTable hashtable;
        TupleTableSlot *slot;
        ExprContext *econtext;
-       uint32          hashvalue;
        Barrier    *build_barrier;
        int                     i;
 
@@ -283,6 +297,7 @@ MultiExecParallelHash(HashState *node)
                        for (;;)
                        {
                                bool            isnull;
+                               uint32          hashvalue;
 
                                slot = ExecProcNode(outerNode);
                                if (TupIsNull(slot))
@@ -296,8 +311,20 @@ MultiExecParallelHash(HashState *node)
                                                                                                                                         &isnull));
 
                                if (!isnull)
+                               {
+                                       /* normal case with a non-null join key */
                                        ExecParallelHashTableInsert(hashtable, slot, hashvalue);
-                               hashtable->partialTuples++;
+                                       hashtable->reportTuples++;
+                               }
+                               else if (node->keep_null_tuples)
+                               {
+                                       /* null join key, but save tuple to be emitted later */
+                                       if (node->null_tuple_store == NULL)
+                                               node->null_tuple_store = ExecHashBuildNullTupleStore(hashtable);
+                                       tuplestore_puttupleslot(node->null_tuple_store, slot);
+                                       hashtable->reportTuples++;
+                               }
+                               /* else we can discard the tuple immediately */
                        }
 
                        /*
@@ -336,7 +363,9 @@ MultiExecParallelHash(HashState *node)
 
        /*
         * We're not yet attached to a batch.  We all agree on the dimensions and
-        * number of inner tuples (for the empty table optimization).
+        * number of inner tuples.  (In parallel mode, totalTuples isn't used in
+        * this module, but we must report it for nodeHashjoin.c's empty-table
+        * optimization.)
         */
        hashtable->curbatch = -1;
        hashtable->nbuckets = pstate->nbuckets;
@@ -405,14 +434,10 @@ ExecInitHash(Hash *node, EState *estate, int eflags)
 
        Assert(node->plan.qual == NIL);
 
-       /*
-        * Delay initialization of hash_expr until ExecInitHashJoin().  We cannot
-        * build the ExprState here as we don't yet know the join type we're going
-        * to be hashing values for and we need to know that before calling
-        * ExecBuildHash32Expr as the keep_nulls parameter depends on the join
-        * type.
-        */
+       /* these fields will be filled by ExecInitHashJoin() */
        hashstate->hash_expr = NULL;
+       hashstate->null_tuple_store = NULL;
+       hashstate->keep_null_tuples = false;
 
        return hashstate;
 }
@@ -508,7 +533,7 @@ ExecHashTableCreate(HashState *state)
        hashtable->nbatch_outstart = nbatch;
        hashtable->growEnabled = true;
        hashtable->totalTuples = 0;
-       hashtable->partialTuples = 0;
+       hashtable->reportTuples = 0;
        hashtable->skewTuples = 0;
        hashtable->innerBatchFile = NULL;
        hashtable->outerBatchFile = NULL;
@@ -1768,7 +1793,6 @@ ExecHashTableInsert(HashJoinTable hashtable,
                 */
                HashJoinTuple hashTuple;
                int                     hashTupleSize;
-               double          ntuples = (hashtable->totalTuples - hashtable->skewTuples);
 
                /* Create the HashJoinTuple */
                hashTupleSize = HJTUPLE_OVERHEAD + tuple->t_len;
@@ -1792,10 +1816,12 @@ ExecHashTableInsert(HashJoinTable hashtable,
                /*
                 * Increase the (optimal) number of buckets if we just exceeded the
                 * NTUP_PER_BUCKET threshold, but only when there's still a single
-                * batch.
+                * batch.  Note that totalTuples - skewTuples is a reliable indicator
+                * of the hash table's size only as long as there's just one batch.
                 */
                if (hashtable->nbatch == 1 &&
-                       ntuples > (hashtable->nbuckets_optimal * NTUP_PER_BUCKET))
+                       (hashtable->totalTuples - hashtable->skewTuples) >
+                       (hashtable->nbuckets_optimal * NTUP_PER_BUCKET))
                {
                        /* Guard against integer overflow and alloc size overflow */
                        if (hashtable->nbuckets_optimal <= INT_MAX / 2 &&
@@ -2622,6 +2648,7 @@ ExecHashSkewTableInsert(HashJoinTable hashtable,
        Assert(hashTuple != hashTuple->next.unshared);
 
        /* Account for space used, and back off if we've used too much */
+       hashtable->skewTuples += 1;
        hashtable->spaceUsed += hashTupleSize;
        hashtable->spaceUsedSkew += hashTupleSize;
        if (hashtable->spaceUsed > hashtable->spacePeak)
@@ -2714,6 +2741,12 @@ ExecHashRemoveNextSkewBucket(HashJoinTable hashtable)
                        hashtable->spaceUsedSkew -= tupleSize;
                }
 
+               /*
+                * We must reduce skewTuples, but totalTuples doesn't change since it
+                * counts both main and skew tuples.
+                */
+               hashtable->skewTuples -= 1;
+
                hashTuple = nextHashTuple;
 
                /* allow this loop to be cancellable */
@@ -2754,6 +2787,31 @@ ExecHashRemoveNextSkewBucket(HashJoinTable hashtable)
        }
 }
 
+/*
+ * Build a tuplestore suitable for holding null-keyed input tuples.
+ * (This function doesn't care whether it's for outer or inner tuples.)
+ *
+ * Note that in a parallel hash join, each worker has its own tuplestore(s)
+ * for these.  There's no need to interact with other workers to decide
+ * what to do with them.  So they're always in private storage.
+ */
+Tuplestorestate *
+ExecHashBuildNullTupleStore(HashJoinTable hashtable)
+{
+       Tuplestorestate *tstore;
+       MemoryContext oldcxt;
+
+       /*
+        * We keep the tuplestore in the hashCxt to ensure it won't go away too
+        * soon.  Size it at work_mem/16 so that it doesn't bloat the node's space
+        * consumption too much.
+        */
+       oldcxt = MemoryContextSwitchTo(hashtable->hashCxt);
+       tstore = tuplestore_begin_heap(false, false, work_mem / 16);
+       MemoryContextSwitchTo(oldcxt);
+       return tstore;
+}
+
 /*
  * Reserve space in the DSM segment for instrumentation data.
  */
index fe96bd556ef71a90e66715d22a6616e05b61697c..0b365d5b4751e2897017e8630ca065081796d09f 100644 (file)
 #include "miscadmin.h"
 #include "utils/lsyscache.h"
 #include "utils/sharedtuplestore.h"
+#include "utils/tuplestore.h"
 #include "utils/wait_event.h"
 
 
 #define HJ_SCAN_BUCKET                 3
 #define HJ_FILL_OUTER_TUPLE            4
 #define HJ_FILL_INNER_TUPLES   5
-#define HJ_NEED_NEW_BATCH              6
+#define HJ_FILL_OUTER_NULL_TUPLES      6
+#define HJ_FILL_INNER_NULL_TUPLES      7
+#define HJ_NEED_NEW_BATCH              8
 
 /* Returns true if doing null-fill on outer relation */
 #define HJ_FILL_OUTER(hjstate) ((hjstate)->hj_NullInnerTupleSlot != NULL)
@@ -347,9 +350,16 @@ ExecHashJoinImpl(PlanState *pstate, bool parallel)
                                /*
                                 * If the inner relation is completely empty, and we're not
                                 * doing a left outer join, we can quit without scanning the
-                                * outer relation.
+                                * outer relation.  (If the inner relation contains only
+                                * null-keyed tuples that we need to emit, we'll fall through
+                                * and do the outer-relation scan.  In principle we could go
+                                * emit those tuples then quit, but it would complicate the
+                                * state machine logic.  The case seems rare enough to not be
+                                * worth optimizing.)
                                 */
-                               if (hashtable->totalTuples == 0 && !HJ_FILL_OUTER(node))
+                               if (hashtable->totalTuples == 0 &&
+                                       hashNode->null_tuple_store == NULL &&
+                                       !HJ_FILL_OUTER(node))
                                {
                                        if (parallel)
                                        {
@@ -396,21 +406,24 @@ ExecHashJoinImpl(PlanState *pstate, bool parallel)
                                                        ExecParallelHashJoinPartitionOuter(node);
                                                BarrierArriveAndWait(build_barrier,
                                                                                         WAIT_EVENT_HASH_BUILD_HASH_OUTER);
-                                       }
-                                       else if (BarrierPhase(build_barrier) == PHJ_BUILD_FREE)
-                                       {
-                                               /*
-                                                * If we attached so late that the job is finished and
-                                                * the batch state has been freed, we can return
-                                                * immediately.
-                                                */
-                                               return NULL;
+                                               Assert(BarrierPhase(build_barrier) == PHJ_BUILD_RUN);
                                        }
 
-                                       /* Each backend should now select a batch to work on. */
-                                       Assert(BarrierPhase(build_barrier) == PHJ_BUILD_RUN);
+                                       /*
+                                        * Each backend should now select a batch to work on.
+                                        * However, if we've already collected some null-keyed
+                                        * tuples, dump them first.  (That is critical when we
+                                        * arrive late enough that no more batches are available;
+                                        * otherwise we'd fail to dump those tuples at all.)
+                                        */
                                        hashtable->curbatch = -1;
-                                       node->hj_JoinState = HJ_NEED_NEW_BATCH;
+
+                                       if (node->hj_NullOuterTupleStore)
+                                               node->hj_JoinState = HJ_FILL_OUTER_NULL_TUPLES;
+                                       else if (hashNode->null_tuple_store)
+                                               node->hj_JoinState = HJ_FILL_INNER_NULL_TUPLES;
+                                       else
+                                               node->hj_JoinState = HJ_NEED_NEW_BATCH;
 
                                        continue;
                                }
@@ -441,12 +454,17 @@ ExecHashJoinImpl(PlanState *pstate, bool parallel)
                                                if (parallel)
                                                {
                                                        /*
-                                                        * Only one process is currently allow to handle
+                                                        * Only one process is currently allowed to handle
                                                         * each batch's unmatched tuples, in a parallel
-                                                        * join.
+                                                        * join.  However, each process must deal with any
+                                                        * null-keyed tuples it found.
                                                         */
                                                        if (ExecParallelPrepHashTableForUnmatched(node))
                                                                node->hj_JoinState = HJ_FILL_INNER_TUPLES;
+                                                       else if (node->hj_NullOuterTupleStore)
+                                                               node->hj_JoinState = HJ_FILL_OUTER_NULL_TUPLES;
+                                                       else if (hashNode->null_tuple_store)
+                                                               node->hj_JoinState = HJ_FILL_INNER_NULL_TUPLES;
                                                        else
                                                                node->hj_JoinState = HJ_NEED_NEW_BATCH;
                                                }
@@ -457,7 +475,14 @@ ExecHashJoinImpl(PlanState *pstate, bool parallel)
                                                }
                                        }
                                        else
-                                               node->hj_JoinState = HJ_NEED_NEW_BATCH;
+                                       {
+                                               /* might have outer null-keyed tuples to fill */
+                                               Assert(hashNode->null_tuple_store == NULL);
+                                               if (node->hj_NullOuterTupleStore)
+                                                       node->hj_JoinState = HJ_FILL_OUTER_NULL_TUPLES;
+                                               else
+                                                       node->hj_JoinState = HJ_NEED_NEW_BATCH;
+                                       }
                                        continue;
                                }
 
@@ -633,8 +658,13 @@ ExecHashJoinImpl(PlanState *pstate, bool parallel)
                                if (!(parallel ? ExecParallelScanHashTableForUnmatched(node, econtext)
                                          : ExecScanHashTableForUnmatched(node, econtext)))
                                {
-                                       /* no more unmatched tuples */
-                                       node->hj_JoinState = HJ_NEED_NEW_BATCH;
+                                       /* no more unmatched tuples, but maybe there are nulls */
+                                       if (node->hj_NullOuterTupleStore)
+                                               node->hj_JoinState = HJ_FILL_OUTER_NULL_TUPLES;
+                                       else if (hashNode->null_tuple_store)
+                                               node->hj_JoinState = HJ_FILL_INNER_NULL_TUPLES;
+                                       else
+                                               node->hj_JoinState = HJ_NEED_NEW_BATCH;
                                        continue;
                                }
 
@@ -650,6 +680,93 @@ ExecHashJoinImpl(PlanState *pstate, bool parallel)
                                        InstrCountFiltered2(node, 1);
                                break;
 
+                       case HJ_FILL_OUTER_NULL_TUPLES:
+
+                               /*
+                                * We have finished a batch, but we are doing left/full join,
+                                * so any null-keyed outer tuples have to be emitted before we
+                                * continue to the next batch.
+                                *
+                                * (We could delay this till the end of the join, but there
+                                * seems little percentage in that.)
+                                *
+                                * We have to use tuplestore_gettupleslot_force because
+                                * hj_OuterTupleSlot may not be able to store a MinimalTuple.
+                                */
+                               while (tuplestore_gettupleslot_force(node->hj_NullOuterTupleStore,
+                                                                                                        true, false,
+                                                                                                        node->hj_OuterTupleSlot))
+                               {
+                                       /*
+                                        * Generate a fake join tuple with nulls for the inner
+                                        * tuple, and return it if it passes the non-join quals.
+                                        */
+                                       econtext->ecxt_outertuple = node->hj_OuterTupleSlot;
+                                       econtext->ecxt_innertuple = node->hj_NullInnerTupleSlot;
+
+                                       if (otherqual == NULL || ExecQual(otherqual, econtext))
+                                               return ExecProject(node->js.ps.ps_ProjInfo);
+                                       else
+                                               InstrCountFiltered2(node, 1);
+
+                                       ResetExprContext(econtext);
+
+                                       /* allow this loop to be cancellable */
+                                       CHECK_FOR_INTERRUPTS();
+                               }
+
+                               /* We don't need the tuplestore any more, so discard it. */
+                               tuplestore_end(node->hj_NullOuterTupleStore);
+                               node->hj_NullOuterTupleStore = NULL;
+
+                               /* Fill inner tuples too if it's a full join, else advance. */
+                               if (hashNode->null_tuple_store)
+                                       node->hj_JoinState = HJ_FILL_INNER_NULL_TUPLES;
+                               else
+                                       node->hj_JoinState = HJ_NEED_NEW_BATCH;
+                               break;
+
+                       case HJ_FILL_INNER_NULL_TUPLES:
+
+                               /*
+                                * We have finished a batch, but we are doing
+                                * right/right-anti/full join, so any null-keyed inner tuples
+                                * have to be emitted before we continue to the next batch.
+                                *
+                                * (We could delay this till the end of the join, but there
+                                * seems little percentage in that.)
+                                */
+                               while (tuplestore_gettupleslot(hashNode->null_tuple_store,
+                                                                                          true, false,
+                                                                                          node->hj_HashTupleSlot))
+                               {
+                                       /*
+                                        * Generate a fake join tuple with nulls for the outer
+                                        * tuple, and return it if it passes the non-join quals.
+                                        */
+                                       econtext->ecxt_outertuple = node->hj_NullOuterTupleSlot;
+                                       econtext->ecxt_innertuple = node->hj_HashTupleSlot;
+
+                                       if (otherqual == NULL || ExecQual(otherqual, econtext))
+                                               return ExecProject(node->js.ps.ps_ProjInfo);
+                                       else
+                                               InstrCountFiltered2(node, 1);
+
+                                       ResetExprContext(econtext);
+
+                                       /* allow this loop to be cancellable */
+                                       CHECK_FOR_INTERRUPTS();
+                               }
+
+                               /*
+                                * Ideally we'd discard the tuplestore now, but we can't
+                                * because we might need it for rescans.
+                                */
+
+                               /* Now we can advance to the next batch. */
+                               node->hj_JoinState = HJ_NEED_NEW_BATCH;
+                               break;
+
                        case HJ_NEED_NEW_BATCH:
 
                                /*
@@ -832,10 +949,7 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 
                /*
                 * Build ExprStates to obtain hash values for either side of the join.
-                * This must be done here as ExecBuildHash32Expr needs to know how to
-                * handle NULL inputs and the required handling of that depends on the
-                * jointype.  We don't know the join type in ExecInitHash() and we
-                * must build the ExprStates before ExecHashTableCreate() so we
+                * Note: must build the ExprStates before ExecHashTableCreate() so we
                 * properly attribute any SubPlans that exist in the hash expressions
                 * to the correct PlanState.
                 */
@@ -847,7 +961,7 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 
                /*
                 * Determine the hash function for each side of the join for the given
-                * hash operator.
+                * join operator, and detect whether the join operator is strict.
                 */
                foreach(lc, node->hashoperators)
                {
@@ -865,11 +979,7 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 
                /*
                 * Build an ExprState to generate the hash value for the expressions
-                * on the outer of the join.  This ExprState must finish generating
-                * the hash value when HJ_FILL_OUTER() is true.  Otherwise,
-                * ExecBuildHash32Expr will set up the ExprState to abort early if it
-                * finds a NULL.  In these cases, we don't need to store these tuples
-                * in the hash table as the jointype does not require it.
+                * on the outer side of the join.
                 */
                hjstate->hj_OuterHash =
                        ExecBuildHash32Expr(hjstate->js.ps.ps_ResultTupleDesc,
@@ -879,8 +989,7 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
                                                                node->hashkeys,
                                                                hash_strict,
                                                                &hjstate->js.ps,
-                                                               0,
-                                                               HJ_FILL_OUTER(hjstate));
+                                                               0);
 
                /* As above, but for the inner side of the join */
                hashstate->hash_expr =
@@ -891,8 +1000,11 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
                                                                hash->hashkeys,
                                                                hash_strict,
                                                                &hashstate->ps,
-                                                               0,
-                                                               HJ_FILL_INNER(hjstate));
+                                                               0);
+
+               /* Remember whether we need to save tuples with null join keys */
+               hjstate->hj_KeepNullTuples = HJ_FILL_OUTER(hjstate);
+               hashstate->keep_null_tuples = HJ_FILL_INNER(hjstate);
 
                /*
                 * Set up the skew table hash function while we have a record of the
@@ -925,6 +1037,7 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
         * initialize hash-specific info
         */
        hjstate->hj_HashTable = NULL;
+       hjstate->hj_NullOuterTupleStore = NULL;
        hjstate->hj_FirstOuterTupleSlot = NULL;
 
        hjstate->hj_CurHashValue = 0;
@@ -948,6 +1061,23 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 void
 ExecEndHashJoin(HashJoinState *node)
 {
+       HashState  *hashNode = castNode(HashState, innerPlanState(node));
+
+       /*
+        * Free tuple stores if we made them (must do this before
+        * ExecHashTableDestroy deletes hashCxt)
+        */
+       if (node->hj_NullOuterTupleStore)
+       {
+               tuplestore_end(node->hj_NullOuterTupleStore);
+               node->hj_NullOuterTupleStore = NULL;
+       }
+       if (hashNode->null_tuple_store)
+       {
+               tuplestore_end(hashNode->null_tuple_store);
+               hashNode->null_tuple_store = NULL;
+       }
+
        /*
         * Free hash table
         */
@@ -1016,11 +1146,19 @@ ExecHashJoinOuterGetTuple(PlanState *outerNode,
 
                        if (!isnull)
                        {
+                               /* normal case with a non-null join key */
                                /* remember outer relation is not empty for possible rescan */
                                hjstate->hj_OuterNotEmpty = true;
 
                                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);
+                       }
 
                        /*
                         * That tuple couldn't match because of a NULL, so discard it and
@@ -1088,7 +1226,17 @@ ExecParallelHashJoinOuterGetTuple(PlanState *outerNode,
                                                                                                                                  &isnull));
 
                        if (!isnull)
+                       {
+                               /* 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);
+                       }
 
                        /*
                         * That tuple couldn't match because of a NULL, so discard it and
@@ -1275,6 +1423,14 @@ ExecParallelHashJoinNewBatch(HashJoinState *hjstate)
        int                     start_batchno;
        int                     batchno;
 
+       /*
+        * If we are a very slow worker, MultiExecParallelHash could have observed
+        * build_barrier phase PHJ_BUILD_FREE and not bothered to set up batch
+        * accessors.  In that case we must be done.
+        */
+       if (hashtable->batches == NULL)
+               return false;
+
        /*
         * If we were already attached to a batch, remember not to bother checking
         * it again, and detach from it (possibly freeing the hash table if we are
@@ -1497,6 +1653,17 @@ ExecReScanHashJoin(HashJoinState *node)
        PlanState  *outerPlan = outerPlanState(node);
        PlanState  *innerPlan = innerPlanState(node);
 
+       /*
+        * We're always going to rescan the outer rel, so drop the associated
+        * null-keys tuplestore; we'll rebuild it during the rescan.  (Must do
+        * this before ExecHashTableDestroy deletes hashCxt.)
+        */
+       if (node->hj_NullOuterTupleStore)
+       {
+               tuplestore_end(node->hj_NullOuterTupleStore);
+               node->hj_NullOuterTupleStore = NULL;
+       }
+
        /*
         * In a multi-batch join, we currently have to do rescans the hard way,
         * primarily because batch temp files may have already been released. But
@@ -1506,6 +1673,10 @@ ExecReScanHashJoin(HashJoinState *node)
         */
        if (node->hj_HashTable != NULL)
        {
+               HashState  *hashNode = castNode(HashState, innerPlan);
+
+               Assert(hashNode->hashtable == node->hj_HashTable);
+
                if (node->hj_HashTable->nbatch == 1 &&
                        innerPlan->chgParam == NULL)
                {
@@ -1530,15 +1701,20 @@ ExecReScanHashJoin(HashJoinState *node)
                         */
                        node->hj_OuterNotEmpty = false;
 
+                       /*
+                        * Also, rewind inner null-key tuplestore so that we can return
+                        * those tuples again.
+                        */
+                       if (hashNode->null_tuple_store)
+                               tuplestore_rescan(hashNode->null_tuple_store);
+
                        /* ExecHashJoin can skip the BUILD_HASHTABLE step */
                        node->hj_JoinState = HJ_NEED_NEW_OUTER;
                }
                else
                {
                        /* must destroy and rebuild hash table */
-                       HashState  *hashNode = castNode(HashState, innerPlan);
 
-                       Assert(hashNode->hashtable == node->hj_HashTable);
                        /* accumulate stats from old hash table, if wanted */
                        /* (this should match ExecShutdownHash) */
                        if (hashNode->ps.instrument && !hashNode->hinstrument)
@@ -1546,6 +1722,14 @@ ExecReScanHashJoin(HashJoinState *node)
                        if (hashNode->hinstrument)
                                ExecHashAccumInstrumentation(hashNode->hinstrument,
                                                                                         hashNode->hashtable);
+
+                       /* free inner null-key tuplestore before ExecHashTableDestroy */
+                       if (hashNode->null_tuple_store)
+                       {
+                               tuplestore_end(hashNode->null_tuple_store);
+                               hashNode->null_tuple_store = NULL;
+                       }
+
                        /* for safety, be sure to clear child plan node's pointer too */
                        hashNode->hashtable = NULL;
 
@@ -1601,7 +1785,6 @@ ExecParallelHashJoinPartitionOuter(HashJoinState *hjstate)
        ExprContext *econtext = hjstate->js.ps.ps_ExprContext;
        HashJoinTable hashtable = hjstate->hj_HashTable;
        TupleTableSlot *slot;
-       uint32          hashvalue;
        int                     i;
 
        Assert(hjstate->hj_FirstOuterTupleSlot == NULL);
@@ -1610,6 +1793,7 @@ ExecParallelHashJoinPartitionOuter(HashJoinState *hjstate)
        for (;;)
        {
                bool            isnull;
+               uint32          hashvalue;
 
                slot = ExecProcNode(outerState);
                if (TupIsNull(slot))
@@ -1624,6 +1808,7 @@ ExecParallelHashJoinPartitionOuter(HashJoinState *hjstate)
 
                if (!isnull)
                {
+                       /* normal case with a non-null join key */
                        int                     batchno;
                        int                     bucketno;
                        bool            shouldFree;
@@ -1637,6 +1822,15 @@ ExecParallelHashJoinPartitionOuter(HashJoinState *hjstate)
                        if (shouldFree)
                                heap_free_minimal_tuple(mintup);
                }
+               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);
+               }
+               /* else we can just discard the tuple immediately */
+
                CHECK_FOR_INTERRUPTS();
        }
 
@@ -1715,6 +1909,7 @@ ExecHashJoinReInitializeDSM(HashJoinState *state, ParallelContext *pcxt)
 {
        int                     plan_node_id = state->js.ps.plan->plan_node_id;
        ParallelHashJoinState *pstate;
+       HashState  *hashNode;
 
        /* Nothing to do if we failed to create a DSM segment. */
        if (pcxt->seg == NULL)
@@ -1744,6 +1939,20 @@ ExecHashJoinReInitializeDSM(HashJoinState *state, ParallelContext *pcxt)
        /* Clear any shared batch files. */
        SharedFileSetDeleteAll(&pstate->fileset);
 
+       /* We'd better clear our local null-key tuplestores, too. */
+       if (state->hj_NullOuterTupleStore)
+       {
+               tuplestore_end(state->hj_NullOuterTupleStore);
+               state->hj_NullOuterTupleStore = NULL;
+       }
+       hashNode = (HashState *) innerPlanState(state);
+       if (hashNode->null_tuple_store)
+       {
+               tuplestore_end(hashNode->null_tuple_store);
+               hashNode->null_tuple_store = NULL;
+       }
+
+
        /* Reset build_barrier to PHJ_BUILD_ELECT so we can go around again. */
        BarrierInit(&pstate->build_barrier, 0);
 }
index 273a4c9b02f51a171ae3e63fabe4bd2b47a34a10..caad7cad0b44761d18207dcc20da83d439fd6c4d 100644 (file)
@@ -1153,6 +1153,38 @@ tuplestore_gettupleslot(Tuplestorestate *state, bool forward,
        }
 }
 
+/*
+ * tuplestore_gettupleslot_force - exported function to fetch a tuple
+ *
+ * This is identical to tuplestore_gettupleslot except the given slot can be
+ * any kind of slot; it need not be one that will accept a MinimalTuple.
+ */
+bool
+tuplestore_gettupleslot_force(Tuplestorestate *state, bool forward,
+                                                         bool copy, TupleTableSlot *slot)
+{
+       MinimalTuple tuple;
+       bool            should_free;
+
+       tuple = (MinimalTuple) tuplestore_gettuple(state, forward, &should_free);
+
+       if (tuple)
+       {
+               if (copy && !should_free)
+               {
+                       tuple = heap_copy_minimal_tuple(tuple, 0);
+                       should_free = true;
+               }
+               ExecForceStoreMinimalTuple(tuple, slot, should_free);
+               return true;
+       }
+       else
+       {
+               ExecClearTuple(slot);
+               return false;
+       }
+}
+
 /*
  * tuplestore_advance - exported function to adjust position without fetching
  *
index 064df01811e878d74da1b599e9293c30d587e66f..07f4b1f7490bfdc8817c04e0fdce09f93a8e56ea 100644 (file)
@@ -346,7 +346,7 @@ extern ExprState *ExecBuildHash32Expr(TupleDesc desc,
                                                                          const List *collations,
                                                                          const List *hash_exprs,
                                                                          const bool *opstrict, PlanState *parent,
-                                                                         uint32 init_value, bool keep_nulls);
+                                                                         uint32 init_value);
 extern ExprState *ExecBuildGroupingEqual(TupleDesc ldesc, TupleDesc rdesc,
                                                                                 const TupleTableSlotOps *lops, const TupleTableSlotOps *rops,
                                                                                 int numCols,
index 9cc82cdea4b6f6f59f98e4d704108118ff33b547..4d342174b9a5136093960dd759a98f4d39636bc2 100644 (file)
  * inner batch file.  Subsequently, while reading either inner or outer batch
  * files, we might find tuples that no longer belong to the current batch;
  * if so, we just dump them out to the correct batch file.
+ *
+ * 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.)
  * ----------------------------------------------------------------
  */
 
@@ -329,9 +338,16 @@ typedef struct HashJoinTableData
 
        bool            growEnabled;    /* flag to shut off nbatch increases */
 
-       double          totalTuples;    /* # tuples obtained from inner plan */
-       double          partialTuples;  /* # tuples obtained from inner plan by me */
-       double          skewTuples;             /* # tuples inserted into skew tuples */
+       /*
+        * totalTuples is the running total of tuples inserted into either the
+        * main or skew hash tables.  reportTuples is the number of tuples that we
+        * want EXPLAIN to show as output from the Hash node (this includes saved
+        * null-keyed tuples as well as those inserted into the hash tables).
+        * skewTuples is the number of tuples present in the skew hash table.
+        */
+       double          totalTuples;
+       double          reportTuples;
+       double          skewTuples;
 
        /*
         * These arrays are allocated for the life of the hash join, but only if
index 4970bb612954694af3f7253672e05d50d0006fac..9ff493b627a76816cda4878f54111ac48728a12f 100644 (file)
@@ -64,6 +64,7 @@ extern void ExecChooseHashTableSize(double ntuples, int tupwidth, bool useskew,
                                                                        int *numbatches,
                                                                        int *num_skew_mcvs);
 extern int     ExecHashGetSkewBucket(HashJoinTable hashtable, uint32 hashvalue);
+extern Tuplestorestate *ExecHashBuildNullTupleStore(HashJoinTable hashtable);
 extern void ExecHashEstimate(HashState *node, ParallelContext *pcxt);
 extern void ExecHashInitializeDSM(HashState *node, ParallelContext *pcxt);
 extern void ExecHashInitializeWorker(HashState *node, ParallelWorkerContext *pwcxt);
index 0716c5a9aed3f3c6d9c76f509fc0976e74cba403..2162ff56c38bf613b87058efc3e343ba998893c9 100644 (file)
@@ -2201,8 +2201,11 @@ typedef struct MergeJoinState
  *             hj_NullOuterTupleSlot   prepared null tuple for right/right-anti/full
  *                                                             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)
  *             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
  *             hj_MatchedOuter                 true if found a join match for current outer
  *             hj_OuterNotEmpty                true if outer relation known not empty
  * ----------------
@@ -2226,8 +2229,10 @@ typedef struct HashJoinState
        TupleTableSlot *hj_HashTupleSlot;
        TupleTableSlot *hj_NullOuterTupleSlot;
        TupleTableSlot *hj_NullInnerTupleSlot;
+       Tuplestorestate *hj_NullOuterTupleStore;
        TupleTableSlot *hj_FirstOuterTupleSlot;
        int                     hj_JoinState;
+       bool            hj_KeepNullTuples;
        bool            hj_MatchedOuter;
        bool            hj_OuterNotEmpty;
 } HashJoinState;
@@ -2664,6 +2669,9 @@ typedef struct HashState
        FmgrInfo   *skew_hashfunction;  /* lookup data for skew hash function */
        Oid                     skew_collation; /* collation to call skew_hashfunction with */
 
+       Tuplestorestate *null_tuple_store;      /* where to put null-keyed tuples */
+       bool            keep_null_tuples;       /* do we need to save such tuples? */
+
        /*
         * In a parallelized hash join, the leader retains a pointer to the
         * shared-memory stats area in its shared_info field, and then copies the
index 1c08e219e89d5cb7d620e376dbcd805f2b998c19..f638b96e15607edf4b75f5bc8bba0bbe46ee4bab 100644 (file)
@@ -73,6 +73,9 @@ extern bool tuplestore_in_memory(Tuplestorestate *state);
 extern bool tuplestore_gettupleslot(Tuplestorestate *state, bool forward,
                                                                        bool copy, TupleTableSlot *slot);
 
+extern bool tuplestore_gettupleslot_force(Tuplestorestate *state, bool forward,
+                                                                                 bool copy, TupleTableSlot *slot);
+
 extern bool tuplestore_advance(Tuplestorestate *state, bool forward);
 
 extern bool tuplestore_skiptuples(Tuplestorestate *state,
index ea3dabff77c35fb10e9360fb63c48b4eb6252f68..a2bd5d20b63c5755676af0a33d37dbe83847d9bc 100644 (file)
@@ -5027,7 +5027,7 @@ order by fault;
 explain (costs off)
 select * from
 (values (1, array[10,20]), (2, array[20,30])) as v1(v1x,v1ys)
-left join (values (1, 10), (2, 20)) as v2(v2x,v2y) on v2x = v1x
+left join (values (1, 10), (2, 20), (2, null)) as v2(v2x,v2y) on v2x = v1x
 left join unnest(v1ys) as u1(u1y) on u1y = v2y;
                          QUERY PLAN                          
 -------------------------------------------------------------
@@ -5043,13 +5043,14 @@ left join unnest(v1ys) as u1(u1y) on u1y = v2y;
 
 select * from
 (values (1, array[10,20]), (2, array[20,30])) as v1(v1x,v1ys)
-left join (values (1, 10), (2, 20)) as v2(v2x,v2y) on v2x = v1x
+left join (values (1, 10), (2, 20), (2, null)) as v2(v2x,v2y) on v2x = v1x
 left join unnest(v1ys) as u1(u1y) on u1y = v2y;
  v1x |  v1ys   | v2x | v2y | u1y 
 -----+---------+-----+-----+-----
    1 | {10,20} |   1 |  10 |  10
    2 | {20,30} |   2 |  20 |  20
-(2 rows)
+   2 | {20,30} |   2 |     |    
+(3 rows)
 
 --
 -- test handling of potential equivalence clauses above outer joins
index bc7cc76467ffaee0931436fdbff65c2421537b8b..75009e297208bd4e1b368f1abc6aa3e1451665f0 100644 (file)
@@ -53,6 +53,7 @@ $$;
 -- estimated size.
 create table simple as
   select generate_series(1, 20000) AS id, 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
+insert into simple values (null, null);
 alter table simple set (parallel_workers = 2);
 analyze simple;
 -- Make a relation whose size we will under-estimate.  We want stats
@@ -308,7 +309,7 @@ $$);
 select count(*) from simple r full outer join simple s using (id);
  count 
 -------
- 20000
+ 20002
 (1 row)
 
 rollback to settings;
@@ -787,7 +788,7 @@ explain (costs off)
 select  count(*) from simple r full outer join simple s using (id);
  count 
 -------
- 20000
+ 20002
 (1 row)
 
 rollback to settings;
@@ -810,7 +811,7 @@ explain (costs off)
 select  count(*) from simple r full outer join simple s using (id);
  count 
 -------
- 20000
+ 20002
 (1 row)
 
 rollback to settings;
@@ -835,7 +836,7 @@ explain (costs off)
 select  count(*) from simple r full outer join simple s using (id);
  count 
 -------
- 20000
+ 20002
 (1 row)
 
 rollback to settings;
@@ -858,7 +859,7 @@ explain (costs off)
 select  count(*) from simple r full outer join simple s on (r.id = 0 - s.id);
  count 
 -------
- 40000
+ 40002
 (1 row)
 
 rollback to settings;
@@ -881,7 +882,7 @@ explain (costs off)
 select  count(*) from simple r full outer join simple s on (r.id = 0 - s.id);
  count 
 -------
- 40000
+ 40002
 (1 row)
 
 rollback to settings;
@@ -906,7 +907,7 @@ explain (costs off)
 select  count(*) from simple r full outer join simple s on (r.id = 0 - s.id);
  count 
 -------
- 40000
+ 40002
 (1 row)
 
 rollback to settings;
index ad90c326c000ca4b31d6cf3fae4a97335e0df514..7f3449e2c84c39c3e53eef517f9c2f73fdf4ec9d 100644 (file)
@@ -1726,12 +1726,12 @@ order by fault;
 explain (costs off)
 select * from
 (values (1, array[10,20]), (2, array[20,30])) as v1(v1x,v1ys)
-left join (values (1, 10), (2, 20)) as v2(v2x,v2y) on v2x = v1x
+left join (values (1, 10), (2, 20), (2, null)) as v2(v2x,v2y) on v2x = v1x
 left join unnest(v1ys) as u1(u1y) on u1y = v2y;
 
 select * from
 (values (1, array[10,20]), (2, array[20,30])) as v1(v1x,v1ys)
-left join (values (1, 10), (2, 20)) as v2(v2x,v2y) on v2x = v1x
+left join (values (1, 10), (2, 20), (2, null)) as v2(v2x,v2y) on v2x = v1x
 left join unnest(v1ys) as u1(u1y) on u1y = v2y;
 
 --
index 53db1754bb261e4903dfb99b40457fae04bf89e5..989390e686486b1622179e6a84904a6123be8f85 100644 (file)
@@ -57,6 +57,7 @@ $$;
 -- estimated size.
 create table simple as
   select generate_series(1, 20000) AS id, 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
+insert into simple values (null, null);
 alter table simple set (parallel_workers = 2);
 analyze simple;