Expand description
Semi-join reduction through an aggregate.
When a grouped aggregate is inner-joined on one of its own grouping keys, only the groups whose key survives the join can appear in the result. Every other group is computed and then discarded. Filtering the aggregate’s input down to the surviving keys first produces exactly the same groups, because an aggregate value depends only on the rows sharing its key.
§The query that motivated this
TPC-H q17 decorrelates to this shape:
Inner Join: part.p_partkey = __scalar_sq_1.l_partkey
├── Inner Join: lineitem.l_partkey = part.p_partkey
│ └── Filter: p_brand = 'Brand#23' AND p_container = 'MED BOX'
└── __scalar_sq_1:
Aggregate: groupBy=[l_partkey], aggr=[avg(l_quantity)]
TableScan: lineitemAt SF100 that aggregate groups all 600M lineitem rows into ~20M groups, and
the join then keeps the ~2000 partkeys matching the brand and container —
four orders of magnitude of thrown-away work. Measured with
explain --analyze, it was 221.03 s of a 252 s query, 88% of all compute,
with spill_count=0: not a memory problem, just work that need not happen.
DataFusion’s dynamic filter does not help here, and it is worth recording why, because the plan looks like it should:
DynamicFilter [ ... l_partkey >= 7682 AND l_partkey <= 19999654 AND hash_lookup ... ]The min/max bounds span essentially the whole key domain, since the 2000 surviving partkeys are scattered uniformly across it, so row-group pruning removes nothing. And the filter belongs to the join, which sits downstream of the aggregate — no amount of selectivity there can reduce what the aggregate already had to read.
§What this rule does
It rewrites the aggregate’s input to a LeftSemi join against the smallest
subtree of the other side that still produces the join key and contains a
filter:
Aggregate: groupBy=[l_partkey], aggr=[avg(l_quantity)]
LeftSemi Join: lineitem.l_partkey = part.p_partkey
TableScan: lineitem
Projection: part.p_partkey
Filter: p_brand = 'Brand#23' AND p_container = 'MED BOX'§Why it is safe
- Inner joins only. Under a left/right/full join the unmatched rows are preserved, so dropping groups would change the result. Anti/semi joins are also excluded — they have their own null semantics.
- The key must be a grouping column, matched by schema position rather
than by name, so requalification through
SubqueryAliasand projections cannot silently pair the wrong columns. - Aggregate values are unchanged. Removing rows whose key is not in the probe side removes whole groups; it never removes part of a surviving group, so no aggregate is computed over a different row set.
- Nulls agree. A null key never satisfies an equi-join, so a null group
would be dropped by the original join anyway;
LeftSemidrops it too. - No duplication.
LeftSemiemits each left row at most once regardless of how many probe rows match, so counts and sums cannot inflate.
§Why it is guarded
The probe subtree is evaluated a second time, so the rule only fires when
that subtree contains a Filter — evidence there is real selectivity to
exploit. Against an unfiltered scan the semi-join would remove nothing and
we would have paid for the extra pass. Descent also stops at the filter
rather than continuing to the scan beneath it, which is what keeps the probe
small (~2000 rows in q17 rather than the whole part table).
Set KRISHIV_SEMI_JOIN_REDUCTION=off to disable.
Structs§
- Semi
Join Pushdown Through Inner Join - Push an existing semi-join down through an inner join, so the selective side filters one join input instead of the join’s output.
- Semi
Join Reduction From Selective Dimension - Reduce a fact stream by a selective dimension it is inner-joined to, before the join that needs it.
- Semi
Join Reduction Through Aggregate - Push a semi-join built from an inner join’s other side into the input of a grouped aggregate, when the join key is one of the grouping columns.
Constants§
- SEMI_
JOIN_ DIMENSION_ ENV - Environment switch for reduction from a selective dimension (the q7 rule).
- SEMI_
JOIN_ PUSHDOWN_ ENV - Environment switch for pushdown through an inner join (the q18 rule).
- SEMI_
JOIN_ REDUCTION_ ENV - Environment switch for reduction through an aggregate (the q17 rule).
Functions§
- semi_
join_ dimension_ reduction_ enabled - Whether reduction from a selective dimension is enabled (default: no).
- semi_
join_ pushdown_ enabled - Whether semi-join pushdown through an inner join is enabled (default: yes).
- semi_
join_ reduction_ enabled - Whether semi-join reduction through aggregates is enabled (default: yes).