diff --git a/fe/fe-core/src/main/java/org/apache/doris/planner/PlanNode.java b/fe/fe-core/src/main/java/org/apache/doris/planner/PlanNode.java index 75e1dfb264c..4a9c0168e93 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/planner/PlanNode.java +++ b/fe/fe-core/src/main/java/org/apache/doris/planner/PlanNode.java @@ -22,6 +22,7 @@ package org.apache.doris.planner; import org.apache.doris.analysis.CompoundPredicate; import org.apache.doris.analysis.Expr; +import org.apache.doris.analysis.ExprSubstitutionMap; import org.apache.doris.analysis.ExprToSqlVisitor; import org.apache.doris.analysis.ExprToThriftVisitor; import org.apache.doris.analysis.SlotDescriptor; @@ -743,6 +744,21 @@ public abstract class PlanNode extends TreeNode { return projectList; } + public List getPointQueryProjectList() { + if (CollectionUtils.isEmpty(projectList) || intermediateProjectListList.isEmpty()) { + return projectList; + } + + List flattenedProjectList = Expr.cloneList(projectList); + for (int i = intermediateProjectListList.size() - 1; i >= 0; --i) { + flattenedProjectList = Expr.cloneList( + flattenedProjectList, + createPointQueryProjectionSmap(intermediateOutputTupleDescList.get(i), + intermediateProjectListList.get(i))); + } + return flattenedProjectList; + } + public void setCardinalityAfterFilter(long cardinalityAfterFilter) { this.cardinalityAfterFilter = cardinalityAfterFilter; } @@ -773,6 +789,20 @@ public abstract class PlanNode extends TreeNode { intermediateProjectListList.add(exprs); } + private ExprSubstitutionMap createPointQueryProjectionSmap( + TupleDescriptor outputTupleDesc, List projectionExprs) { + List outputSlots = outputTupleDesc.getSlots(); + Preconditions.checkState(outputSlots.size() == projectionExprs.size(), + "point query projection slot size %s does not match expr size %s", + outputSlots.size(), projectionExprs.size()); + + ExprSubstitutionMap substitutionMap = new ExprSubstitutionMap(); + for (int i = 0; i < outputSlots.size(); ++i) { + substitutionMap.put(new SlotRef(outputSlots.get(i)), projectionExprs.get(i)); + } + return substitutionMap; + } + public List collectInCurrentFragment(Predicate predicate) { List result = Lists.newArrayList(); foreachDownInCurrentFragment(child -> { diff --git a/fe/fe-core/src/main/java/org/apache/doris/qe/ShortCircuitQueryContext.java b/fe/fe-core/src/main/java/org/apache/doris/qe/ShortCircuitQueryContext.java index e0296702815..c55ae01ef92 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/qe/ShortCircuitQueryContext.java +++ b/fe/fe-core/src/main/java/org/apache/doris/qe/ShortCircuitQueryContext.java @@ -17,6 +17,7 @@ package org.apache.doris.qe; +import org.apache.doris.analysis.Expr; import org.apache.doris.analysis.ExprToThriftVisitor; import org.apache.doris.analysis.Queriable; import org.apache.doris.catalog.OlapTable; @@ -88,9 +89,10 @@ public class ShortCircuitQueryContext { new TSerializer().serialize(options)); List exprs = new ArrayList<>(); OlapScanNode olapScanNode = (OlapScanNode) planner.getScanNodes().get(0); - if (olapScanNode.getProjectList() != null) { + List pointQueryProjectList = olapScanNode.getPointQueryProjectList(); + if (pointQueryProjectList != null) { // project on scan node - exprs.addAll(olapScanNode.getProjectList().stream() + exprs.addAll(pointQueryProjectList.stream() .map(ExprToThriftVisitor::treeToThrift).collect(Collectors.toList())); } else { // add output slots diff --git a/fe/fe-core/src/test/java/org/apache/doris/planner/PlanNodeTest.java b/fe/fe-core/src/test/java/org/apache/doris/planner/PlanNodeTest.java new file mode 100644 index 00000000000..a065a186e7e --- /dev/null +++ b/fe/fe-core/src/test/java/org/apache/doris/planner/PlanNodeTest.java @@ -0,0 +1,102 @@ +// 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.planner; + +import org.apache.doris.analysis.ArithmeticExpr; +import org.apache.doris.analysis.Expr; +import org.apache.doris.analysis.IntLiteral; +import org.apache.doris.analysis.SlotDescriptor; +import org.apache.doris.analysis.SlotId; +import org.apache.doris.analysis.SlotRef; +import org.apache.doris.analysis.TupleDescriptor; +import org.apache.doris.analysis.TupleId; +import org.apache.doris.catalog.Column; +import org.apache.doris.catalog.Function.NullableMode; +import org.apache.doris.catalog.Type; +import org.apache.doris.thrift.TPlanNode; + +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class PlanNodeTest { + @Test + public void testGetPointQueryProjectListFlattensIntermediateProjection() { + TestPlanNode planNode = new TestPlanNode(); + + TupleDescriptor scanTuple = new TupleDescriptor(new TupleId(0)); + SlotDescriptor scanSlotA = createColumnSlot(scanTuple, 0, "a"); + SlotDescriptor scanSlotB = createColumnSlot(scanTuple, 1, "b"); + + TupleDescriptor intermediateTuple = new TupleDescriptor(new TupleId(1)); + SlotDescriptor projectedSlotA = createColumnSlot(intermediateTuple, 2, "a"); + SlotDescriptor projectedExprSlot = createExprSlot(intermediateTuple, 3, "expr_col"); + + planNode.addIntermediateOutputTupleDescList(intermediateTuple); + planNode.addIntermediateProjectList(Lists.newArrayList( + new SlotRef(scanSlotA), + new ArithmeticExpr(ArithmeticExpr.Operator.ADD, new SlotRef(scanSlotB), + new IntLiteral(1), Type.BIGINT, NullableMode.DEPEND_ON_ARGUMENT, true))); + planNode.setProjectList(Lists.newArrayList( + new ArithmeticExpr(ArithmeticExpr.Operator.ADD, new SlotRef(projectedSlotA), + new SlotRef(projectedExprSlot), Type.BIGINT, + NullableMode.DEPEND_ON_ARGUMENT, true))); + + List pointQueryProjectList = planNode.getPointQueryProjectList(); + Assertions.assertEquals(1, pointQueryProjectList.size()); + + Set slotIds = new HashSet<>(); + Expr.extractSlots(pointQueryProjectList.get(0), slotIds); + Assertions.assertEquals(Sets.newHashSet(scanSlotA.getId(), scanSlotB.getId()), slotIds); + Assertions.assertFalse(slotIds.contains(projectedSlotA.getId())); + Assertions.assertFalse(slotIds.contains(projectedExprSlot.getId())); + } + + private static SlotDescriptor createColumnSlot(TupleDescriptor tupleDescriptor, int slotId, + String columnName) { + SlotDescriptor slotDescriptor = new SlotDescriptor(new SlotId(slotId), tupleDescriptor); + slotDescriptor.setColumn(new Column(columnName, Type.BIGINT)); + slotDescriptor.setLabel(columnName); + tupleDescriptor.addSlot(slotDescriptor); + return slotDescriptor; + } + + private static SlotDescriptor createExprSlot(TupleDescriptor tupleDescriptor, int slotId, + String label) { + SlotDescriptor slotDescriptor = new SlotDescriptor(new SlotId(slotId), tupleDescriptor); + slotDescriptor.setType(Type.BIGINT); + slotDescriptor.setLabel(label); + tupleDescriptor.addSlot(slotDescriptor); + return slotDescriptor; + } + + private static class TestPlanNode extends PlanNode { + TestPlanNode() { + super(new PlanNodeId(0), "TEST"); + } + + @Override + protected void toThrift(TPlanNode msg) { + } + } +} diff --git a/regression-test/data/point_query_p0/test_short_circuit_project.out b/regression-test/data/point_query_p0/test_short_circuit_project.out new file mode 100644 index 00000000000..9a6beaa74f4 --- /dev/null +++ b/regression-test/data/point_query_p0/test_short_circuit_project.out @@ -0,0 +1,37 @@ +-- This file is automatically generated. You should know what you did if you want to edit this +-- !sql -- +7 9.5 + +-- !arith -- +410 4 11.57 + +-- !cast -- +10 hello 3 10.5 + +-- !case_when -- +big 6.28 + +-- !string_fn -- +HELLO WORLD! 11 hello + +-- !math_fn -- +10 4 3 3.1 100.0 + +-- !null_fn -- +10 \N hello world 3.14 + +-- !if_cmp -- +200 200 1.0 + +-- !nested -- +210-pos 313.97 + +-- !simple -- +10 hello world + +-- !null_row -- +0 empty null_int -1 + +-- !neg_arith -- +5 25 -5.0 + diff --git a/regression-test/suites/point_query_p0/test_short_circuit_project.groovy b/regression-test/suites/point_query_p0/test_short_circuit_project.groovy new file mode 100644 index 00000000000..b5150ad537b --- /dev/null +++ b/regression-test/suites/point_query_p0/test_short_circuit_project.groovy @@ -0,0 +1,322 @@ +// 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("test_short_circuit_project", "p0") { + // ---------- common table setup ---------- + def tableName = "cir_19608_short_circuit_project" + sql "DROP TABLE IF EXISTS ${tableName}" + sql """ + CREATE TABLE ${tableName} ( + `uuid` BIGINT NULL, + `interstitial_ad_watch_cnt_lt` BIGINT NULL, + `splash_ad_watch_cnt_lt` BIGINT NULL, + `splash_ad_revenue_lt` DOUBLE NULL + ) ENGINE=OLAP + UNIQUE KEY(`uuid`) + DISTRIBUTED BY HASH(`uuid`) BUCKETS 1 + PROPERTIES ( + "replication_allocation" = "tag.location.default: 1", + "store_row_column" = "true", + "enable_unique_key_merge_on_write" = "true", + "light_schema_change" = "true", + "storage_format" = "V2" + ) + """ + + sql """ + INSERT INTO ${tableName} VALUES + (10001, 3, 4, 5.5) + """ + + // ---------- original test: IFNULL + ADD ---------- + explain { + sql """ + SELECT /*+ SET_VAR(enable_nereids_planner=true) */ + IFNULL(interstitial_ad_watch_cnt_lt, 0) + IFNULL(splash_ad_watch_cnt_lt, 0), + IFNULL(splash_ad_revenue_lt, 0) + IFNULL(splash_ad_watch_cnt_lt, 0) + FROM ${tableName} + WHERE uuid = 10001 + """ + contains "SHORT-CIRCUIT" + } + + qt_sql """ + SELECT /*+ SET_VAR(enable_nereids_planner=true) */ + IFNULL(interstitial_ad_watch_cnt_lt, 0) + IFNULL(splash_ad_watch_cnt_lt, 0), + IFNULL(splash_ad_revenue_lt, 0) + IFNULL(splash_ad_watch_cnt_lt, 0) + FROM ${tableName} + WHERE uuid = 10001 + """ + + // ---------- extended table with richer types ---------- + def extTable = "short_circuit_project_ext" + sql "DROP TABLE IF EXISTS ${extTable}" + sql """ + CREATE TABLE ${extTable} ( + `id` BIGINT NOT NULL, + `col_int` INT NULL, + `col_bigint` BIGINT NULL, + `col_double` DOUBLE NULL, + `col_decimal` DECIMALV3(18, 4) NULL, + `col_str` VARCHAR(200) NULL, + `col_date` DATE NULL, + `col_datetime` DATETIME NULL + ) ENGINE=OLAP + UNIQUE KEY(`id`) + DISTRIBUTED BY HASH(`id`) BUCKETS 1 + PROPERTIES ( + "replication_allocation" = "tag.location.default: 1", + "store_row_column" = "true", + "enable_unique_key_merge_on_write" = "true", + "light_schema_change" = "true", + "storage_format" = "V2" + ) + """ + + sql """ + INSERT INTO ${extTable} VALUES + (1, 10, 200, 3.14, 99.9900, 'hello world', '2026-03-10', '2026-03-10 12:30:00'), + (2, -5, 0, 0.0, 0.0000, '', '2000-01-01', '2000-01-01 00:00:00'), + (3, NULL, NULL, NULL, NULL, NULL, NULL, NULL) + """ + + // ---------- 1. Arithmetic operators: +, -, *, /, % ---------- + explain { + sql """ + SELECT /*+ SET_VAR(enable_nereids_planner=true) */ + (col_int + col_bigint) * 2 - col_int, + col_bigint % 7, + col_double / 2.0 + col_int + FROM ${extTable} WHERE id = 1 + """ + contains "SHORT-CIRCUIT" + } + + qt_arith """ + SELECT /*+ SET_VAR(enable_nereids_planner=true) */ + (col_int + col_bigint) * 2 - col_int, + col_bigint % 7, + col_double / 2.0 + col_int + FROM ${extTable} WHERE id = 1 + """ + + // ---------- 2. CAST / type coercion ---------- + explain { + sql """ + SELECT /*+ SET_VAR(enable_nereids_planner=true) */ + CAST(col_int AS VARCHAR(20)), + CAST(col_str AS VARCHAR(5)), + CAST(col_double AS BIGINT), + CAST(col_int AS DOUBLE) + 0.5 + FROM ${extTable} WHERE id = 1 + """ + contains "SHORT-CIRCUIT" + } + + qt_cast """ + SELECT /*+ SET_VAR(enable_nereids_planner=true) */ + CAST(col_int AS VARCHAR(20)), + CAST(col_str AS VARCHAR(5)), + CAST(col_double AS BIGINT), + CAST(col_int AS DOUBLE) + 0.5 + FROM ${extTable} WHERE id = 1 + """ + + // ---------- 3. CASE WHEN ---------- + explain { + sql """ + SELECT /*+ SET_VAR(enable_nereids_planner=true) */ + CASE WHEN col_int > 5 THEN 'big' WHEN col_int > 0 THEN 'small' ELSE 'zero_or_neg' END, + CASE WHEN col_double > 3.0 THEN col_double * 2 ELSE col_double END + FROM ${extTable} WHERE id = 1 + """ + contains "SHORT-CIRCUIT" + } + + qt_case_when """ + SELECT /*+ SET_VAR(enable_nereids_planner=true) */ + CASE WHEN col_int > 5 THEN 'big' WHEN col_int > 0 THEN 'small' ELSE 'zero_or_neg' END, + CASE WHEN col_double > 3.0 THEN col_double * 2 ELSE col_double END + FROM ${extTable} WHERE id = 1 + """ + + // ---------- 4. String functions ---------- + explain { + sql """ + SELECT /*+ SET_VAR(enable_nereids_planner=true) */ + CONCAT(UPPER(col_str), '!'), + LENGTH(col_str), + SUBSTR(col_str, 1, 5) + FROM ${extTable} WHERE id = 1 + """ + contains "SHORT-CIRCUIT" + } + + qt_string_fn """ + SELECT /*+ SET_VAR(enable_nereids_planner=true) */ + CONCAT(UPPER(col_str), '!'), + LENGTH(col_str), + SUBSTR(col_str, 1, 5) + FROM ${extTable} WHERE id = 1 + """ + + // ---------- 5. Math functions ---------- + explain { + sql """ + SELECT /*+ SET_VAR(enable_nereids_planner=true) */ + ABS(col_int - 20), + CEIL(col_double), + FLOOR(col_double), + ROUND(col_double, 1), + POWER(col_int, 2) + FROM ${extTable} WHERE id = 1 + """ + contains "SHORT-CIRCUIT" + } + + qt_math_fn """ + SELECT /*+ SET_VAR(enable_nereids_planner=true) */ + ABS(col_int - 20), + CEIL(col_double), + FLOOR(col_double), + ROUND(col_double, 1), + POWER(col_int, 2) + FROM ${extTable} WHERE id = 1 + """ + + // ---------- 6. NULL handling: COALESCE, NULLIF ---------- + explain { + sql """ + SELECT /*+ SET_VAR(enable_nereids_planner=true) */ + COALESCE(col_int, col_bigint, -1), + NULLIF(col_int, 10), + IFNULL(col_str, 'N/A'), + COALESCE(NULL, col_double, 0) + FROM ${extTable} WHERE id = 1 + """ + contains "SHORT-CIRCUIT" + } + + qt_null_fn """ + SELECT /*+ SET_VAR(enable_nereids_planner=true) */ + COALESCE(col_int, col_bigint, -1), + NULLIF(col_int, 10), + IFNULL(col_str, 'N/A'), + COALESCE(NULL, col_double, 0) + FROM ${extTable} WHERE id = 1 + """ + + // ---------- 7. IF / comparison: IF, GREATEST, LEAST ---------- + explain { + sql """ + SELECT /*+ SET_VAR(enable_nereids_planner=true) */ + IF(col_int > col_bigint, col_int, col_bigint), + GREATEST(col_int, col_bigint, 100), + LEAST(col_double, CAST(col_int AS DOUBLE), 1.0) + FROM ${extTable} WHERE id = 1 + """ + contains "SHORT-CIRCUIT" + } + + qt_if_cmp """ + SELECT /*+ SET_VAR(enable_nereids_planner=true) */ + IF(col_int > col_bigint, col_int, col_bigint), + GREATEST(col_int, col_bigint, 100), + LEAST(col_double, CAST(col_int AS DOUBLE), 1.0) + FROM ${extTable} WHERE id = 1 + """ + + // ---------- 8. Deeply nested multi-operator expressions ---------- + explain { + sql """ + SELECT /*+ SET_VAR(enable_nereids_planner=true) */ + CAST(IF(col_int > 0, + CONCAT(CAST(col_int + col_bigint AS VARCHAR(50)), '-pos'), + CONCAT(CAST(ABS(col_int) AS VARCHAR(50)), '-neg')) AS VARCHAR(100)), + ROUND(COALESCE(col_double, 0) * IFNULL(col_decimal, 1.0), 2) + FROM ${extTable} WHERE id = 1 + """ + contains "SHORT-CIRCUIT" + } + + qt_nested """ + SELECT /*+ SET_VAR(enable_nereids_planner=true) */ + CAST(IF(col_int > 0, + CONCAT(CAST(col_int + col_bigint AS VARCHAR(50)), '-pos'), + CONCAT(CAST(ABS(col_int) AS VARCHAR(50)), '-neg')) AS VARCHAR(100)), + ROUND(COALESCE(col_double, 0) * IFNULL(col_decimal, 1.0), 2) + FROM ${extTable} WHERE id = 1 + """ + + // ---------- 9. Simple projection (no intermediate layer) ---------- + explain { + sql """ + SELECT /*+ SET_VAR(enable_nereids_planner=true) */ + col_int, col_str + FROM ${extTable} WHERE id = 1 + """ + contains "SHORT-CIRCUIT" + } + + qt_simple """ + SELECT /*+ SET_VAR(enable_nereids_planner=true) */ + col_int, col_str + FROM ${extTable} WHERE id = 1 + """ + + // ---------- 10. NULL row with complex expressions ---------- + explain { + sql """ + SELECT /*+ SET_VAR(enable_nereids_planner=true) */ + IFNULL(col_int, 0) + IFNULL(col_bigint, 0), + COALESCE(col_str, 'empty'), + CASE WHEN col_int IS NULL THEN 'null_int' ELSE 'has_int' END, + CAST(IFNULL(col_double, -1) AS BIGINT) + FROM ${extTable} WHERE id = 3 + """ + contains "SHORT-CIRCUIT" + } + + qt_null_row """ + SELECT /*+ SET_VAR(enable_nereids_planner=true) */ + IFNULL(col_int, 0) + IFNULL(col_bigint, 0), + COALESCE(col_str, 'empty'), + CASE WHEN col_int IS NULL THEN 'null_int' ELSE 'has_int' END, + CAST(IFNULL(col_double, -1) AS BIGINT) + FROM ${extTable} WHERE id = 3 + """ + + // ---------- 11. Negative values with arithmetic ---------- + explain { + sql """ + SELECT /*+ SET_VAR(enable_nereids_planner=true) */ + ABS(col_int) + ABS(col_bigint), + col_int * col_int, + IFNULL(col_double, 0) + IFNULL(CAST(col_int AS DOUBLE), 0) + FROM ${extTable} WHERE id = 2 + """ + contains "SHORT-CIRCUIT" + } + + qt_neg_arith """ + SELECT /*+ SET_VAR(enable_nereids_planner=true) */ + ABS(col_int) + ABS(col_bigint), + col_int * col_int, + IFNULL(col_double, 0) + IFNULL(CAST(col_int AS DOUBLE), 0) + FROM ${extTable} WHERE id = 2 + """ +}