diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/PlannerHelper.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/PlannerHelper.java index 1cef9ab4ce633..e2fb103dda5bf 100644 --- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/PlannerHelper.java +++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/PlannerHelper.java @@ -139,6 +139,8 @@ public static IgniteRel optimize(SqlNode sqlNode, IgnitePlanner planner, IgniteL rel = planner.transform(PlannerPhase.HEP_PROJECT_PUSH_DOWN, rel.getTraitSet(), rel); + rel = planner.transform(PlannerPhase.HEP_EMPTY_NODES_ELIMINATION, rel.getTraitSet(), rel); + rel = optimizeJoinsOrder(planner, rel, topHints); RelTraitSet desired = rel.getCluster().traitSet() diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/PlannerPhase.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/PlannerPhase.java index b8333a541663f..32c676b1fdd6f 100644 --- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/PlannerPhase.java +++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/PlannerPhase.java @@ -75,6 +75,7 @@ import org.apache.ignite.internal.processors.query.calcite.rule.logical.IgniteMultiJoinOptimizeRule; import org.apache.ignite.internal.processors.query.calcite.rule.logical.LogicalOrToUnionRule; import org.apache.ignite.internal.processors.query.calcite.rule.logical.ProjectScanMergeRule; +import org.apache.ignite.internal.processors.query.calcite.rule.logical.PruneTableModifyRule; import static org.apache.ignite.internal.processors.query.calcite.prepare.IgnitePrograms.cbo; import static org.apache.ignite.internal.processors.query.calcite.prepare.IgnitePrograms.hep; @@ -151,11 +152,42 @@ public enum PlannerPhase { } }, + /** */ + HEP_EMPTY_NODES_ELIMINATION("Heuristic phase to eliminate empty nodes") { + /** {@inheritDoc} */ + @Override public RuleSet getRules(PlanningContext ctx) { + return ctx.rules(RuleSets.ofList( + PruneEmptyRules.PROJECT_INSTANCE, + PruneEmptyRules.FILTER_INSTANCE, + PruneEmptyRules.SORT_INSTANCE, + PruneEmptyRules.AGGREGATE_INSTANCE, + PruneEmptyRules.JOIN_LEFT_INSTANCE, + PruneEmptyRules.JOIN_RIGHT_INSTANCE + )); + } + + /** {@inheritDoc} */ + @Override public Program getProgram(PlanningContext ctx) { + return hep(getRules(ctx)); + } + }, + /** */ HEP_OPTIMIZE_JOIN_ORDER("Heuristic phase to optimize joins order") { /** {@inheritDoc} */ @Override public RuleSet getRules(PlanningContext ctx) { - return ctx.rules(RuleSets.ofList(IgniteMultiJoinOptimizeRule.INSTANCE)); + return ctx.rules(RuleSets.ofList( + IgniteMultiJoinOptimizeRule.INSTANCE, + + CoreRules.JOIN_PUSH_TRANSITIVE_PREDICATES, + + PruneEmptyRules.PROJECT_INSTANCE, + PruneEmptyRules.FILTER_INSTANCE, + PruneEmptyRules.SORT_INSTANCE, + PruneEmptyRules.AGGREGATE_INSTANCE, + PruneEmptyRules.JOIN_LEFT_INSTANCE, + PruneEmptyRules.JOIN_RIGHT_INSTANCE + )); } /** {@inheritDoc} */ @@ -253,6 +285,7 @@ public enum PlannerPhase { PruneEmptyRules.CORRELATE_LEFT_INSTANCE, PruneEmptyRules.CORRELATE_RIGHT_INSTANCE, + PruneTableModifyRule.INSTANCE, // Useful of this rule is not clear now. // CoreRules.AGGREGATE_REDUCE_FUNCTIONS, diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/logical/FilterScanMergeRule.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/logical/FilterScanMergeRule.java index 1b886792e5da3..8dba3d7b2e22e 100644 --- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/logical/FilterScanMergeRule.java +++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/logical/FilterScanMergeRule.java @@ -27,6 +27,7 @@ import org.apache.calcite.plan.RelTraitSet; import org.apache.calcite.rel.RelNode; import org.apache.calcite.rel.logical.LogicalFilter; +import org.apache.calcite.rel.logical.LogicalValues; import org.apache.calcite.rex.RexBuilder; import org.apache.calcite.rex.RexInputRef; import org.apache.calcite.rex.RexNode; @@ -121,6 +122,21 @@ private FilterScanMergeRule(Config config) { // We need to replace RexInputRef with RexLocalRef because TableScan doesn't have inputs. condition = RexUtils.replaceInputRefs(condition); + // Eliminate scan if always false condition found. + if (condition.isAlwaysFalse()) { + call.transformTo(LogicalValues.createEmpty(cluster, scan.getRowType())); + call.getPlanner().prune(filter); + call.getPlanner().prune(scan); + return; + } + + // Eliminate always true condition. + if (condition.isAlwaysTrue()) { + call.transformTo(scan); + call.getPlanner().prune(filter); + return; + } + // Set default traits, real traits will be calculated for physical node. RelTraitSet trait = cluster.traitSet(); diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/logical/PruneTableModifyRule.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/logical/PruneTableModifyRule.java new file mode 100644 index 0000000000000..cb1a84699a4f9 --- /dev/null +++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/logical/PruneTableModifyRule.java @@ -0,0 +1,76 @@ +/* + * 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.ignite.internal.processors.query.calcite.rule.logical; + +import java.util.Collections; +import java.util.List; +import org.apache.calcite.plan.RelOptRule; +import org.apache.calcite.plan.RelOptRuleCall; +import org.apache.calcite.plan.RelRule; +import org.apache.calcite.rel.RelNode; +import org.apache.calcite.rel.core.TableModify; +import org.apache.calcite.rel.core.Values; +import org.apache.calcite.rel.rules.SubstitutionRule; +import org.apache.calcite.rex.RexLiteral; +import org.immutables.value.Value; + +/** + * Rule that eliminates table modify node if it doesn't have any source rows. + */ +@Value.Enclosing +public class PruneTableModifyRule extends RelRule implements SubstitutionRule { + /** */ + public static final RelOptRule INSTANCE = Config.DEFAULT.toRule(); + + /** + * Constructor. + * + * @param config Rule configuration. + */ + private PruneTableModifyRule(PruneTableModifyRule.Config config) { + super(config); + } + + /** {@inheritDoc} */ + @Override public void onMatch(RelOptRuleCall call) { + TableModify singleRel = call.rel(0); + + // TODO https://issues.apache.org/jira/browse/IGNITE-23512: Default Calcite RexBuilder ignores field type and extract type from + // the given value. E.g. for zero value RexBuilder creates INT literal. Use simple way create `singleValue` after fixing the issue. + // RelNode singleValue = call.builder().values(singleRel.getRowType(), 0L).build(); + RexLiteral zeroLiteral = + singleRel.getCluster().getRexBuilder().makeLiteral(0L, singleRel.getRowType().getFieldList().get(0).getType()); + RelNode singleVal = call.builder().values(List.of(List.of(zeroLiteral)), singleRel.getRowType()).build(); + + singleVal = singleVal.copy(singleRel.getCluster().traitSet(), Collections.emptyList()); + call.transformTo(singleVal); + } + + /** Rule configuration. */ + @Value.Immutable(singleton = false) + public interface Config extends RuleFactoryConfig { + /** */ + Config DEFAULT = ImmutablePruneTableModifyRule.Config.builder() + .withDescription("PruneTableModify") + .withRuleFactory(PruneTableModifyRule::new) + .withOperandSupplier(b0 -> + b0.operand(TableModify.class).oneInput(b1 -> + b1.operand(Values.class).predicate(Values::isEmpty).noInputs())) + .build(); + } +} diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/TableDmlIntegrationTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/TableDmlIntegrationTest.java index f94c45162fbaa..36a05f0f7af84 100644 --- a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/TableDmlIntegrationTest.java +++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/TableDmlIntegrationTest.java @@ -42,6 +42,7 @@ import org.apache.ignite.internal.util.typedef.F; import org.apache.ignite.internal.util.typedef.internal.S; import org.apache.ignite.testframework.GridTestUtils; +import org.hamcrest.CoreMatchers; import org.junit.Test; /** */ @@ -672,6 +673,18 @@ public void testInsertMultiRowValues() { assertQuery("SELECT * FROM test").resultSize(rowsCnt).check(); } + /** */ + @Test + public void insertFromSelectWithAlwaysFalseCondition() { + sql("CREATE TABLE test (id INT PRIMARY KEY, val REAL)"); + sql("CREATE TABLE test2 (id INT PRIMARY KEY, val REAL)"); + + assertQuery("INSERT INTO test2 SELECT id, val FROM test WHERE val > 1 AND val < 0") + .matches(CoreMatchers.not(QueryChecker.containsSubPlan("IgniteTableModify"))) + .returns(0L) + .check(); + } + /** */ private void checkDefaultValue(String sqlType, String sqlVal, Object expectedVal) { try { diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/AbstractPlannerTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/AbstractPlannerTest.java index b0310c799ce22..0ffeafa163d3c 100644 --- a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/AbstractPlannerTest.java +++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/AbstractPlannerTest.java @@ -464,6 +464,8 @@ protected void assertPlan( ) throws Exception { IgniteRel plan = physicalPlan(plannerCtx(sql, schemas, planLsnr, disabledRules)); + System.out.println("plan = " + RelOptUtil.toString(plan, SqlExplainLevel.ALL_ATTRIBUTES)); + checkSplitAndSerialization(plan, schemas); if (!predicate.test((T)plan)) { diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/ProjectFilterScanMergePlannerTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/ProjectFilterScanMergePlannerTest.java index 95b0b3c2700c2..247009d0a2110 100644 --- a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/ProjectFilterScanMergePlannerTest.java +++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/ProjectFilterScanMergePlannerTest.java @@ -19,13 +19,16 @@ import java.util.List; import java.util.Objects; +import java.util.function.Predicate; import java.util.stream.Collectors; +import org.apache.calcite.rex.RexLiteral; import org.apache.calcite.rex.RexNode; import org.apache.calcite.util.ImmutableBitSet; import org.apache.ignite.internal.processors.query.calcite.prepare.bounds.SearchBounds; import org.apache.ignite.internal.processors.query.calcite.rel.IgniteAggregate; import org.apache.ignite.internal.processors.query.calcite.rel.IgniteIndexScan; import org.apache.ignite.internal.processors.query.calcite.rel.IgniteTableScan; +import org.apache.ignite.internal.processors.query.calcite.rel.IgniteValues; import org.apache.ignite.internal.processors.query.calcite.schema.IgniteSchema; import org.apache.ignite.internal.processors.query.calcite.trait.IgniteDistributions; import org.junit.Before; @@ -235,6 +238,80 @@ public void testFilterIdentityFilterMerge() throws Exception { "ProjectFilterTransposeRule", "FilterProjectTransposeRule"); } + /** */ + @Test + public void testAlwaysTrueFilterPruning() throws Exception { + String sql = "SELECT a, c FROM tbl WHERE a > 1 OR a < 3 OR a IS NULL"; + + assertPlan(sql, publicSchema, isInstanceOf(IgniteTableScan.class) + .and(scan -> scan.projects() == null) + .and(scan -> scan.condition() == null) + .and(scan -> ImmutableBitSet.of(0, 2).equals(scan.requiredColumns())), + "ProjectFilterTransposeRule", "FilterProjectTransposeRule"); + } + + /** */ + @Test + public void testAlwaysFalseFilterPruning() throws Exception { + Predicate hasEmptyValuesOnly = hasEmptyValuesOnlyPredicate(); + + // Table scan elimination. + String sql = "SELECT a, c FROM tbl WHERE a > 1 AND a < 0"; + assertPlan(sql, publicSchema, hasEmptyValuesOnly); + + sql = "SELECT a, c FROM (SELECT a, c FROM tbl WHERE a > 1) WHERE c = 1 AND c IS NULL"; + assertPlan(sql, publicSchema, hasEmptyValuesOnly, + "ProjectFilterTransposeRule", "FilterProjectTransposeRule"); + + sql = "SELECT a, c FROM (SELECT a, c FROM tbl WHERE a > 1) WHERE a < 0"; + assertPlan(sql, publicSchema, hasEmptyValuesOnly, + "ProjectFilterTransposeRule", "FilterProjectTransposeRule"); + + // JOIN branch elimination. + sql = "SELECT t1.a, t2.a, t1.c FROM tbl AS t1 LEFT JOIN tbl AS t2 ON t1.a = t2.a WHERE t2.a = 1 AND t2.a IS NULL AND t1.c = 1"; + assertPlan(sql, publicSchema, hasEmptyValuesOnly); + + sql = "SELECT t1.a, t2.a, t1.c FROM tbl AS t1 INNER JOIN tbl AS t2 ON t1.a = t2.a WHERE t2.a = 1 AND t2.a IS NULL"; + assertPlan(sql, publicSchema, hasEmptyValuesOnly); + + sql = "SELECT t1.a, t2.a, t1.c FROM tbl AS t1 INNER JOIN tbl AS t2 ON t1.a = t2.a WHERE t1.a = 1 AND t2.a = 2"; + assertPlan(sql, publicSchema, hasEmptyValuesOnly); + } + + /** */ + @Test + public void testJoinWithAlwaysFalseConditionPruning() throws Exception { + String sql = "SELECT t1.a, t2.a, t1.c FROM tbl AS t1 LEFT JOIN tbl AS t2 ON (t1.a = t2.a AND t2.a = 1 AND t2.a = 2) WHERE t1.c = 1"; + assertPlan(sql, publicSchema, isInstanceOf(IgniteTableScan.class) + .and(scan -> scan.projects() != null) + .and(scan -> scan.condition() != null) + .and(scan -> "=($t1, 1)".equals(scan.condition().toString())) + ); + + sql = "SELECT t1.a, t2.a, t1.c FROM tbl AS t1 INNER JOIN tbl AS t2 ON t1.a = t2.a AND t2.a = 1 AND t2.a = 2"; + assertPlan(sql, publicSchema, hasEmptyValuesOnlyPredicate()); + } + + /** */ + @Test + public void testAlwaysFalseFilterPruningWithDml() throws Exception { + Predicate zeroDmlResultPredicate = isInstanceOf(IgniteValues.class) + .and(values -> values.getTuples().size() == 1) // single row + .and(values -> values.getTuples().get(0).size() == 1) // row of single column + .and(values -> RexLiteral.intValue(values.getTuples().get(0).get(0)) == 0L); + + String sql = "INSERT INTO tbl (a, c) SELECT a, b FROM tbl WHERE a > 1 AND a < 0"; + assertPlan(sql, publicSchema, zeroDmlResultPredicate); + + sql = "INSERT INTO tbl (a, c) (SELECT a, c FROM (SELECT a, c FROM tbl WHERE a > 1) WHERE a < 0)"; + assertPlan(sql, publicSchema, zeroDmlResultPredicate); + } + + /** */ + private Predicate hasEmptyValuesOnlyPredicate() { + return isInstanceOf(IgniteValues.class).and(values -> values.getTuples().isEmpty()); + } + /** */ @Test public void testFilterFilterMerge() throws Exception {