perf: Emit LeftSemi hash join rows while probing - #24794
Draft
Dandandan wants to merge 2 commits into
Draft
Conversation
A `LeftSemi` hash join emitted nothing while probing: every matched build row
came out at the end from the visited bitmap, like `LeftAnti` and `LeftMark`.
`HashJoinExec` nevertheless reported `EmissionType::Incremental` for it,
grouped with Inner and RightSemi under "If we only need to generate matched
rows from the probe side" -- which is not what LeftSemi does, since its rows
come from the build side.
So the join was blocking, and a LIMIT above it bought nothing:
select ws_order_number from web_sales ws
where exists (select 1 from web_returns wr
where wr.wr_order_number = ws.ws_order_number)
limit 10
took 9.2 ms on TPC-DS SF1, against 8.6 ms for the same query with no LIMIT at
all. Mirrored so it plans as `RightSemi`, which is probe-driven, it took 2.8 ms.
A semi join only needs to know whether a build row has matched yet, and the
bitmap already carries that. Emitting a build row when its bit flips from unset
to set produces the same rows in the same order, while probing. The final stage
then has nothing left to do. The comment at the site said as much already:
"When visit the right batch, we can output the matched left row and don't need
to wait the end of loop".
The query above now takes 2.7 ms, matching the RightSemi form.
Only the hash join changes. `NestedLoopJoinExec` keeps emitting LeftSemi rows
at the end, so `need_produce_result_in_final` is untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgqwvctZdvR1ZCz2hbkEJC
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #24794 +/- ##
=======================================
Coverage 81.52% 81.52%
=======================================
Files 1123 1123
Lines 405970 406054 +84
Branches 405970 406054 +84
=======================================
+ Hits 330978 331051 +73
- Misses 55627 55639 +12
+ Partials 19365 19364 -1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Which issue does this PR close?
Rationale for this change
A
LeftSemihash join emits nothing while probing.adjust_indices_by_join_typereturns empty index arrays for it, and every matched build row comes out at the
end from the visited bitmap, the same way
LeftAntiandLeftMarkwork.HashJoinExec::compute_propertiesnevertheless reportsEmissionType::IncrementalforLeftSemi, grouped withInnerandRightSemiunder the comment "If we only need to generate matched rows fromthe probe side". That is not what
LeftSemidoes: its rows come from the buildside.
LeftAntiandLeftMarkare correctly classifiedBoth.So the join is blocking, and a
LIMITabove it buys nothing. On TPC-DS SF1:LeftSemi, withLIMIT 10LIMITat allRightSemi, withLIMIT 10What changes are included in this PR?
A semi join only needs to know whether a build row has matched yet, and the
visited bitmap already carries that. This emits a build row when its bit flips
from unset to set, which produces the same rows in the same order but while
probing rather than after it. The final stage then has nothing left to do.
The comment at that site already described the fix: "When visit the right
batch, we can output the matched left row and don't need to wait the end of
loop".
The query above now takes 2.7 ms, matching the
RightSemiform, andEmissionType::Incrementalbecomes true rather than aspirational.Only the hash join changes.
NestedLoopJoinExecstill emits itsLeftSemirows at the end, so
need_produce_result_in_finalis untouched.Are these changes tested?
Yes, by the existing coverage: the full sqllogictest suite (504 files) and the
1117 join unit tests pass. Semi joins are heavily covered by
joins.slt,subquery.sltand the hash join's own tests, including across partition modesand batch sizes.
One expectation changed, in
push_down_filter_parquet.slt. It is anEXPLAIN ANALYZEwhose join reportsinput_batches=1, input_rows=2where it used toreport
2and4;output_rowsis 2 either way. The join no longer holds itsoutput back, so the consumer finishes and the probe scan stops earlier.
Are there any user-facing changes?
LeftSemihash joins are no longer blocking, so aLIMITor any otherearly-terminating consumer above one can now stop it early. Results are
unchanged.