Why RIGHT SEMI JOIN Can Be Slower Than LEFT SEMI JOIN in Velox
TL;DR
A query rewrite from A LEFT SEMI JOIN B to B RIGHT SEMI JOIN A can be much
slower in Velox, even though the two forms are semantically equivalent.
At first glance, this rewrite looks attractive when A is much smaller than
B: building on A should reduce build-side work and often helps regular hash
joins. In one real query, we made exactly this change expecting a speedup, but
instead observed roughly a 10x regression.
The root cause is execution asymmetry:
- RIGHT SEMI needs to mark build-side rows as matched.
- That marking (
setProbedFlag) is random-memory-write heavy. - With duplicate or skewed keys, redundant marking work grows quickly.
A targeted optimization for RIGHT SEMI FILTER without extra filter moves marking into probe-time hit traversal and adds early stop logic for duplicate chains, significantly reducing redundant work.
The Symptom
In profiling, a large fraction of CPU was concentrated in
RowContainer::setProbedFlag, with additional time in join result listing and
runtime plumbing.
This aligned with prior intuition from the community:
- setting probed flags is expensive due to random access in row storage,
- and marking during probing may be more efficient than marking later from expanded join results.
Why LEFT SEMI and RIGHT SEMI Differ in Cost
A LEFT SEMI JOIN B and B RIGHT SEMI JOIN A are result-equivalent, but they
are not implementation-equivalent.
LEFT SEMI (no extra filter)
The operator mainly answers: does this probe row have at least one match?
That keeps the path probe-centric and avoids right-side matched-row tracking machinery.
RIGHT SEMI FILTER
The operator must output build-side rows that matched at least one probe row. That requires:
- a probed flag per relevant build row,
- setting the flag when matches are found,
- scanning build rows and outputting those with
probed = true.
This extra state tracking and scan are absent in the same form on LEFT SEMI.
