Skip to content

fix(processor): do not compact flat join keys' selection vectors on null - #1141

Merged
adsharma merged 2 commits into
LadybugDB:mainfrom
zahariash:fix/hash-join-multi-key-null
Oct 7, 2026
Merged

adsharma merged 2 commits into
LadybugDB:mainfrom
zahariash:fix/hash-join-multi-key-null

Conversation

@zahariash

Copy link
Copy Markdown
Contributor

This work was produced with the help of language models.

Follow-up to #1119, answering the review comment on it. It also fixes the COUNT { } failure mentioned in that thread, which turned out to be the same bug.

Bug. JoinHashTable dropped null keys by compacting every key's selection vector, but each side restored only one key state. With two keys in different data chunks, every row after a null in the other key was lost, on either side of the join. This shows up as wrong results and segfaults: a correlated COUNT { } subquery after an OPTIONAL MATCH that found nothing returns {1|32001} instead of {0|1, 1|32000}.

Fix. Flat keys are never compacted: a null flat key just rules the row out. Only an unflat key state is ever compacted; the planner allows at most one per side, and each side already restores it. This also removes the per-row save/restore in the flat probe path (the "all keys' states should be restored" TODO) and the LEFT join hack.

Moving the save/restore into JoinHashTable. The build side now does it in appendVectors (second commit, separate so it can be dropped). The probe side cannot: the operators read through the compacted selection after probe() returns.

Cost. Instruction counts drop 0.4-2.5% on hash-join queries and are unchanged elsewhere (details below).

Instruction counts, 1 thread, against `main`, same graph and queries as #1119
query commit 1 commits 1+2
flat-key build called once per comment -1.83% -1.48%
chained OPTIONAL MATCH (#1109's shape) -2.68% -2.47%
correlated EXISTS -0.45% -0.42%
plain hash join; flat probe after WITH ~0 ~0
scan and aggregate queries without a hash join 0.00% 0.00%

The flat probe path no longer saves and restores per row. The second commit adds 0.2-0.35% over the first on flat-key builds (one more non-inlined call per append). Cycle differences are within noise.

Tests. The new null_keys.test fails on main (wrong results, segfault) and passes here; ctest: 0 of 2934 failed.

zahariash and others added 2 commits October 7, 2026 19:42
JoinHashTable dropped null keys with ValueVector::discardNull on every
key. For a flat key this set its data chunk's selection size to 0, and
each side of the join restored only one key state: the build the unflat
key state, the flat probe path keyVectors[0]'s. Flatten writes each next
row through that same selection vector without resetting its size, so
with two keys in different data chunks every row after a null in the
other key was lost, on either side of the join. LEFT joins carried a
hack that reset the key sizes to 1; COUNT joins had none.

A null flat key now only rules the row out, without touching its state;
only the unflat key state is compacted, and the side that owns it
restores it. appendVectors no longer ignores that no row qualifies, so
rows with a null flat key cannot enter the hash table. On a null key,
probe also clears the chain left by the previous probe: with a flat key
on the unflat probe path, matchUnFlatKey would otherwise follow it now
that the key keeps its selection size. The flat probe path no longer
needs to save and restore its key state per row, and the LEFT join hack
goes away. This also fixes a correlated COUNT subquery counting 1 for a
null outer row and crashes in other COUNT subquery shapes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ctors

JoinHashTable::appendVectors discards null keys by compacting the unflat
key state's selection vector, which the child writes its next batch
through, and HashJoinBuild worked around that by appending through a
copy and restoring the child's afterwards. appendVectors now makes the
copy and restores the child's selection vector itself, so the change and
its undo sit in one function and HashJoinBuild no longer needs
SelVectorOverWriter. The copy is still skipped when every unflat key has
a no-null guarantee.

The probe side keeps its save and restore in the operator: probe() fills
probedTuples by position in the compacted selection, and the probe and
the operators above it read through that selection until the next batch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@adsharma

adsharma commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

Thank you!

@adsharma
adsharma merged commit 33ef97a into LadybugDB:main Oct 7, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants