Dandandan opened a new pull request, #24784:
URL: https://github.com/apache/datafusion/pull/24784

   ## Which issue does this PR close?
   
   - Closes #.
   
   ## Rationale for this change
   
   A correlated scalar subquery must return at most one row per set of outer
   values. DataFusion enforced this in the analyzer by requiring an aggregate on
   top of the subquery. Without one, planning failed with
   
   ```
   Correlated scalar subquery must be aggregated to return at most one row
   ```
   
   So a plain attribute lookup did not plan at all:
   
   ```sql
   select o_orderkey, (select c_name from customer where c_custkey = o_custkey)
   from orders
   ```
   
   To run it you had to wrap the column in `min`, `max` or `any_value`. That
   aggregate does no useful work; it only proves a row count that the data 
already
   guarantees, and it costs a full hash aggregation over the subquery side.
   
   The single join from Neumann and Kemper's unnesting paper removes the need 
for
   it. It is an outer join that emits one row per row of its preserved side and
   raises an error when a second row matches, so the subquery decorrelates into 
a
   join with no aggregate at all.
   
   ## What changes are included in this PR?
   
   - `JoinType::LeftSingle` and `JoinType::RightSingle`. They behave like `Left`
     and `Right` except that a second match for a row of the preserved side 
fails
     with `Scalar subquery returned more than one row`, the error
     `ScalarSubqueryExec` already raises for uncorrelated scalar subqueries.
   - `ScalarSubqueryToJoin` uses a single join only for subqueries not already
     known to return at most one row. Subqueries that are known to, from an
     aggregate grouped on correlated columns or from functional dependencies
     showing the subquery unique on the join keys, keep their plain `LEFT JOIN`.
     The analyzer no longer rejects the rest.
   - `HashJoinExec` implements both directions, so `JoinSelection` can still 
swap
     the inputs to choose a build side. `NestedLoopJoinExec` implements both for
     correlations with no equijoin key. Both reuse the matched bitmaps as the
     duplicate detector, so the check costs one bit test per matched row and
     covers matches spread over batches and partitions.
   - `SortMergeJoinExec` and `PiecewiseMergeJoin` reject single joins, and the
     physical planner routes them to an operator that implements them.
   - Proto round-trip for the new variants. The Substrait producer reports them 
as
     unsupported, since Substrait has no equivalent.
   
   No query in TPC-H (22) or TPC-DS (99) produces a single join, so no plan in
   either suite changes. Every correlated scalar subquery in them aggregates a
   real value rather than proving a row count.
   
   ## Benchmarks
   
   TPC-H SF10, comparing the form you had to write before against the form that
   now plans. Median of 7 runs, interleaved and repeated.
   
   | | outer / subquery rows | forced aggregate | single join | |
   |---|---|---|---|---|
   | lookup in a projection | 100K / 1.5M | 15.1 ms | 6.1 ms | 2.5x faster |
   | lookup in a projection | 15M / 1.5M | 174 ms | 141 ms | 1.24x faster |
   | lookup in a projection | 60M / 2M | 565 ms | 542 ms | within noise |
   | lookup in a filter | 15M / 1.5M | 102 ms | 153 ms | 1.5x slower |
   | lookup in a filter, non-pushable predicate | 15M / 1.5M | 90 ms | 139 ms | 
1.55x slower |
   
   In a projection the aggregate is pure overhead, and removing it saves the 
share
   of the work the subquery side represents.
   
   In a filter it is not that simple. A plain `LEFT JOIN` lets the optimizer 
use a
   predicate on the subquery output: `eliminate_outer_join` makes the join inner
   because the predicate rejects nulls, `extract_equijoin_predicate` folds a
   comparison against an outer column into a second join key, and 
`eliminate_join`
   turns the result into a semi join. None of these are sound for a single join,
   because all three change how many rows match, which is what the single join
   must observe.
   
   So the rule tries harder to avoid needing a single join. Besides the 
aggregate
   it can see at the top of the subquery, it asks the decorrelated subquery's
   functional dependencies whether it is already unique on the columns the join
   equates with the outer plan. That covers uniqueness the subquery inherits
   rather than declares: a key that survives an extra condition in the 
correlated
   predicate, a `GROUP BY` on the correlated column with nothing to aggregate, 
and
   uniqueness carried up through the subquery's own joins.
   
   With `c_custkey` declared a primary key, the filter cases end up faster than
   the form they replace, because they get both the aggregate removal and the
   rewrites the left join enables:
   
   | | forced aggregate | single join path | |
   |---|---|---|---|
   | lookup in a filter | 87 ms | 55 ms | 1.6x faster |
   | lookup in a filter, non-pushable predicate | 97 ms | 85 ms | 1.15x faster |
   | lookup in a projection | 180 ms | 152 ms | 1.2x faster |
   
   On a table with no declared key and no way to infer uniqueness, the filter 
case
   keeps its 1.5x cost. That is the price of the stricter semantics, and it is
   opt-in: the aggregated query still plans exactly as before.
   
   ## Are these changes tested?
   
   Yes.
   
   - `subquery.slt` gains a single join section: attribute lookup with and 
without
     a match, the more-than-one-row error, the same in a filter rather than a
     projection, the nested loop path for a correlation with no equijoin key, 
and
     plan assertions for each way a subquery can show it needs no single join 
(an
     aggregate, a declared key surviving an extra predicate, a bare `GROUP BY`),
     plus one showing a key proves nothing under an inequality. Four existing
     cases changed from rejected by the analyzer to running.
   - `HashJoinExec` and `NestedLoopJoinExec` unit tests cover both directions
     across partition modes and batch sizes, the accepted and the rejected case,
     and the all-NULL build key path.
   - The full sqllogictest suite (504 files) and the workspace test suite pass.
   
   ## Are there any user-facing changes?
   
   - Correlated scalar subqueries without an aggregate now plan and run. When 
the
     data has a second matching row they fail at run time with `Scalar subquery
     returned more than one row` instead of failing to plan.
   - A `GROUP BY` in a correlated scalar subquery may now name columns the
     correlated predicate does not fix.
   - A `LIMIT` above the correlated predicate still cannot be decorrelated. That
     case now reports `Correlated scalar subquery with a LIMIT must be limited 
to
     a single row` instead of reporting a missing aggregate.
   - `JoinType` gains two variants, so exhaustive matches on it in downstream 
code
     need a new arm. This is an API change.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to