From d81254270236d08c3120f158161e32ce497b9730 Mon Sep 17 00:00:00 2001 From: morrySnow Date: Wed, 9 Sep 2026 18:04:42 +0800 Subject: [PATCH] [fix](window) Respect frames when simplifying window functions (#67706) ## Problem The optimizer simplifies window functions when the partition keys are unique because each partition contains at most one row. It replaces functions such as `SUM(value)` with `value` and a non-null `COUNT` with `1`. That replacement produces incorrect results when the window frame excludes the current row. For a one-row partition, a preceding-only or following-only frame is empty, so `SUM` must return `NULL` and `COUNT` must return `0`. ## Root cause The simplification rule used partition cardinality alone and did not check whether the normalized frame actually contains the partition's only row. ## How to reproduce Create a merge-on-write unique-key table with one row and run `SUM` and `COUNT(*)` over a window partitioned by the unique key with `ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING` (or the equivalent following-only frame). Before this change, the optimizer removed the window and returned the current value and `1`; the correct result is `NULL` and `0`. ## Fix Check the normalized frame boundaries before simplifying frame-dependent functions. `COUNT`, `SUM`, `MIN`, `MAX`, `AVG`, `FIRST_VALUE`, and `LAST_VALUE` are simplified only when the frame contains the current row. Ranking functions retain their existing simplification because their result does not depend on frame membership. ## Tests - Added a regression suite covering preceding-only and following-only frames that must retain `PhysicalWindow` and return `NULL`/`0`. - Added current-row and centered-frame cases that continue to eliminate `PhysicalWindow` and return the simplified values. - Ran the new regression suite in verification mode: 1 suite passed, 0 failed. - Built the FE successfully with all reactor modules passing and no checkstyle violations. --- .../rewrite/SimplifyWindowExpression.java | 14 +++- .../simplify_window_frame.out | 22 ++++++ .../simplify_window_frame.groovy | 78 +++++++++++++++++++ 3 files changed, 111 insertions(+), 3 deletions(-) create mode 100644 regression-test/data/nereids_rules_p0/simplify_window_expression/simplify_window_frame.out create mode 100644 regression-test/suites/nereids_rules_p0/simplify_window_expression/simplify_window_frame.groovy diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/SimplifyWindowExpression.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/SimplifyWindowExpression.java index 311fe57cf16d19..9e8e0e5f8cffb4 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/SimplifyWindowExpression.java +++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/SimplifyWindowExpression.java @@ -27,6 +27,7 @@ import org.apache.doris.nereids.trees.expressions.NamedExpression; import org.apache.doris.nereids.trees.expressions.Slot; import org.apache.doris.nereids.trees.expressions.WindowExpression; +import org.apache.doris.nereids.trees.expressions.WindowFrame; import org.apache.doris.nereids.trees.expressions.functions.BoundFunction; import org.apache.doris.nereids.trees.expressions.functions.agg.Count; import org.apache.doris.nereids.trees.expressions.literal.TinyIntLiteral; @@ -90,11 +91,12 @@ private Plan simplify(MatchingContext> ctx) { if (function instanceof BoundFunction) { BoundFunction boundFunction = (BoundFunction) function; String name = ((BoundFunction) function).getName(); - if ((name.equals(COUNT) && checkCount((Count) boundFunction)) - || REWRRITE_TO_CONST_WINDOW_FUNCTIONS.contains(name)) { + boolean frameContainsCurrentRow = windowFrameContainsCurrentRow(windowExpression); + if (REWRRITE_TO_CONST_WINDOW_FUNCTIONS.contains(name) + || (frameContainsCurrentRow && name.equals(COUNT) && checkCount((Count) boundFunction))) { projectionsBuilder.add(new Alias(alias.getExprId(), new Cast(new TinyIntLiteral((byte) 1), function.getDataType()), alias.getName())); - } else if (REWRRITE_TO_SLOT_WINDOW_FUNCTIONS.contains(name)) { + } else if (frameContainsCurrentRow && REWRRITE_TO_SLOT_WINDOW_FUNCTIONS.contains(name)) { projectionsBuilder.add(new Alias(alias.getExprId(), TypeCoercionUtils.castIfNotSameType(boundFunction.child(0), boundFunction.getDataType()), alias.getName())); @@ -127,6 +129,12 @@ private Plan simplify(MatchingContext> ctx) { } } + private boolean windowFrameContainsCurrentRow(WindowExpression windowExpression) { + WindowFrame windowFrame = windowExpression.getWindowFrame().get(); + return !windowFrame.getLeftBoundary().asFollowing() + && !windowFrame.getRightBoundary().asPreceding(); + } + private boolean checkCount(Count count) { return count.isCountStar() || count.child(0).notNullable(); } diff --git a/regression-test/data/nereids_rules_p0/simplify_window_expression/simplify_window_frame.out b/regression-test/data/nereids_rules_p0/simplify_window_expression/simplify_window_frame.out new file mode 100644 index 00000000000000..78e824fb4e4d69 --- /dev/null +++ b/regression-test/data/nereids_rules_p0/simplify_window_expression/simplify_window_frame.out @@ -0,0 +1,22 @@ +-- This file is automatically generated. You should know what you did if you want to edit this +-- !frames_exclude_current_row -- +1 \N 0 \N 0 + +-- !frames_exclude_current_row_shape -- +PhysicalResultSink +--PhysicalProject +----PhysicalWindow +------PhysicalWindow +--------PhysicalQuickSort[LOCAL_SORT] +----------PhysicalProject +------------filter((test_simplify_window_frame.__DORIS_DELETE_SIGN__ = 0)) +--------------PhysicalOlapScan[test_simplify_window_frame] + +-- !frames_contain_current_row -- +1 7 1 + +-- !frames_contain_current_row_shape -- +PhysicalResultSink +--PhysicalProject +----filter((test_simplify_window_frame.__DORIS_DELETE_SIGN__ = 0)) +------PhysicalOlapScan[test_simplify_window_frame] diff --git a/regression-test/suites/nereids_rules_p0/simplify_window_expression/simplify_window_frame.groovy b/regression-test/suites/nereids_rules_p0/simplify_window_expression/simplify_window_frame.groovy new file mode 100644 index 00000000000000..68e90026920097 --- /dev/null +++ b/regression-test/suites/nereids_rules_p0/simplify_window_expression/simplify_window_frame.groovy @@ -0,0 +1,78 @@ +// 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("simplify_window_frame") { + sql "DROP TABLE IF EXISTS test_simplify_window_frame" + + sql """ + CREATE TABLE test_simplify_window_frame ( + pk INT NOT NULL, + v INT NULL + ) + UNIQUE KEY(pk) + DISTRIBUTED BY HASH(pk) BUCKETS 1 + PROPERTIES ( + "replication_num" = "1", + "enable_unique_key_merge_on_write" = "true" + ) + """ + + sql "INSERT INTO test_simplify_window_frame VALUES (1, 7)" + sql "SYNC" + + qt_frames_exclude_current_row """ + SELECT pk, + SUM(v) OVER (PARTITION BY pk ORDER BY pk + ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) AS sum_prev, + COUNT(*) OVER (PARTITION BY pk ORDER BY pk + ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) AS count_prev, + SUM(v) OVER (PARTITION BY pk ORDER BY pk + ROWS BETWEEN 1 FOLLOWING AND 1 FOLLOWING) AS sum_next, + COUNT(*) OVER (PARTITION BY pk ORDER BY pk + ROWS BETWEEN 1 FOLLOWING AND 1 FOLLOWING) AS count_next + FROM test_simplify_window_frame + ORDER BY pk + """ + + qt_frames_exclude_current_row_shape """ + EXPLAIN SHAPE PLAN + SELECT SUM(v) OVER (PARTITION BY pk ORDER BY pk + ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) AS sum_prev, + COUNT(*) OVER (PARTITION BY pk ORDER BY pk + ROWS BETWEEN 1 FOLLOWING AND 1 FOLLOWING) AS count_next + FROM test_simplify_window_frame + """ + + qt_frames_contain_current_row """ + SELECT pk, + SUM(v) OVER (PARTITION BY pk ORDER BY pk + ROWS BETWEEN CURRENT ROW AND CURRENT ROW) AS sum_cur, + COUNT(*) OVER (PARTITION BY pk ORDER BY pk + ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) AS count_centered + FROM test_simplify_window_frame + ORDER BY pk + """ + + qt_frames_contain_current_row_shape """ + EXPLAIN SHAPE PLAN + SELECT SUM(v) OVER (PARTITION BY pk ORDER BY pk + ROWS BETWEEN CURRENT ROW AND CURRENT ROW) AS sum_cur, + COUNT(*) OVER (PARTITION BY pk ORDER BY pk + ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) AS count_centered + FROM test_simplify_window_frame + """ +}