This is an automated email from the ASF dual-hosted git repository.

yiguolei pushed a commit to branch branch-4.1
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/branch-4.1 by this push:
     new 1e9ef832537 branch-4.1: [fix](nereids) partition topn opt requires 
chosen window func partition key to be a subset of co-located ones (#65073)
1e9ef832537 is described below

commit 1e9ef8325373a1ba35b3224c2cbb0214ce17425a
Author: Calvin Kirs <[email protected]>
AuthorDate: Thu Jul 2 08:15:57 2026 +0800

    branch-4.1: [fix](nereids) partition topn opt requires chosen window func 
partition key to be a subset of co-located ones (#65073)
    
    cherry-pick apache/doris#64764
---
 .../nereids/trees/plans/logical/LogicalWindow.java |  17 ++-
 .../GeneratePartitionTopnFromWindowTest.java       | 107 ++++++++++++++
 .../partition_topn/check_partitionkey.out          |  19 +++
 .../partition_topn/check_partitionkey.groovy       | 160 +++++++++++++++++++++
 .../push_down_multi_filter_through_window.groovy   |  57 ++++----
 5 files changed, 329 insertions(+), 31 deletions(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalWindow.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalWindow.java
index 72067d73ef8..fc0217a1b3e 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalWindow.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalWindow.java
@@ -320,11 +320,26 @@ public class LogicalWindow<CHILD_TYPE extends Plan> 
extends LogicalUnary<CHILD_T
                 && chosenRowNumberPartitionLimit == Long.MAX_VALUE)) {
             return null;
         } else {
-            // 4. check all windowExpression's order key is empty or is the 
same as chosenWindowFunc's order key
+            // 4. check all windowExpression's partition key and order key are 
compatible with chosenWindowFunc.
+            // The generated partitionTopN is pushed below the whole window 
node, so it prunes the input rows
+            // (keeping per-partition top-k of the chosen function) of every 
co-located window function.
+            // For another window function W to stay correct, every row that 
could affect W's value for a
+            // surviving row must also survive the pruning. This holds iff the 
chosen partition key is a
+            // SUBSET of W's partition key (chosen is coarser): then any row 
in W's partition with a smaller
+            // order value is also in the same chosen-partition with an order 
value <= the surviving row's,
+            // so its chosen-rank is within top-k and it is kept. The order 
key must also be compatible
+            // (empty or identical). Otherwise we must disable the 
optimization, e.g.
+            //   'row_number() over (partition by a order by c)' as rn 
together with
+            //   'row_number() over (partition by b order by c)' as rk, filter 
on rn
+            // (independent partitions), or a chosen partition (a, b) finer 
than a co-located 'partition by a'
+            // would prune rows the latter still needs and produce a wrong 
result.
             for (NamedExpression windowExpr : windowExpressions) {
                 if (windowExpr != null && windowExpr instanceof Alias
                         && windowExpr.child(0) instanceof WindowExpression) {
                     WindowExpression windowFunc = (WindowExpression) 
windowExpr.child(0);
+                    if 
(!windowFunc.getPartitionKeys().containsAll(chosenWindowFunc.getPartitionKeys()))
 {
+                        return null;
+                    }
                     if (windowFunc.getOrderKeys().isEmpty()
                             || 
windowFunc.getOrderKeys().equals(chosenWindowFunc.getOrderKeys())) {
                         continue;
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/GeneratePartitionTopnFromWindowTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/GeneratePartitionTopnFromWindowTest.java
index 00d47bb8b78..9c3993d4d2f 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/GeneratePartitionTopnFromWindowTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/GeneratePartitionTopnFromWindowTest.java
@@ -157,4 +157,111 @@ public class GeneratePartitionTopnFromWindowTest 
implements MemoPatternMatchSupp
                                                 logicalOlapScan()
                                         ))));
     }
