From 712a53b1f3e548dda8ef62014c2c72395ef990fb Mon Sep 17 00:00:00 2001 From: minghong Date: Mon, 24 Aug 2026 20:02:25 +0800 Subject: [PATCH] branch-4.2 [fix](fd) Fold function projections over uniform constants for predicate pruning (#66906) ### What problem does this PR solve? Problem Summary: A project expression that computes a function over a uniform constant slot (e.g. `date_sub(dt, INTERVAL 1 DAY)` where `dt` is a uniform constant slot) did not propagate the resulting constant: `LogicalProject.computeUniform` only handled constant projects and bare slot aliases, so the projected slot had no uniform value and downstream constant propagation could not fold predicates over it. For a join predicate like `t1.dt = p.prev_dt` where `p.prev_dt` is such a projection, the predicate stayed unfolded, could not be pushed into the scan, and partition pruning failed. Fix: `LogicalProject`/`PhysicalProject`/`LogicalLoadProject.computeUniform` now fold a project expression whose input slots are all uniform constants: the constant values are substituted into the expression (new helper `ExpressionUtils.foldToConstantByUniformValues`) and the projected slot is registered as a uniform constant, so downstream constant propagation can fold predicates over it and push them into the scan for partition pruning. ### Release note None ### Check List (For Author) - Test: FE unit test ConstantProjectionFoldingTest (the uniform constant source is a filter predicate, independent of constant CTEs; asserts the outer scan prunes to the expected single partition). - Behavior changed: No - Does this need documentation: No --- .../plans/logical/LogicalLoadProject.java | 7 ++ .../trees/plans/logical/LogicalProject.java | 7 ++ .../trees/plans/physical/PhysicalProject.java | 7 ++ .../doris/nereids/util/ExpressionUtils.java | 25 +++++ .../ConstantProjectionFoldingTest.java | 101 ++++++++++++++++++ 5 files changed, 147 insertions(+) create mode 100644 fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/ConstantProjectionFoldingTest.java diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalLoadProject.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalLoadProject.java index 99d281f40c7638..18399fcd5f45a7 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalLoadProject.java +++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalLoadProject.java @@ -251,6 +251,13 @@ public void computeUniform(DataTrait.Builder builder) { } else if (childTrait.isUniform(slot)) { builder.addUniformSlot(proj.toSlot()); } + } else { + // e.g. project `days_sub(begin_time, 1)` over a uniform constant slot `begin_time`: + // substitute the constant values so the projected slot also becomes a uniform + // constant, then downstream constant propagation can fold predicates over it. + Optional constantExpr = ExpressionUtils.foldToConstantByUniformValues( + proj.child(0), child(0).getLogicalProperties().getTrait()); + constantExpr.ifPresent(expr -> builder.addUniformSlotAndLiteral(proj.toSlot(), expr)); } } } diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalProject.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalProject.java index 3d818754b04a30..cd773d3ef03f9e 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalProject.java +++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalProject.java @@ -282,6 +282,13 @@ public void computeUniform(DataTrait.Builder builder) { } else if (childTrait.isUniform(slot)) { builder.addUniformSlot(proj.toSlot()); } + } else { + // e.g. project `days_sub(begin_time, 1)` over a uniform constant slot `begin_time`: + // substitute the constant values so the projected slot also becomes a uniform + // constant, then downstream constant propagation can fold predicates over it. + Optional constantExpr = ExpressionUtils.foldToConstantByUniformValues( + proj.child(0), child(0).getLogicalProperties().getTrait()); + constantExpr.ifPresent(expr -> builder.addUniformSlotAndLiteral(proj.toSlot(), expr)); } } } diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/physical/PhysicalProject.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/physical/PhysicalProject.java index eb65a9c9f60600..3cfcfabab45a5a 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/physical/PhysicalProject.java +++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/physical/PhysicalProject.java @@ -323,6 +323,13 @@ public void computeUniform(DataTrait.Builder builder) { } else if (childTrait.isUniform(slot)) { builder.addUniformSlot(proj.toSlot()); } + } else { + // e.g. project `days_sub(begin_time, 1)` over a uniform constant slot `begin_time`: + // substitute the constant values so the projected slot also becomes a uniform + // constant, then downstream constant propagation can fold predicates over it. + Optional constantExpr = ExpressionUtils.foldToConstantByUniformValues( + proj.child(0), child(0).getLogicalProperties().getTrait()); + constantExpr.ifPresent(expr -> builder.addUniformSlotAndLiteral(proj.toSlot(), expr)); } } } diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/util/ExpressionUtils.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/util/ExpressionUtils.java index 6ab9695c1ca59a..d7f0fe81c58840 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/nereids/util/ExpressionUtils.java +++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/util/ExpressionUtils.java @@ -26,6 +26,7 @@ import org.apache.doris.nereids.analyzer.Scope; import org.apache.doris.nereids.analyzer.UnboundSlot; import org.apache.doris.nereids.exceptions.AnalysisException; +import org.apache.doris.nereids.properties.DataTrait; import org.apache.doris.nereids.properties.PhysicalProperties; import org.apache.doris.nereids.rules.analysis.ExpressionAnalyzer; import org.apache.doris.nereids.rules.expression.ExpressionRewriteContext; @@ -1413,6 +1414,30 @@ public static boolean unionConstExprsSatisfyConjuncts(LogicalUnion union, Set foldToConstantByUniformValues(Expression expr, DataTrait childTrait) { + Set inputSlots = expr.getInputSlots(); + if (inputSlots.isEmpty()) { + return Optional.empty(); + } + Map replaceMap = new HashMap<>(); + for (Slot slot : inputSlots) { + if (!childTrait.isUniformAndHasConstValue(slot)) { + return Optional.empty(); + } + replaceMap.put(slot, childTrait.getUniformValue(slot).get()); + } + Expression constantExpr = replace(expr, replaceMap); + return constantExpr.isConstant() ? Optional.of(constantExpr) : Optional.empty(); + } + /** check constant value the expression */ public static Optional checkConstantExpr(Expression expr, Optional context) { if (expr instanceof Literal) { diff --git a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/ConstantProjectionFoldingTest.java b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/ConstantProjectionFoldingTest.java new file mode 100644 index 00000000000000..758f76c951f1f0 --- /dev/null +++ b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/ConstantProjectionFoldingTest.java @@ -0,0 +1,101 @@ +// 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. + +package org.apache.doris.nereids.rules.rewrite; + +import org.apache.doris.catalog.OlapTable; +import org.apache.doris.common.FeConstants; +import org.apache.doris.nereids.trees.plans.Plan; +import org.apache.doris.nereids.trees.plans.logical.LogicalOlapScan; +import org.apache.doris.nereids.util.MemoPatternMatchSupported; +import org.apache.doris.nereids.util.PlanChecker; +import org.apache.doris.utframe.TestWithFeService; + +import com.google.common.collect.Sets; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Set; + +/** + * Test that a project expression whose input slots are all uniform constants (e.g. + * `date_sub(dt, INTERVAL 1 DAY)` where `dt` is a uniform constant slot) is folded and the + * projected slot is registered as a uniform constant, so downstream constant propagation can + * fold predicates over the projected slot and partition pruning on the referenced tables works. + * + *

The uniform constant source here is a filter predicate (`WHERE dt = '2026-07-28'`), which + * is independent of constant CTEs. + */ +class ConstantProjectionFoldingTest extends TestWithFeService implements MemoPatternMatchSupported { + + // the subquery projects `date_sub(dt, INTERVAL 1 DAY)`; `dt` is a uniform constant there + // (from `WHERE dt = '2026-07-28'`), so the projected slot must fold to a uniform constant + // and the outer join predicate `t1.dt = prev_dt` folds to `t1.dt = '2026-07-27'`, letting + // the outer scan prune to a single partition. + private static final String SQL = "SELECT * FROM cte_prune_t t1\n" + + "JOIN (\n" + + " SELECT dt, date_sub(dt, INTERVAL 1 DAY) AS prev_dt\n" + + " FROM cte_prune_t\n" + + " WHERE dt = '2026-07-28'\n" + + ") p ON t1.dt = p.prev_dt"; + + @Override + protected void runBeforeAll() throws Exception { + createDatabase("test"); + useDatabase("test"); + createTable("CREATE TABLE `test`.`cte_prune_t` (\n" + + " `dt` DATE NULL COMMENT \"\",\n" + + " `sn` VARCHAR(50) NULL COMMENT \"\",\n" + + " `v` DOUBLE NULL COMMENT \"\"\n" + + ") DUPLICATE KEY(`dt`, `sn`)\n" + + "PARTITION BY RANGE(`dt`)\n" + + "(PARTITION p20260101 VALUES [(\"2026-01-01\"), (\"2026-01-02\")),\n" + + " PARTITION p20260726 VALUES [(\"2026-07-26\"), (\"2026-07-27\")),\n" + + " PARTITION p20260727 VALUES [(\"2026-07-27\"), (\"2026-07-28\")),\n" + + " PARTITION p20260728 VALUES [(\"2026-07-28\"), (\"2026-07-29\")),\n" + + " PARTITION p20260729 VALUES [(\"2026-07-29\"), (\"2026-07-30\")),\n" + + " PARTITION p20260901 VALUES [(\"2026-09-01\"), (\"2026-09-02\")))\n" + + "DISTRIBUTED BY HASH(`sn`) BUCKETS 3\n" + + "PROPERTIES('replication_num' = '1');"); + FeConstants.runningUnitTest = true; + } + + @Test + void testUniformConstantFoldThroughFunctionProjection() { + PlanChecker planChecker = PlanChecker.from(connectContext) + .analyze(SQL) + .rewrite(); + Plan plan = planChecker.getCascadesContext().getRewritePlan(); + String planString = plan.treeString(); + + List scans = plan.collectToList(LogicalOlapScan.class::isInstance); + Assertions.assertEquals(2, scans.size(), + "both the outer table and the subquery should scan cte_prune_t, plan: " + planString); + Set selectedPartitions = Sets.newHashSet(); + for (LogicalOlapScan scan : scans) { + // the outer scan (t1) should prune to p20260727, the subquery scan to p20260728 + Assertions.assertEquals(1, scan.getSelectedPartitionIds().size(), + "scan on cte_prune_t should prune to exactly one partition, plan: " + planString); + selectedPartitions.add(((OlapTable) scan.getTable()) + .getPartition(scan.getSelectedPartitionIds().get(0)).getName()); + } + Assertions.assertEquals(Sets.newHashSet("p20260727", "p20260728"), selectedPartitions, + "outer scan should prune to p20260727 and subquery scan to p20260728, plan: " + + planString); + } +}