starocean999 opened a new pull request, #63763:
URL: https://github.com/apache/doris/pull/63763

   Problem Summary:
   
   The `AggScalarSubQueryToWindowFunction` rewrite rule (WinMagic) rewrites a 
correlated scalar subquery like:
   
   ```sql
   SELECT d.did, f.id, f.k, f.v
   FROM fact f, dim d
   WHERE f.k = d.k
     AND f.v * 2 > (
       SELECT SUM(f2.v)
       FROM fact f2
       WHERE f2.k = d.k
     )
   ```
   
   into a window-function plan:
   
   ```
   Filter(f.v * 2 > SUM(v) OVER (PARTITION BY d.k))
     Window(SUM(v) OVER (PARTITION BY d.k))
       CrossJoin(Scan(fact f), Scan(dim d))
   ```
   
   This rewrite had three correctness issues:
   
   ---
   
   **Bug 1 — Join amplification when the correlated key is not unique**
   
   ```sql
   -- dim has DUPLICATE KEY(did), so k is NOT unique and can be NULL
   CREATE TABLE dim (did INT, k INT, tag INT)
   ENGINE=OLAP DUPLICATE KEY(did) ...;
   
   -- data: two rows with the same k=1
   INSERT INTO dim VALUES (10, 1, 1), (11, 1, 1);
   
   -- fact and dim are joined on f.k = d.k
   SELECT d.did, f.id, f.k, f.v
   FROM fact f, dim d
   WHERE f.k = d.k
     AND f.v * 2 > (
       SELECT SUM(f2.v) FROM fact f2 WHERE f2.k = d.k
     );
   ```
   
   Before the fix, the rule **did not check** whether the correlated outer-only 
table's key (`d.k`) is unique. Since `dim` is a DUPLICATE KEY table, `k=1` has 
two rows. After the CrossJoin, each fact row with `k=1` joins with both dim 
rows, producing 2× more output rows than the original correlated scalar 
subquery (which is evaluated per outer row). The window function then sees this 
inflated row set, producing wrong aggregate values.
   
   **Fix:** Added `checkUniqueCorrelatedTable()` which verifies the correlated 
outer-only table's key is unique & non-null via 
`DataTrait.isUniqueAndNotNull()`. This covers OLAP UNIQUE_KEYS metadata, 
explicit `UNIQUE` constraints, and rejects nullable keys. Queries against 
non-unique-keyed tables are left as original scalar subqueries.
   
   ---
   
   **Bug 2 — Inner filter split by PushDownFilterThroughProject causes rewrite 
to fail**
   
   ```sql
   -- inner subquery has both a correlated predicate and a non-correlated 
predicate
   SELECT d.did, f.id, f.k, f.v
   FROM fact f, dim_unique d
   WHERE f.k = d.k
     AND f.v < 10
     AND f.v * 2 > (
       SELECT SUM(f2.v)
       FROM fact f2
       WHERE f2.k = d.k    -- correlated, cannot push through project
         AND f2.v < 10     -- non-correlated, can push through project
     );
   ```
   
   The optimizer's `PushDownFilterThroughProject` splits the inner `WHERE f2.k 