+
+    @Test
+    public void testMultipleWindowsWithDifferentPartitions() {
+        ConnectContext context = MemoTestUtils.createConnectContext();
+        context.getSessionVariable().setEnablePartitionTopN(true);
+        NamedExpression gender = scan.getOutput().get(1).toSlot();
+        NamedExpression name = scan.getOutput().get(2).toSlot();
+        NamedExpression age = scan.getOutput().get(3).toSlot();
+
+        List<OrderExpression> orderKeyList = ImmutableList.of(new 
OrderExpression(
+                new OrderKey(age, true, true)));
+        WindowFrame windowFrame = new 
WindowFrame(WindowFrame.FrameUnitsType.ROWS,
+                WindowFrame.FrameBoundary.newPrecedingBoundary(),
+                WindowFrame.FrameBoundary.newCurrentRowBoundary());
+
+        // window1: partition by gender, order by age
+        WindowExpression window1 = new WindowExpression(new RowNumber(),
+                ImmutableList.of(gender), orderKeyList, windowFrame);
+        Alias windowAlias1 = new Alias(window1, window1.toSql());
+
+        // window2: same order key (age) but different partition key (name)
+        WindowExpression window2 = new WindowExpression(new RowNumber(),
+                ImmutableList.of(name), orderKeyList, windowFrame);
+        Alias windowAlias2 = new Alias(window2, window2.toSql());
+
+        List<NamedExpression> expressions = Lists.newArrayList(windowAlias1, 
windowAlias2);
+        LogicalWindow<LogicalOlapScan> window = new 
LogicalWindow<>(expressions, scan);
+        // filter on window1 (row_number partitioned by gender)
+        Expression filterPredicate = new 
LessThanEqual(window.getOutput().get(4).toSlot(), Literal.of(1));
+
+        LogicalPlan plan = new LogicalPlanBuilder(window)
+                .filter(filterPredicate)
+                .project(ImmutableList.of(0))
+                .build();
+
+        // The optimization must be disabled: pushing a partitionTopN 
partitioned by gender below the whole
+        // window would prune the input rows of window2 (partitioned by name) 
and corrupt its result.
+        PlanChecker.from(context, plan)
+                .applyTopDown(new CreatePartitionTopNFromWindow())
+                .matches(
+                        logicalProject(
+                                logicalFilter(
+                                        logicalWindow(
+                                                logicalOlapScan()
+                                        ))));
+    }
+
+    @Test
+    public void testMultipleWindowsSubsetPartitionGeneratesTopn() {
+        ConnectContext context = MemoTestUtils.createConnectContext();
+        context.getSessionVariable().setEnablePartitionTopN(true);
+        NamedExpression gender = scan.getOutput().get(1).toSlot();
+        NamedExpression name = scan.getOutput().get(2).toSlot();
+        NamedExpression age = scan.getOutput().get(3).toSlot();
+
+        List<Expression> partitionKeyList = ImmutableList.of(gender);
+        List<OrderExpression> orderKeyList = ImmutableList.of(new 
OrderExpression(
+                new OrderKey(age, true, true)));
+        WindowFrame windowFrame = new 
WindowFrame(WindowFrame.FrameUnitsType.ROWS,
+                WindowFrame.FrameBoundary.newPrecedingBoundary(),
+                WindowFrame.FrameBoundary.newCurrentRowBoundary());
+
+        // window1 (chosen via filter): partition by gender, the coarser key
+        WindowExpression window1 = new WindowExpression(new RowNumber(),
+                partitionKeyList, orderKeyList, windowFrame);
+        Alias windowAlias1 = new Alias(window1, window1.toSql());
+
+        // window2: partition by (gender, name) -- a SUPERSET of window1's 
partition key
+        WindowExpression window2 = new WindowExpression(new RowNumber(),
+                ImmutableList.of(gender, name), orderKeyList, windowFrame);
+        Alias windowAlias2 = new Alias(window2, window2.toSql());
+
+        List<NamedExpression> expressions = Lists.newArrayList(windowAlias1, 
windowAlias2);
+        LogicalWindow<LogicalOlapScan> window = new 
LogicalWindow<>(expressions, scan);
+        // filter on window1 (the coarser, chosen window)
+        Expression filterPredicate = new 
LessThanEqual(window.getOutput().get(4).toSlot(), Literal.of(100));
+
+        LogicalPlan plan = new LogicalPlanBuilder(window)
+                .filter(filterPredicate)
+                .project(ImmutableList.of(0))
+                .build();
+
+        // The chosen partition key (gender) is a subset of the other window's
+        // (gender, name), so pruning per gender cannot corrupt window2 -> the
+        // partition topn is still generated.
+        PlanChecker.from(context, plan)
+                .applyTopDown(new CreatePartitionTopNFromWindow())
+                .matches(
+                        logicalProject(
+                                logicalFilter(
+                                        logicalWindow(
+                                                logicalPartitionTopN(
+                                                        logicalOlapScan()
+                                                ).when(logicalPartitionTopN -> 
{
+                                                    WindowFuncType funName = 
logicalPartitionTopN.getFunction();
+                                                    List<Expression> 
partitionKeys = logicalPartitionTopN.getPartitionKeys();
+                                                    boolean hasGlobalLimit = 
logicalPartitionTopN.hasGlobalLimit();
+                                                    long partitionLimit = 
logicalPartitionTopN.getPartitionLimit();
+                                                    return funName == 
WindowFuncType.ROW_NUMBER
+                                                        && 
partitionKeys.equals(partitionKeyList)
+                                                        && !hasGlobalLimit && 
partitionLimit == 100;
+                                                })
+                                        )
+                                ).when(filter -> 
filter.getConjuncts().equals(ImmutableSet.of(filterPredicate)))
+                        )
+                );
+    }
 }
