Repository navigation
fix: keep build-side input order across batches in hash join - #26228
dwsmith1983 wants to merge 1 commit into
Conversation
On the hash map build path the build batches were concatenated in reverse batch order, so build rows emitted by index (anti and semi joins, unmatched outer rows) came out last batch first. Batches are still inserted last to first so each key's chain keeps its earliest row on top, but every batch now gets its forward-order start offset and the batches are concatenated forward. Closes apache#26226
andygrove
left a comment
There was a problem hiding this comment.
LGTM, thanks @dwsmith1983!
The offsets line up with the forward concat (num_rows is exactly the sum of the batch rows, so offset ends at 0), and the insertion order into the map is unchanged, so each probe row's matches still come back in build order. The new inner join test pins that nicely.
On performance: hashing and hash table inserts are identical, only the stored row indices change. I ran a paired benchmark of the old vs new offset schemes on the hash map path (4M-16M row builds with unique and duplicate keys, plus probe and gather) and saw no measurable difference: everything within about 1%, with confidence intervals spanning 1.0.
|
Filed #26235 as a follow-up: now that the build-side order no longer depends on which map the build uses, |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #26228 +/- ##
==========================================
- Coverage 82.83% 82.82% -0.01%
==========================================
Files 1149 1149
Lines 452344 452337 -7
Branches 452344 452337 -7
==========================================
- Hits 374680 374659 -21
- Misses 54864 54878 +14
Partials 22800 22800 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
@alamb can you take a look at this pr? |
|
Hi @dwsmith1983 -- thanks for this PR. Is this a general purpose change or something that is comet specific? It seems like maybe others would benefit if you could show a sort by build side was eliminated or something? I worry about adding a bunch of constrants to DataFusion for some specific usecase (Comet) that isn't used by other users of DataFusion I am not enough of an expert in the Join code to review this PR efficient. Perhaps @2010YOUY01 or @jayzhan211 could suggest someone who might be able to review? |
|
That is a fair question, @alamb. Comet is where this came up, and as the PR stands DataFusion itself doesn't rely on the order: HashJoinExec still reports the build side order as not maintained. The order can change on its own today, though. The array map path already returns build rows in input order, but the hash map path returned them last batch first. So the same anti or semi join comes back in a different order depending on how sparse the join keys are. With this change both paths agree. LeftSemi, LeftAnti and LeftMark then emit build rows in input order, so maintains_input_order could report that, and a sort above something like NOT EXISTS ... ORDER BY could be dropped. I can add that with an EXPLAIN test if it seems worthwhile. @andygrove, could you weigh in from the Comet side on whether Comet should depend on this ordering, or whether it's better handled there? |
|
I think this is both an optimization opportunity and a potential constraint on future features. For example, if HJ runs out of memory, the ordering guarantee could prevent it from switching to a spilling implementation that doesn't preserve that order: In general, I think we should prioritize general-purpose features over specialized optimizations. Perhaps we should first figure out how to support this kind of fallback cleanly before committing to the ordering guarantee. (though I'm also unsure if ordering promise itself is a common requirement that worth maintaining) |
jayzhan211
left a comment
There was a problem hiding this comment.
Thanks @dwsmith1983 , LGTM!
|
Maybe we could make this ordering guarantee configurable — based on whether spilling is enabled, or on some other config. But I'm fine with the current approach until we actually have a use case for that. |
|
Thanks @2010YOUY01, that's a good point, and I agree it's a reason not to promise the order yet. To be clear, this PR doesn't make that promise: The guarantee itself is tracked in #26235. I'll add your spilling example and @jayzhan211's idea of a config there so it gets settled before anyone picks that up. |
This PR is already making a promise with UTs asserting output order. If you only need it at operator level in comet, perhaps we can do a alternative constructor Instead of doing #26235, and make it into a plan-level optimization guarantee. @jayzhan211 suggested use config to use config to set the ordering promise, I think this might cause some extra challenge for planning/optimization. Thank you for the invetigations 🙏🏼 @dwsmith1983 |
|
Fair point: the tests pin the order, so in practice it's a promise. An opt-in flag sounds right to me and fits how Comet builds the join. Since @andygrove would that work for Comet? It would set the flag on the null-aware anti join and then drop the fallback from apache/datafusion-comet#6810 after upgrading. |
Which issue does this PR close?
Rationale for this change
A
HashJoinExecthat builds a hash map put whole build batches in reverse order, so aCollectLeftanti or semi join that returns build rows emitted them last batch first. The array map path already kept input order, so the output order depended on how sparse the join keys were. Comet relies on the build side's order for a null-aware anti join and gets unsorted partitions when keys are sparse and the build side has more than one batch.What changes are included in this PR?
As suggested in apache/datafusion-comet#6810 (comment):
concat_build_batchesloses itsreverseflag, which no caller needs anymore.HashJoinExecshow the new layout.What is the testing strategy for this PR?
New tests:
join_left_anti_keeps_build_order_across_batches: aLeftAntijoin over two build batches (keys[10, 20]and[3000, 4000]) with no matches must return10, 20, 3000, 4000. It fails on main for every batch size on the hash map path (returns3000, 4000, 10, 20) and passes on the array map path.join_inner_keeps_build_order_for_duplicate_keys_across_batches: duplicate keys across build batches still match in build order. This passes before and after; it pins the chain order so a future change can't flip it.prepared_build_keeps_build_order_across_batches: prepared build mode only supports inner joins, so this checks the concatenated build batch directly (10, 20, 3000, 4000) and then runs a join to confirm the hash map indices still point at the right rows. It fails on main with the build batch in3000, 4000, 10, 20order.cargo test -p datafusion-physical-plan --liband the sqllogictests pass; no existing expected output changed.Are there any user-facing changes?
Build-side rows from a hash join (anti, semi, and unmatched rows of outer joins in
CollectLeftmode) now come out in input order across build batches. No API changes.