= d.k AND f2.v < 10` into two separate `LogicalFilter` nodes:
   - `Filter(k = d.k)` stays above the project (references correlated slot 
`d.k`, not in project output)
   - `Filter(v < 10)` is pushed below the project (only references `v`, which 
is in the project output)
   
   The old `checkFilter()` required **exactly one** inner `LogicalFilter` and 
returned false when seeing two, causing the rule to miss this valid rewrite.
   
   **Fix:** `checkFilter()` now collects conjuncts from **all** inner 
`LogicalFilter` nodes rather than requiring exactly one.
   
   ---
   
   **Bug 3 — Matched inner-filter predicates incorrectly placed above the 
window**
   
   ```sql
   -- f.v < 10 appears both in the outer WHERE and the inner WHERE
   SELECT d.did, f.id, f.k, f.v
   FROM fact f, dim_unique d
   WHERE f.k = d.k
     AND f.v < 10
     AND f.v * 2 > (
       SELECT SUM(f2.v)
       FROM fact f2
       WHERE f2.k = d.k
         AND f2.v < 10
     );
   ```
   
   The predicate `f.v < 10` is **semantically part of the inner aggregate's 
filter** — the original scalar subquery only sums `f2.v` where `v < 10`. 
However, the old rewrite placed this conjunct **above** the window:
   
   ```
   -- Wrong: f.v < 10 above the window lets the window see ALL rows
   Filter(f.v < 10 AND f.v * 2 > SUM(v) OVER (...))
     Window(SUM(v) OVER (PARTITION BY d.k))   -- aggregates all rows, not just 
v<10
       CrossJoin(Scan(fact f), Scan(dim_unique d))
   ```
   
   This means the window function computes `SUM(v)` over **all** fact rows per 
key (e.g., `v=5+7+6+4+10+6+8`), instead of only rows where `v < 10` (e.g., 
`v=5+7+6+4+6+8`). The comparison `f.v * 2 > sum` then uses the wrong aggregate, 
potentially returning incorrect rows.
   
   **Fix:** `checkFilter()` now tracks which outer conjuncts were matched 
against inner subquery filter conjuncts. `rewrite()` places these matched 
conjuncts **below** the window, preserving the inner aggregate's filter 
semantic:
   
   ```
   -- Correct: f.v < 10 below the window restricts the window's input
   Filter(f.v * 2 > SUM(v) OVER (...))
     Window(SUM(v) OVER (PARTITION BY d.k))
       Filter(f.v < 10)     -- restricts rows seen by the window
         CrossJoin(Scan(fact f), Scan(dim_unique d))
   ```
   
   ---
   
   ### Changes
   
   - `checkUniqueCorrelatedTable()` — validates correlated key uniqueness via 
`DataTrait.isUniqueAndNotNull()`
   - Fix `checkFilter()` — collect conjuncts from ALL inner filters, track 
matched conjuncts
   - Enhance `rewrite()` — classify conjuncts: matched inner-filter → below 
window, shared-table → above, volatile → above, outer-only → below
   - Add `stripOuterFilters()` and `ensureProjectOutput()` helpers
   - 13 unit tests + 6 regression cases with result and plan-shape verification
   
   ### Release note
   
   Scalar correlated subquery to window function rewrite is now correct: 
non-unique keys are rejected, split inner filters are handled, and inner-filter 
predicates are placed below the window.
   
   ### Check List (For Author)
   
   - Test:
       - Unit Test: 13 tests, all passing
       - Regression Test: 6 cases, all passing with verified plan shapes
   - Behavior changed: Yes — previously the rule could produce incorrect 
results for non-unique keys and misplaced predicates
   - Does this need documentation: No
   ```
   
   ### Check List (For Author)
   
   - Test <!-- At least one of them must be included. -->
       - [ ] Regression test
       - [ ] Unit Test
       - [ ] Manual test (add detailed scripts or steps below)
       - [ ] No need to test or manual test. Explain why:
           - [ ] This is a refactor/code format and no logic has been changed.
           - [ ] Previous test can cover this change.
           - [ ] No code files have been changed.
           - [ ] Other reason <!-- Add your reason?  -->
   
   - Behavior changed:
       - [ ] No.
       - [ ] Yes. <!-- Explain the behavior change -->
   
   - Does this need documentation?
       - [ ] No.
       - [ ] Yes. <!-- Add document PR link here. eg: 
https://github.com/apache/doris-website/pull/1214 -->
   
   ### Check List (For Reviewer who merge this PR)
   
   - [ ] Confirm the release note
   - [ ] Confirm test cases
   - [ ] Confirm document
   - [ ] Add branch pick label <!-- Add branch pick label that this PR should 
merge into -->
   
   


-- 
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