diff --git 
a/regression-test/data/nereids_p0/partition_topn/check_partitionkey.out 
b/regression-test/data/nereids_p0/partition_topn/check_partitionkey.out
new file mode 100644
index 00000000000..c33a0912cc4
--- /dev/null
+++ b/regression-test/data/nereids_p0/partition_topn/check_partitionkey.out
@@ -0,0 +1,19 @@
+-- This file is automatically generated. You should know what you did if you 
want to edit this
+-- !multi_window --
+1      A       X       1       1       1
+4      B       X       4       1       3
+7      C       X       7       1       4
+
+-- !subset_safe_2window --
+1      A       X       1       1       1
+2      A       X       2       2       2
+4      B       X       4       1       1
+5      B       Y       5       2       1
+7      C       X       7       1       1
+8      C       Z       8       2       1
+
+-- !subset_safe_3window --
+1      A       X       1       1       1       1
+4      B       X       4       1       1       1
+7      C       X       7       1       1       1
+
diff --git 
a/regression-test/suites/nereids_p0/partition_topn/check_partitionkey.groovy 
b/regression-test/suites/nereids_p0/partition_topn/check_partitionkey.groovy
new file mode 100644
index 00000000000..14457badd2c
--- /dev/null
+++ b/regression-test/suites/nereids_p0/partition_topn/check_partitionkey.groovy
@@ -0,0 +1,160 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+suite("check_partitionkey") {
+    sql "drop table if exists multi_window_cases"
+    sql """
+    create table multi_window_cases (
+        id int,
+        g1 varchar(8),
+        g2 varchar(8),
+        ord_key int,
+        amt int
+    ) properties ('replication_num'='1');
+
+    insert into multi_window_cases values
+    (1,'A','X',1,10),
+    (2,'A','X',2,20),
+    (3,'A','Y',3,30),
+    (4,'B','X',4,40),
+    (5,'B','Y',5,50),
+    (6,'B','Y',6,60),
+    (7,'C','X',7,70),
+    (8,'C','Z',8,80);
+    """
+
+    // Two row_number() with the SAME order key (ord_key) but DIFFERENT 
partition keys
+    // (g1 vs g2). The filter is on rn1 (partition by g1). The partition topn 
generated
+    // from rn1 would be pushed below the whole window node and prune the 
input rows of
+    // rn2 (partition by g2), corrupting rn2. So partition topn must NOT be 
applied here.
+    explain {
+        sql """
+            select id, g1, g2, ord_key, rn1, rn2
+            from (
+                select
+                    id, g1, g2, ord_key,
+                    row_number() over (partition by g1 order by ord_key) as 
rn1,
+                    row_number() over (partition by g2 order by ord_key) as rn2
+                from multi_window_cases
+            ) q
+            where rn1 <= 1;
+            """
+        notContains("VPartitionTopN")
+    }
+
+    qt_multi_window """
+        select id, g1, g2, ord_key, rn1, rn2
+        from (
+            select
+                id, g1, g2, ord_key,
+                row_number() over (partition by g1 order by ord_key) as rn1,
+                row_number() over (partition by g2 order by ord_key) as rn2
+            from multi_window_cases
+        ) q
+        where rn1 <= 1
+        order by id;
+        """
+
+    // ---------------------------------------------------------------------
+    // The optimization is STILL valid when the chosen window's partition key
+    // is a SUBSET of every other co-located window's partition key (i.e. the
+    // chosen one is coarser). Pruning per chosen-partition top-k then only
+    // removes rows the finer windows do not need, so partition topn MUST still
+    // fire and the results must stay correct.
+
+    // 2 windows: the chosen rank(partition by g1) is coarser than the
+    // row_number(partition by g1, g2). The filter is on the coarser window, so
+    // the partition topn (partition by g1) is safe to push down -> it fires.
+    explain {
+        sql """
+            select id, g1, g2, ord_key, rk, rn
+            from (
+                select
+                    id, g1, g2, ord_key,
+                    rank() over (partition by g1 order by ord_key) as rk,
+                    row_number() over (partition by g1, g2 order by ord_key) 
as rn
+                from multi_window_cases
+            ) q
+            where rk <= 2;
+            """
+        contains("VPartitionTopN")
+    }
+
+    qt_subset_safe_2window """
+        select id, g1, g2, ord_key, rk, rn
+        from (
+            select
+                id, g1, g2, ord_key,
+                rank() over (partition by g1 order by ord_key) as rk,
+                row_number() over (partition by g1, g2 order by ord_key) as rn
+            from multi_window_cases
+        ) q
+        where rk <= 2
+        order by id;
+        """
+
+    // 3 windows: the chosen rank(partition by g1) is a subset of both the
+    // (g1, g2) windows, so pruning by g1 cannot corrupt them -> it still 
fires.
+    explain {
+        sql """
+            select id, g1, g2, ord_key, rk, rn, rk2
+            from (
+                select
+                    id, g1, g2, ord_key,
+                    rank() over (partition by g1 order by ord_key) as rk,
+                    row_number() over (partition by g1, g2 order by ord_key) 
as rn,
+                    rank() over (partition by g1, g2 order by ord_key) as rk2
+                from multi_window_cases
+            ) q
+            where rk <= 1;
+            """
+        contains("VPartitionTopN")
+    }
+
+    qt_subset_safe_3window """
+        select id, g1, g2, ord_key, rk, rn, rk2
+        from (
+            select
+                id, g1, g2, ord_key,
+                rank() over (partition by g1 order by ord_key) as rk,
+                row_number() over (partition by g1, g2 order by ord_key) as rn,
+                rank() over (partition by g1, g2 order by ord_key) as rk2
+            from multi_window_cases
+        ) q
+        where rk <= 1
+        order by id;
+        """
+
+    // 3 windows but one is partitioned by g2, which is NOT a superset of the
+    // chosen g1. Pruning by g1 would corrupt the g2 window, so the 
optimization
+    // must be disabled.
+    explain {
+        sql """
+            select id, g1, g2, ord_key, rk, rn, rk2
+            from (
+                select
+                    id, g1, g2, ord_key,
+                    rank() over (partition by g1 order by ord_key) as rk,
+                    row_number() over (partition by g1, g2 order by ord_key) 
as rn,
+                    rank() over (partition by g2 order by ord_key) as rk2
+                from multi_window_cases
+            ) q
+            where rk <= 1;
+            """
+        notContains("VPartitionTopN")
+    }
+}
diff --git 
a/regression-test/suites/nereids_rules_p0/push_down_filter_through_window/push_down_multi_filter_through_window.groovy
 
