| From: | David Rowley <dgrowleyml(at)gmail(dot)com> |
|---|---|
| To: | Tomas Vondra <tomas(at)vondra(dot)me> |
| Cc: | Dan Stefura <dstefura(at)bluecatnetworks(dot)com>, "pgsql-bugs(at)lists(dot)postgresql(dot)org" <pgsql-bugs(at)lists(dot)postgresql(dot)org> |
| Subject: | Re: Hash Semi Join 5,000-50,000x slower on PG18 vs PG17 with 10+ equality columns and NULL values (identical plan, no spill) |
| Date: | 2026-07-31 05:24:31 |
| Message-ID: | CAApHDvq1p__jN_4mebz6JC=71TA0Dxzh8moJk1vk72_o5F_5wg@mail.gmail.com |
| Views: | Whole Thread | Raw Message | Download mbox | Resend email |
| Thread: | |
| Lists: | pgsql-bugs |
On Fri, 31 Jul 2026 at 13:19, Tomas Vondra <tomas(at)vondra(dot)me> wrote:
> commit 9ca67658d19e6c258eb4021a326ed7d38b3ab75f
> Author: David Rowley <drowley(at)postgresql(dot)org>
> Date: Thu Oct 17 14:25:08 2024 +1300
>
> Don't store intermediate hash values in ExprState->resvalue
>
> adf97c156 made it so ExprStates could support hashing and changed
> Hash Join to use that instead of manually extracting Datums from
> tuples and hashing them one column at a time.
>
> ...
>
> I don't quite see why, exactly. David, any ideas?
Looks like I used the wrong place to store the hash result when
aborting due to NULLs for EEOP_HASHDATUM_NEXT32_STRICT and
EEOP_HASHDATUM_FIRST_STRICT. Because this isn't an outer join, we
normally don't want to insert records with NULL hash keys into the
hash table. The *_STRICT operators are meant to handle this by
aborting early when we encounter a NULL, then MultiExecPrivateHash()
sees the isnull and heads to the "else if (node->keep_null_tuples)"
path instead.
You can see when assembling this expression in ExecBuildHash32Expr()
that I adjust the place where to store the result of the hash
depending on whether it's the final step or not. This code:
if (i == num_exprs - 1)
{
/* the result for hashing the final expr is stored in the state */
scratch.resvalue = &state->resvalue;
scratch.resnull = &state->resnull;
}
else
{
Assert(iresult != NULL);
/* intermediate values are stored in an intermediate result */
scratch.resvalue = &iresult->value;
scratch.resnull = &iresult->isnull;
}
So, if it's the last hash key we're hashing, store the result in the
ExprState's resvalue/resnull. Or if we're doing an intermediate hash
key, store it in the place for the intermediate result. The problem is
that if we abort before the final hash key due to finding a NULL, then
we only store the hashed value in the intermediate value location. We
then immediately do EEO_JUMP(op->d.hashdatum.jumpdone); which does not
pick up the intermediate value we just stored.
What should be happening is that when we abort early due to a NULL, we
should store that directly to state->resnull and state->resvalue.
I'm quite surprised this bug has lasted so long. It does seem only to
be a performance problem of hashing things we don't need and don't
ever look up again. I suspect most hash joins have a single hash key,
which are not affected by this, but still surprising since multi-key
hash joins are still very common.
Here's a WIP patch. I'm still looking at the JIT version to see if
that needs to be adjusted.
David
| Attachment | Content-Type | Size |
|---|---|---|
| wip_fix_for_strict_hashing.patch | application/octet-stream | 948 bytes |
| From | Date | Subject | |
|---|---|---|---|
| Next Message | Tristan Partin | 2026-07-31 05:41:01 | Re: BUG #19589: JSON_QUERY rejects domain-over-bytea input when using FORMAT JSON ENCODING UTF8. |
| Previous Message | Tender Wang | 2026-07-31 02:52:55 | Re: DO NOT pull up a sublink when it has no join condition with the upper relation |