Repository navigation
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #25810 +/- ##
==========================================
- Coverage 82.55% 82.55% -0.01%
==========================================
Files 1141 1141
Lines 440209 440212 +3
Branches 440209 440212 +3
==========================================
- Hits 363409 363399 -10
- Misses 54829 54836 +7
- Partials 21971 21977 +6 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
…ction PullUpCorrelatedExpr moved a correlated filter from below a Window to the decorrelated join. The window function then ran over all rows of its input instead of only the rows for each outer row, so row_number(), rank(), lag() and similar functions gave wrong values. A Window whose input holds an outer reference of the current scope now sets can_pull_up = false. The subquery stays correlated and fails with a not-implemented error instead of wrong results. A correlated filter above the window is still pulled up. Closes apache#25792 Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
4215cfc to
3ebce96
Compare
jayzhan211
left a comment
There was a problem hiding this comment.
Thanks @adriangb , 1 comment
| // rows for each outer row. If the filter moves above the window, | ||
| // the window function runs over all rows of the input, and the | ||
| // values it computes are different. | ||
| LogicalPlan::Window(ref window) if holds_outer_reference(&window.input) => { |
There was a problem hiding this comment.
This also rejects queries that main already decorrelates correctly: when the window is PARTITION BY the inner column of the correlated equality, pulling the filter up keeps the result. Both queries below return correct rows on main (2, and (1,1,1),(2,2,1),(2,3,2)) and error on this branch, which contradicts "a query that decorrelates correctly does not change."
CREATE TABLE po(k INT) AS VALUES (1), (2), (5);
CREATE TABLE pb(id INT, y INT) AS VALUES (1, 1), (2, 2), (3, 2);
SELECT po.k FROM po WHERE EXISTS (SELECT 1 FROM (SELECT b.id, row_number() OVER (PARTITION BY b.y ORDER BY b.id) AS rn FROM (SELECT * FROM pb WHERE pb.y = po.k) AS b) AS w WHERE w.rn = 2);
SELECT po.k, sub.id, sub.rn FROM po, LATERAL (SELECT pb.id, row_number() OVER (PARTITION BY pb.y ORDER BY pb.id) AS rn FROM pb WHERE pb.y = po.k) AS sub;Suggest allowing the pull-up when every correlated predicate in window.input is inner_col = outer_ref with inner_col in each window expr's partition_by. Better still, append inner_col to partition_by, which fixes the unpartitioned top-N-per-row case too.
apache#25838) ## Which issue does this PR close? - Closes apache#25834. - Closes apache#25792. This PR also contains the code change of apache#25810 (`Window`) and its tests (the test commit is the same). apache#25810 has no reviews yet, so I suggest to review this PR instead and close apache#25810 when this PR merges. Related: apache#25837 (found while writing the tests: the `LATERAL` test uses the table name as the alias of the derived table because of it), apache#25507, apache#25792, apache#25808, apache#25283. ## Rationale for this change A correlated subquery that has an ASOF join returns wrong rows when the correlated filter is on the right side of the join. ```sql CREATE TABLE o(k INT) AS VALUES (1), (2); CREATE TABLE l(id INT, ts INT) AS VALUES (1, 10); CREATE TABLE r(ts INT, val INT) AS VALUES (5, 1), (8, 2); SELECT o.k, s.rts FROM o, LATERAL ( SELECT r.ts AS rts FROM l ASOF JOIN (SELECT * FROM r WHERE r.val = o.k) AS r MATCH_CONDITION (l.ts >= r.ts) ) AS s ORDER BY o.k; ``` | | Result | |---|---| | Correct | `(1, 5), (2, 8)` | | `main` | `(2, 8)` | | This PR | `This feature is not implemented: Physical plan does not support logical expression OuterReferenceColumn(...)` | The filter `r.val = o.k` selects the rows the ASOF join can match. The decorrelation moves it above the join. There, the join has already matched `l` to `r.ts = 8` for every outer row, and the filter removes the row for `o.k = 1`. The root cause is not specific to the ASOF join. `PullUpCorrelatedExpr` moves a correlated filter above every plan node that it does not block explicitly. Each node it does not know is a possible wrong result. apache#25507 (outer join) and apache#25792 (window) are two more cases of the same problem. `PushDownFilter` asks the same question in the other direction, and there the default is safe: a filter stays where it is unless the rule knows the node. ## What changes are included in this PR? `PullUpCorrelatedExpr::f_down` now matches every `LogicalPlan` variant, with no `_` arm. A new variant does not compile until somebody puts it in a group. | Group | Variants | What `f_down` does | |---|---|---| | Special arms (no change) | `Filter`, `Subquery`, `Join`, `Union`, `Sort`, `Extension`, `Limit` | Same as before | | Pass-through | `Projection`, `Aggregate`, `Distinct::All`, `Join` (preserved sides), `AsOfJoin` (left side), `Repartition`, `SubqueryAlias`, `TableScan`, `EmptyRelation`, `Values` | Continue. Stop when the node's own expressions hold an outer reference (same as before). | | Stop when the input holds an outer reference | `AsOfJoin` (right side) | New | | Stop when the input holds an outer reference | `Window` | New (same as apache#25810) | | Stop when the input holds an outer reference | `Distinct::On`, `Unnest`, `RecursiveQuery`, `Statement`, `Explain`, `Analyze`, `Dml`, `Ddl`, `Copy`, `DescribeTable` | New | "Stop" means the subquery stays correlated. The check uses `holds_outer_reference`, which does not go into a nested `Subquery`. The effect for each new stop: | Node | Before | After | |---|---|---| | `AsOfJoin`, filter on the right side | Wrong rows | Not-implemented error | | `Window` | Wrong rows | Not-implemented error | | `Unnest` | `Optimizer rule 'decorrelate_predicate_subquery' failed: Schema error: No field named t.id` | Not-implemented error | | `Distinct::On`, `RecursiveQuery` | Not reachable in the default optimizer (`ReplaceDistinctWithAggregate` runs first) or not seen in tests | Stop | | `Statement`, `Explain`, `Analyze`, `Dml`, `Ddl`, `Copy`, `DescribeTable` | Not in a subquery | Stop | ## What is the testing strategy for this PR? New `sqllogictest` cases: | File | Case | Without the fix | |---|---|---| | `asof_join.slt` | LATERAL, filter on the right side: `EXPLAIN` shows the subquery stays correlated, and the query errors | Wrong rows `(2, 8)` | | `asof_join.slt` | EXISTS, filter on the right side: the query errors | Wrong result (no rows, correct is `1`) | | `asof_join.slt` | EXISTS, filter on the left side: `EXPLAIN` and result | Same plan and result | | `asof_join.slt` | LATERAL, filter above the join: `EXPLAIN` and result | Same plan and result | | `subquery.slt` | EXISTS, filter below an `Unnest`: `EXPLAIN` and the error | Schema error | | `subquery.slt`, `lateral_join.slt` | Window cases from apache#25810 | Wrong rows | All new negative cases fail when `decorrelate.rs` is reverted to `main`. The full `sqllogictest` suite (including the TPC-H plan files) and `cargo test -p datafusion-optimizer` pass with no change to an existing test. ## Are there any user-facing changes? Queries that returned wrong rows (ASOF join, window) or failed with a schema error (unnest) now fail with a not-implemented error. No API change. Out of scope: the `Aggregate` and `Limit` arms still have exemptions that are too broad (apache#25808, apache#25283). This PR does not change them. `Distinct::All` is a pass-through, with the same behavior as an `Aggregate` without aggregate expressions. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <[email protected]>
apache#25838) ## Which issue does this PR close? - Closes apache#25834. - Closes apache#25792. This PR also contains the code change of apache#25810 (`Window`) and its tests (the test commit is the same). apache#25810 has no reviews yet, so I suggest to review this PR instead and close apache#25810 when this PR merges. Related: apache#25837 (found while writing the tests: the `LATERAL` test uses the table name as the alias of the derived table because of it), apache#25507, apache#25792, apache#25808, apache#25283. ## Rationale for this change A correlated subquery that has an ASOF join returns wrong rows when the correlated filter is on the right side of the join. ```sql CREATE TABLE o(k INT) AS VALUES (1), (2); CREATE TABLE l(id INT, ts INT) AS VALUES (1, 10); CREATE TABLE r(ts INT, val INT) AS VALUES (5, 1), (8, 2); SELECT o.k, s.rts FROM o, LATERAL ( SELECT r.ts AS rts FROM l ASOF JOIN (SELECT * FROM r WHERE r.val = o.k) AS r MATCH_CONDITION (l.ts >= r.ts) ) AS s ORDER BY o.k; ``` | | Result | |---|---| | Correct | `(1, 5), (2, 8)` | | `main` | `(2, 8)` | | This PR | `This feature is not implemented: Physical plan does not support logical expression OuterReferenceColumn(...)` | The filter `r.val = o.k` selects the rows the ASOF join can match. The decorrelation moves it above the join. There, the join has already matched `l` to `r.ts = 8` for every outer row, and the filter removes the row for `o.k = 1`. The root cause is not specific to the ASOF join. `PullUpCorrelatedExpr` moves a correlated filter above every plan node that it does not block explicitly. Each node it does not know is a possible wrong result. apache#25507 (outer join) and apache#25792 (window) are two more cases of the same problem. `PushDownFilter` asks the same question in the other direction, and there the default is safe: a filter stays where it is unless the rule knows the node. ## What changes are included in this PR? `PullUpCorrelatedExpr::f_down` now matches every `LogicalPlan` variant, with no `_` arm. A new variant does not compile until somebody puts it in a group. | Group | Variants | What `f_down` does | |---|---|---| | Special arms (no change) | `Filter`, `Subquery`, `Join`, `Union`, `Sort`, `Extension`, `Limit` | Same as before | | Pass-through | `Projection`, `Aggregate`, `Distinct::All`, `Join` (preserved sides), `AsOfJoin` (left side), `Repartition`, `SubqueryAlias`, `TableScan`, `EmptyRelation`, `Values` | Continue. Stop when the node's own expressions hold an outer reference (same as before). | | Stop when the input holds an outer reference | `AsOfJoin` (right side) | New | | Stop when the input holds an outer reference | `Window` | New (same as apache#25810) | | Stop when the input holds an outer reference | `Distinct::On`, `Unnest`, `RecursiveQuery`, `Statement`, `Explain`, `Analyze`, `Dml`, `Ddl`, `Copy`, `DescribeTable` | New | "Stop" means the subquery stays correlated. The check uses `holds_outer_reference`, which does not go into a nested `Subquery`. The effect for each new stop: | Node | Before | After | |---|---|---| | `AsOfJoin`, filter on the right side | Wrong rows | Not-implemented error | | `Window` | Wrong rows | Not-implemented error | | `Unnest` | `Optimizer rule 'decorrelate_predicate_subquery' failed: Schema error: No field named t.id` | Not-implemented error | | `Distinct::On`, `RecursiveQuery` | Not reachable in the default optimizer (`ReplaceDistinctWithAggregate` runs first) or not seen in tests | Stop | | `Statement`, `Explain`, `Analyze`, `Dml`, `Ddl`, `Copy`, `DescribeTable` | Not in a subquery | Stop | ## What is the testing strategy for this PR? New `sqllogictest` cases: | File | Case | Without the fix | |---|---|---| | `asof_join.slt` | LATERAL, filter on the right side: `EXPLAIN` shows the subquery stays correlated, and the query errors | Wrong rows `(2, 8)` | | `asof_join.slt` | EXISTS, filter on the right side: the query errors | Wrong result (no rows, correct is `1`) | | `asof_join.slt` | EXISTS, filter on the left side: `EXPLAIN` and result | Same plan and result | | `asof_join.slt` | LATERAL, filter above the join: `EXPLAIN` and result | Same plan and result | | `subquery.slt` | EXISTS, filter below an `Unnest`: `EXPLAIN` and the error | Schema error | | `subquery.slt`, `lateral_join.slt` | Window cases from apache#25810 | Wrong rows | All new negative cases fail when `decorrelate.rs` is reverted to `main`. The full `sqllogictest` suite (including the TPC-H plan files) and `cargo test -p datafusion-optimizer` pass with no change to an existing test. ## Are there any user-facing changes? Queries that returned wrong rows (ASOF join, window) or failed with a schema error (unnest) now fail with a not-implemented error. No API change. Out of scope: the `Aggregate` and `Limit` arms still have exemptions that are too broad (apache#25808, apache#25283). This PR does not change them. `Distinct::All` is a pass-through, with the same behavior as an `Aggregate` without aggregate expressions. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <[email protected]>
Which issue does this PR close?
Rationale for this change
A correlated filter below a window function gives wrong results. The window function must see only the rows that match the current outer row.
mainEXISTS(above)1,21NOT EXISTS52,51 IN (SELECT row_number() OVER (ORDER BY id) FROM b WHERE b.y = o.k)1,21max(rn)(1, 1),(2, 1),(5, NULL)(1, 1),(2, 2),(5, NULL)LATERALrn = 1fork = 2rn = 2fork = 2PullUpCorrelatedExprmoves the filterb.y = o.kfrom below theWindowAggrto the join that replaces the subquery. Thenrow_number()numbers the rows of all outer rows together.An error is better than wrong rows. A query that decorrelates correctly does not change.
What changes are included in this PR?
PullUpCorrelatedExpr::f_down, aLogicalPlan::Windowwhose input holds an outer reference setscan_pull_up = false. The subquery stays correlated.holds_outer_reference, which fix: keep a correlated filter below the nullable side of an outer join #25764 added, to find an outer reference in the window input. That helper does not go into aLogicalPlan::Subquery, because the outer references there belong to a nested scope (for example a nestedLATERAL).A correlated filter above the window does not change. The window input does not depend on the outer row, so the filter is pulled up as before.
This is the same class of bug as #25507 (correlated filter below an outer join), which #25764 fixed with the same guard pattern. #25808 is another bug of this class (
LATERALwithSELECT DISTINCT), which this PR does not fix.A later change can decorrelate some of these queries. For example, when the correlated filter is an equality on an inner column, the pull up can add that column to
PARTITION BY. This PR does not do that.What is the testing strategy for this PR?
New sqllogictest cases:
subquery.slt: anEXPLAINthat shows the subquery stays correlated, andstatement errorcases forEXISTS,NOT EXISTS,INand a scalar subquery. AnEXPLAINand a result for a correlated filter above the window, which is still decorrelated.lateral_join.slt: astatement errorcase for theLATERALform, and anEXPLAINand a result for a correlated filter above the window.Without the fix, all six new error and
EXPLAINcases fail: each query returns the wrong rows in the table above. The full sqllogictest suite andcargo test -p datafusion-optimizerpass, and no existing plan changes.Are there any user-facing changes?
Queries with a correlated filter below a window function now fail with a not-implemented error. Before, they returned wrong results. No public API changes.
🤖 Generated with Claude Code