b/regression-test/suites/nereids_rules_p0/push_down_filter_through_window/push_down_multi_filter_through_window.groovy
index 015a6e7fae7..607391bab0b 100644
--- 
a/regression-test/suites/nereids_rules_p0/push_down_filter_through_window/push_down_multi_filter_through_window.groovy
+++ 
b/regression-test/suites/nereids_rules_p0/push_down_filter_through_window/push_down_multi_filter_through_window.groovy
@@ -48,18 +48,19 @@ suite("push_down_multi_filter_through_window") {
         notContains "VPartitionTopN"
     }
 
+    // The chosen window row_number(partition by c1, c2) is NOT a subset of the
+    // co-located rank(partition by c1), so pushing its partition topn below 
the
+    // whole window would prune rows that rk still needs -> optimization 
disabled.
     explain {
         sql ("select * from (select row_number() over(partition by c1, c2 
order by c3) as rn, rank() over(partition by c1 order by c3) as rk from 
push_down_multi_predicate_through_window_t) t where rn <= 1 and rk <= 1;")
-        contains "VPartitionTopN"
-        contains "functions: row_number"
-        contains "partition limit: 1"
+        notContains "VPartitionTopN"
     }
 
+    // row_number(partition by c1, c2) preferred as chosen, but it is not a 
subset
+    // of rank(partition by c1) -> disabled.
     explain {
         sql ("select * from (select rank() over(partition by c1 order by c3) 
as rk, row_number() over(partition by c1, c2 order by c3) as rn from 
push_down_multi_predicate_through_window_t) t where rn <= 10 and rk <= 1;")
-        contains "VPartitionTopN"
-        contains "functions: row_number"
-        contains "partition limit: 10"
+        notContains "VPartitionTopN"
     }
 
     explain {
@@ -69,18 +70,16 @@ suite("push_down_multi_filter_through_window") {
         contains "partition limit: 1"
     }
 
+    // chosen row_number(partition by c1, c2) is not a subset of 
rank(partition by c1) -> disabled.
     explain {
         sql ("select * from (select rank() over(partition by c1 order by c3) 
as rk, row_number() over(partition by c1, c2 order by c3) as rn from 
push_down_multi_predicate_through_window_t) t where rn <= 10;")
-        contains "VPartitionTopN"
-        contains "functions: row_number"
-        contains "partition limit: 10"
+        notContains "VPartitionTopN"
     }
 
+    // chosen rank(partition by c1, c2) (min limit) is not a subset of 
rank(partition by c1) -> disabled.
     explain {
         sql ("select * from (select rank() over(partition by c1 order by c3) 
as rk, rank() over(partition by c1, c2 order by c3) as rn from 
push_down_multi_predicate_through_window_t) t where rn <= 1 and rk <= 10;")
-        contains "VPartitionTopN"
-        contains "functions: rank"
-        contains "partition limit: 1"
+        notContains "VPartitionTopN"
     }
 
     explain {
@@ -97,18 +96,16 @@ suite("push_down_multi_filter_through_window") {
         contains "partition limit: 1"
     }
 
+    // chosen rank(partition by c1, c2) is not a subset of rank(partition by 
c1) -> disabled.
     explain {
         sql ("select * from (select rank() over(partition by c1 order by c3) 
as rk, rank() over(partition by c1, c2 order by c3) as rn from 
push_down_multi_predicate_through_window_t) t where rn <= 1 and rk > 1;")
-        contains "VPartitionTopN"
-        contains "functions: rank"
-        contains "partition limit: 1"
+        notContains "VPartitionTopN"
     }
 
+    // chosen row_number(partition by c1, c2) is not a subset of 
rank(partition by c1) -> disabled.
     explain {
         sql ("select * from (select row_number() over(partition by c1, c2 
order by c3) as rn, rank() over(partition by c1 order by c3) as rk from 
push_down_multi_predicate_through_window_t) t limit 10;")
-        contains "VPartitionTopN"
-        contains "functions: row_number"
-        contains "partition limit: 10"
+        notContains "VPartitionTopN"
     }
 
     explain {
@@ -125,32 +122,32 @@ suite("push_down_multi_filter_through_window") {
         contains "partition limit: 1"
     }
 
+    // chosen row_number(partition by c1, c2) (min limit) is not a subset of
+    // row_number(partition by c1) -> disabled.
     explain {
         sql ("select * from (select row_number() over(partition by c1, c2 
order by c3) as rn, row_number() over(partition by c1 order by c3) as rk from 
push_down_multi_predicate_through_window_t) t where rn <= 1 and rk <= 10;")
-        contains "VPartitionTopN"
-        contains "functions: row_number"
-        contains "partition limit: 1"
+        notContains "VPartitionTopN"
     }
 
+    // 3 windows: chosen row_number(partition by c1, c2) is not a subset of
+    // rank(partition by c1) nor rank(partition by c2) -> disabled.
     explain {
         sql ("select * from (select row_number() over(partition by c1, c2 
order by c3) as rn, rank() over(partition by c1 order by c3) as rk1, rank() 
over(partition by c2 order by c3) as rk2 from 
push_down_multi_predicate_through_window_t) t where rn <= 1 and rk1 <= 10 and 
rk2 <= 100;")
-        contains "VPartitionTopN"
-        contains "functions: row_number"
-        contains "partition limit: 1"
+        notContains "VPartitionTopN"
     }
 
+    // 3 windows: chosen row_number(partition by c2) (min limit) is not a 
subset
+    // of row_number(partition by c1) -> disabled.
     explain {
         sql ("select * from (select row_number() over(partition by c1 order by 
c3) as rn1, row_number() over(partition by c2 order by c3) as rn2, rank() 
over(partition by c1, c2 order by c3) as rk from 
push_down_multi_predicate_through_window_t) t where rn1 <= 10 and rn2 <= 1 and 
rk <= 100;")
-        contains "VPartitionTopN"
-        contains "functions: row_number"
-        contains "partition limit: 1"
+        notContains "VPartitionTopN"
     }
 
+    // 3 windows: chosen row_number(partition by c1) (min limit) is not a 
subset
+    // of row_number(partition by c2) -> disabled.
     explain {
         sql ("select * from (select rank() over(partition by c1, c2 order by 
c3) as rk, row_number() over(partition by c1 order by c3) as rn1, row_number() 
over(partition by c2 order by c3) as rn2 from 
push_down_multi_predicate_through_window_t) t where rn1 <= 1 and rn2 <= 10 and 
rk <= 100;")
-        contains "VPartitionTopN"
-        contains "functions: row_number"
-        contains "partition limit: 1"
+        notContains "VPartitionTopN"
     }
 
     explain {


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

Reply via email to