diff --git a/docs/_docs/SQL/sql-calcite.adoc b/docs/_docs/SQL/sql-calcite.adoc index 63e65244a2f6e..90a9a910aa4a1 100644 --- a/docs/_docs/SQL/sql-calcite.adoc +++ b/docs/_docs/SQL/sql-calcite.adoc @@ -276,9 +276,9 @@ SELECT /*+ FORCE_INDEX('TBL2_idx1') */ T1.V1, T2.V1 FROM TBL1 T1, TBL2 T2 WHERE ---- === Hint scope -Hints are defined for a relation operator, usually for SELECT. Most of the hints are "visible" to their relation -operators, for the following operators, queries and subqueries. The hints defined in the subquery are "visible" only for -this subquery and its subqueries. Hint is not "visible" to the previous relation operator if it is defined after it. +Hints are defined for SELECT relation operator and are "visible" for the following operators, queries and subqueries. +The hints defined in the subquery are "visible" only for this subquery and its subqueries. Hint is not "visible" to +the previous relation operator if it is defined after it. Example: [source, SQL] @@ -309,6 +309,18 @@ The optimizer tries to apply every hint and its parameters, if possible. But it * The hint parameter is incorrect or refers to a nonexistent object, such as a nonexistent index or table. * The current hints or current parameters are incompatible with the previous ones, such as forcing the use and disabling of the same index. +=== Hint limitations +Currently, SQL hints do not recognize the aliases. You can't refer to an alias like this: +[source, SQL] +---- +SELECT /*+ MERGE_JOIN(T2) */ T2.V1 FROM TBL1 T1 JOIN TBL2 T2 ON T1.V3=T2.V1 WHERE T1.V2=? AND T2.V2=? +---- +Instead, a table name have to be used: +[source, SQL] +---- +SELECT /*+ MERGE_JOIN(TBL2) */ T2.V1 FROM TBL1 T1 JOIN TBL2 T2 ON T1.V3=T2.V1 WHERE T1.V2=? AND T2.V2=? +---- + === Supportted hints ==== FORCE_INDEX / NO_INDEX @@ -342,6 +354,25 @@ SELECT /*+ ENFORCE_JOIN_ORDER */ T1.V1, T2.V1, T2.V2, T3.V1, T3.V2, T3.V3 FROM T SELECT t1.v1, t3.v2 FROM TBL1 t1 JOIN TBL3 t3 on t1.v3=t3.v3 WHERE t1.v2 in (SELECT /*+ ENFORCE_JOIN_ORDER */ t2.v2 FROM TBL2 t2 JOIN TBL3 t3 ON t2.v1=t3.v1) ---- +==== MERGE_JOIN, NL_JOIN, CNL_JOIN +Forces certain join type: Merge, Nested Loop and Correlated Nested Loop respectively. + +Every of those has the negation like 'NO_INDEX': CNL_JOIN, NO_CNL_JOIN. The negation hint disables certain join type. + +===== Parameters: +* Empty. To force or disable certain join type for every join. +* Single or several tables names force or disable certain join type only for joining of these tables. + +===== Example: +[source, SQL] +---- +SELECT /*+ MERGE_JOIN */ t1.v1, t2.v2 FROM TBL1 t1, TBL2 t2 WHERE t1.v3=t2.v3 + +SELECT /*+ NL_JOIN(TBL3,TBL1) */ t4.v1, t2.v2 FROM TBL1 t4 JOIN TBL2 t2 on t1.v3=t2.v3 WHERE t2.v1 in (SELECT t3.v3 FROM TBL3 t3 JOIN TBL1 t4 on t3.v2=t4.v2) + +SELECT t1.v1, t2.v2 FROM TBL2 t1 JOIN TBL1 t2 on t1.v3=t2.v3 WHERE t2.v3 in (SELECT /*+ NO_CNL_JOIN(TBL4) */ t3.v3 FROM TBL3 t3 JOIN TBL4 t4 on t3.v1=t4.v1) +---- + ==== EXPAND_DISTINCT_AGG If the optimizer wraps aggregation operations with a join, forces expanding of only distinct aggregates to the join. Removes duplicates before the joining and speeds up it. diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/hint/HintDefinition.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/hint/HintDefinition.java index a20e2675282da..c8d4e2f2b4cc6 100644 --- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/hint/HintDefinition.java +++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/hint/HintDefinition.java @@ -96,6 +96,84 @@ public enum HintDefinition { @Override public HintOptionsChecker optionsChecker() { return NO_INDEX.optionsChecker(); } + }, + + /** Forces merge join. */ + MERGE_JOIN { + /** {@inheritDoc} */ + @Override public HintPredicate predicate() { + return HintPredicates.JOIN; + } + + /** {@inheritDoc} */ + @Override public HintOptionsChecker optionsChecker() { + return HintsConfig.OPTS_CHECK_NO_KV; + } + }, + + /** Disables merge join. */ + NO_MERGE_JOIN { + /** {@inheritDoc} */ + @Override public HintPredicate predicate() { + return MERGE_JOIN.predicate(); + } + + /** {@inheritDoc} */ + @Override public HintOptionsChecker optionsChecker() { + return MERGE_JOIN.optionsChecker(); + } + }, + + /** Forces nested loop join. */ + NL_JOIN { + /** {@inheritDoc} */ + @Override public HintPredicate predicate() { + return HintPredicates.JOIN; + } + + /** {@inheritDoc} */ + @Override public HintOptionsChecker optionsChecker() { + return HintsConfig.OPTS_CHECK_NO_KV; + } + }, + + /** Disables nested loop join. */ + NO_NL_JOIN { + /** {@inheritDoc} */ + @Override public HintPredicate predicate() { + return NL_JOIN.predicate(); + } + + /** {@inheritDoc} */ + @Override public HintOptionsChecker optionsChecker() { + return NL_JOIN.optionsChecker(); + } + }, + + /** Forces correlated nested loop join. */ + CNL_JOIN { + /** {@inheritDoc} */ + @Override public HintPredicate predicate() { + return HintPredicates.JOIN; + } + + /** {@inheritDoc} */ + @Override public HintOptionsChecker optionsChecker() { + return HintsConfig.OPTS_CHECK_NO_KV; + } + }, + + /** Disables correlated nested loop join. */ + NO_CNL_JOIN { + /** {@inheritDoc} */ + @Override public HintPredicate predicate() { + return CNL_JOIN.predicate(); + } + + /** {@inheritDoc} */ + @Override public HintOptionsChecker optionsChecker() { + return CNL_JOIN.optionsChecker(); + } }; /** diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/logical/IgniteLogicalTableScan.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/logical/IgniteLogicalTableScan.java index 0756b5342519f..ed973c4ec3679 100644 --- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/logical/IgniteLogicalTableScan.java +++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rel/logical/IgniteLogicalTableScan.java @@ -19,6 +19,7 @@ import java.util.List; +import com.google.common.collect.ImmutableList; import org.apache.calcite.plan.RelOptCluster; import org.apache.calcite.plan.RelOptTable; import org.apache.calcite.plan.RelTraitSet; @@ -40,7 +41,8 @@ public static IgniteLogicalTableScan create( @Nullable RexNode cond, @Nullable ImmutableBitSet requiredColumns ) { - return new IgniteLogicalTableScan(cluster, traits, tbl, hints, proj, cond, requiredColumns); + return new IgniteLogicalTableScan(cluster, traits, tbl, hints == null ? ImmutableList.of() : hints, proj, cond, + requiredColumns); } /** diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/AbstractIgniteJoinConverterRule.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/AbstractIgniteJoinConverterRule.java new file mode 100644 index 0000000000000..5b44da340b598 --- /dev/null +++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/AbstractIgniteJoinConverterRule.java @@ -0,0 +1,187 @@ +/* + * 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; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; +import org.apache.calcite.plan.RelOptRuleCall; +import org.apache.calcite.plan.volcano.RelSubset; +import org.apache.calcite.rel.RelNode; +import org.apache.calcite.rel.core.Join; +import org.apache.calcite.rel.hint.RelHint; +import org.apache.calcite.rel.logical.LogicalJoin; +import org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition; +import org.apache.ignite.internal.processors.query.calcite.hint.HintUtils; + +import static org.apache.calcite.util.Util.last; +import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.CNL_JOIN; +import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.MERGE_JOIN; +import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.NL_JOIN; +import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.NO_CNL_JOIN; +import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.NO_MERGE_JOIN; +import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.NO_NL_JOIN; + +/** */ +abstract class AbstractIgniteJoinConverterRule extends AbstractIgniteConverterRule { + /** Known join type hints and the opposite hints. */ + private static final EnumMap HINTS = new EnumMap<>(HintDefinition.class); + + /** Known join type hints as flat array. */ + private static final HintDefinition[] ALL_HINTS; + + /** Hint disabing this join type. */ + private final HintDefinition knownDisableHint; + + /** Hint forcing usage of this join type. */ + private final HintDefinition knownForceHint; + + static { + HINTS.put(NL_JOIN, NO_NL_JOIN); + HINTS.put(CNL_JOIN, NO_CNL_JOIN); + HINTS.put(MERGE_JOIN, NO_MERGE_JOIN); + + ALL_HINTS = Stream.concat(HINTS.keySet().stream(), HINTS.values().stream()).toArray(HintDefinition[]::new); + } + + /** */ + protected AbstractIgniteJoinConverterRule(String descriptionPrefix, HintDefinition forceHint) { + super(LogicalJoin.class, descriptionPrefix); + + assert HINTS.containsKey(forceHint); + + knownDisableHint = HINTS.get(forceHint); + knownForceHint = forceHint; + } + + /** {@inheritDoc} */ + @Override public final boolean matches(RelOptRuleCall call) { + return super.matches(call) && matchesJoin(call) && !disabledByHints(call.rel(0)); + } + + /** */ + private boolean disabledByHints(LogicalJoin join) { + if (HintUtils.allRelHints(join).isEmpty()) + return false; + + boolean ruleDisabled = false; + + Map> hintedTables = new HashMap<>(); + + Set joinTbls = joinTblNames(join); + + assert joinTbls.size() < 3; + + for (RelHint hint : HintUtils.hints(join, ALL_HINTS)) { + Set matchedTbls = hint.listOptions.isEmpty() ? joinTbls : new HashSet<>(hint.listOptions); + + if (!hint.listOptions.isEmpty()) + matchedTbls.retainAll(joinTbls); + + if (matchedTbls.isEmpty()) + continue; + + HintDefinition curHintDef = HintDefinition.valueOf(hint.hintName); + boolean curHintIsDisable = !HINTS.containsKey(curHintDef); + boolean unableToProcess = false; + + for (String tbl : joinTbls) { + Collection prevTblHints = hintedTables.get(tbl); + + if (prevTblHints == null) + continue; + + Set disabled = null; + + for (HintDefinition prevTblHint : prevTblHints) { + boolean prevHintIsDisable = !HINTS.containsKey(prevTblHint); + + if (prevHintIsDisable) { + if (disabled == null) + disabled = new HashSet<>(); + + disabled.add(prevTblHint); + } + + // Prohibited: disabling all join types, combinations of forcing and disabling same join type, + // forcing of different join types. + if (curHintIsDisable && (disabled != null && disabled.size() == HINTS.size() - 1) + || isMutuallyExclusive(curHintDef, prevTblHint)) + unableToProcess = true; + } + } + + if (unableToProcess) { + HintUtils.skippedHint(join, hint, "This join type is already disabled or forced to use before " + + "by previous hints"); + + continue; + } + + for (String tbl : matchedTbls) + hintedTables.computeIfAbsent(tbl, t -> new ArrayList<>()).add(curHintDef); + + // This join type is directyly disabled or other join type is forced. + if (curHintIsDisable && curHintDef == knownDisableHint || !curHintIsDisable && knownForceHint != curHintDef) + ruleDisabled = true; + } + + return ruleDisabled; + } + + /** + * @return {@code True} if {@code curHint} and {@code prevHint} cannot be applied both. {@code False} otherwise. + */ + private static boolean isMutuallyExclusive(HintDefinition curHint, HintDefinition prevHint) { + if (curHint == prevHint) + return false; + + HintDefinition curDisable = HINTS.get(curHint); + HintDefinition prevDisable = HINTS.get(prevHint); + + return curDisable != null && prevDisable != null || curDisable == prevHint || curHint == prevDisable; + } + + /** */ + protected static Set joinTblNames(Join join) { + Set res = new LinkedHashSet<>(); + + for (RelNode in : join.getInputs()) { + if (in instanceof RelSubset) + in = ((RelSubset)in).getOriginal(); + + if (in.getTable() != null) + res.add(last(in.getTable().getQualifiedName())); + } + + return res; + } + + /** + * @return {@code True} if {@code call} is supported by current join rule. {@code False} otherwise. + */ + protected boolean matchesJoin(RelOptRuleCall call) { + return true; + } +} diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/CorrelatedNestedLoopJoinRule.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/CorrelatedNestedLoopJoinRule.java index bfb5662bbd7b6..7d46e8adc95f8 100644 --- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/CorrelatedNestedLoopJoinRule.java +++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/CorrelatedNestedLoopJoinRule.java @@ -39,13 +39,14 @@ import org.apache.calcite.rex.RexNode; import org.apache.calcite.rex.RexShuttle; import org.apache.calcite.tools.RelBuilder; +import org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition; import org.apache.ignite.internal.processors.query.calcite.rel.IgniteConvention; import org.apache.ignite.internal.processors.query.calcite.rel.IgniteCorrelatedNestedLoopJoin; import org.apache.ignite.internal.processors.query.calcite.trait.CorrelationTrait; import org.apache.ignite.internal.processors.query.calcite.trait.RewindabilityTrait; /** */ -public class CorrelatedNestedLoopJoinRule extends AbstractIgniteConverterRule { +public class CorrelatedNestedLoopJoinRule extends AbstractIgniteJoinConverterRule { /** */ public static final RelOptRule INSTANCE = new CorrelatedNestedLoopJoinRule(1); @@ -57,7 +58,7 @@ public class CorrelatedNestedLoopJoinRule extends AbstractIgniteConverterRule { +public class MergeJoinConverterRule extends AbstractIgniteJoinConverterRule { /** */ public static final RelOptRule INSTANCE = new MergeJoinConverterRule(); @@ -43,11 +44,11 @@ public class MergeJoinConverterRule extends AbstractIgniteConverterRule { +public class NestedLoopJoinConverterRule extends AbstractIgniteJoinConverterRule { /** */ public static final RelOptRule INSTANCE = new NestedLoopJoinConverterRule(); @@ -39,7 +40,7 @@ public class NestedLoopJoinConverterRule extends AbstractIgniteConverterRule sql(client, sql), AssertionError.class); diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/DataTypesTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/DataTypesTest.java index 2db195183fad3..a3cf6362f2426 100644 --- a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/DataTypesTest.java +++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/DataTypesTest.java @@ -167,8 +167,7 @@ private void testUuid(boolean indexed) { .returns(2, uuid2.toString(), uuid2) .check(); - //TODO: https://issues.apache.org/jira/browse/IGNITE-16693 Incorrect processing nulls values in merge join. - assertQuery("SELECT /*+ DISABLE_RULE('MergeJoinConverter') */ t1.* from t t1, (select * from t) t2 " + + assertQuery("SELECT t1.* from t t1, (select * from t) t2 " + "where t1.uid = t2.uid") .returns(1, "fd10556e-fc27-4a99-b5e4-89b8344cb3ce", uuid1) .returns(2, uuid2.toString(), uuid2) diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/HashSpoolIntegrationTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/HashSpoolIntegrationTest.java index 8f66f8b2b540c..d1ddc5423bf5a 100644 --- a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/HashSpoolIntegrationTest.java +++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/HashSpoolIntegrationTest.java @@ -55,8 +55,7 @@ public void testNullsInSearchRowMultipleColumns() { executeSql("INSERT INTO t0 VALUES (null, 0), (1, null), (null, 2), (3, null), (1, 1)"); executeSql("INSERT INTO t1 VALUES (null, 0), (null, 1), (2, null), (3, null), (1, 1)"); - String sql = "SELECT /*+ DISABLE_RULE ('MergeJoinConverter', 'NestedLoopJoinConverter', " + - "'FilterSpoolMergeToSortedIndexSpoolRule')*/ * " + + String sql = "SELECT /*+ CNL_JOIN, DISABLE_RULE('FilterSpoolMergeToSortedIndexSpoolRule') */ * " + "FROM t0 JOIN t1 ON t0.i1=t1.i1 AND t0.i2=t1.i2"; assertQuery(sql) @@ -90,8 +89,7 @@ public void testIsNotDistinctFrom() { executeSql("CREATE TABLE t2(i3 INTEGER, i4 INTEGER)"); executeSql("INSERT INTO t2 VALUES (1, 1), (2, 2), (null, 3), (4, null)"); - String sql = "SELECT /*+ DISABLE_RULE('NestedLoopJoinConverter', 'MergeJoinConverter') */ i1, i4 " + - "FROM t1 JOIN t2 ON i1 IS NOT DISTINCT FROM i3"; + String sql = "SELECT /*+ CNL_JOIN */ i1, i4 FROM t1 JOIN t2 ON i1 IS NOT DISTINCT FROM i3"; assertQuery(sql) .matches(QueryChecker.containsSubPlan("IgniteHashIndexSpool")) @@ -100,8 +98,7 @@ public void testIsNotDistinctFrom() { .returns(null, 3) .check(); - sql = "SELECT /*+ DISABLE_RULE('NestedLoopJoinConverter', 'MergeJoinConverter') */ i1, i4 " + - "FROM t1 JOIN t2 ON i1 IS NOT DISTINCT FROM i3 AND i2 = i4"; + sql = "SELECT /*+ CNL_JOIN */ i1, i4 FROM t1 JOIN t2 ON i1 IS NOT DISTINCT FROM i3 AND i2 = i4"; assertQuery(sql) .matches(QueryChecker.containsSubPlan("IgniteHashIndexSpool")) diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/IndexRebuildIntegrationTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/IndexRebuildIntegrationTest.java index 032b7f90f354d..ec9a6e14fedaf 100644 --- a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/IndexRebuildIntegrationTest.java +++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/IndexRebuildIntegrationTest.java @@ -321,7 +321,7 @@ public void testRebuildOnRemoteNodeCorrelated() throws Exception { IgniteEx initNode = grid(0); // Correlated join with correlation in filter, without project. - String sql = "SELECT /*+ DISABLE_RULE('MergeJoinConverter', 'NestedLoopJoinConverter') */ tbl2.id, tbl.val " + + String sql = "SELECT /*+ CNL_JOIN */ tbl2.id, tbl.val " + "FROM tbl2 LEFT JOIN tbl ON tbl.id = tbl2.id AND tbl.val = tbl2.val AND tbl.id % 2 = 0 " + "WHERE tbl2.id BETWEEN 10 AND 19"; @@ -334,7 +334,7 @@ public void testRebuildOnRemoteNodeCorrelated() throws Exception { checkRebuildIndexQuery(grid(1), checker, checker); // Correlated join with correlation in filter, with project. - sql = "SELECT /*+ DISABLE_RULE('MergeJoinConverter', 'NestedLoopJoinConverter') */ tbl2.id, tbl.val1 " + + sql = "SELECT /*+ CNL_JOIN */ tbl2.id, tbl.val1 " + "FROM tbl2 JOIN (SELECT tbl.val || '-' AS val1, val, id FROM tbl) AS tbl " + "ON tbl.id = tbl2.id AND tbl.val = tbl2.val " + "WHERE tbl2.id BETWEEN 10 AND 12"; @@ -353,7 +353,7 @@ public void testRebuildOnRemoteNodeCollationRestore() throws Exception { IgniteEx initNode = grid(0); // Correlated join with correlation in filter, with project as a subset of collation. - String sql = "SELECT /*+ DISABLE_RULE('MergeJoinConverter', 'NestedLoopJoinConverter') */ tbl2.id, tbl.id1 " + + String sql = "SELECT /*+ CNL_JOIN */ tbl2.id, tbl.id1 " + "FROM tbl2 JOIN (SELECT tbl.id + 1 AS id1, id FROM tbl WHERE val >= 'val') AS tbl " + "ON tbl.id = tbl2.id " + "WHERE tbl2.val BETWEEN 'val10' AND 'val12'"; @@ -366,7 +366,7 @@ public void testRebuildOnRemoteNodeCollationRestore() throws Exception { checkRebuildIndexQuery(grid(1), checker, checker); // Correlated join with correlation in filter, with a project as a subset of collation with DESC ordering. - sql = "SELECT /*+ DISABLE_RULE('MergeJoinConverter', 'NestedLoopJoinConverter') */ tbl2.id, tbl.id1 " + + sql = "SELECT /*+ CNL_JOIN */ tbl2.id, tbl.id1 " + "FROM tbl2 JOIN (SELECT tbl.id + 1 AS id1, id FROM tbl WHERE val2 >= 'val') AS tbl " + "ON tbl.id = tbl2.id " + "WHERE tbl2.val BETWEEN 'val10' AND 'val12'"; diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/IndexScanlIntegrationTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/IndexScanlIntegrationTest.java index 093acb259fce8..7e7c0df7cbe4e 100644 --- a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/IndexScanlIntegrationTest.java +++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/IndexScanlIntegrationTest.java @@ -69,9 +69,7 @@ public void testNullsInCNLJSearchRow() { RowCountingIndex idx = injectRowCountingIndex(grid(0), "T", "T_IDX"); - String sql = "SELECT /*+ DISABLE_RULE('NestedLoopJoinConverter', 'MergeJoinConverter') */ t1.i1, t2.i1 " + - "FROM t t1 " + - "LEFT JOIN t t2 ON t1.i2 = t2.i1"; + String sql = "SELECT /*+ CNL_JOIN */ t1.i1, t2.i1 FROM t t1 LEFT JOIN t t2 ON t1.i2 = t2.i1"; assertQuery(sql) .matches(QueryChecker.containsSubPlan("IgniteCorrelatedNestedLoopJoin")) @@ -227,8 +225,7 @@ public void testIsNotDistinctFrom() { executeSql("INSERT INTO t2 VALUES (1, 1), (2, 2), (null, 3), (4, null)"); executeSql("CREATE INDEX t2_idx ON t2(i2)"); - String sql = "SELECT /*+ DISABLE_RULE('NestedLoopJoinConverter', 'MergeJoinConverter') */ i1, i3 " + - "FROM t1 JOIN t2 ON i1 IS NOT DISTINCT FROM i2"; + String sql = "SELECT /*+ CNL_JOIN */ i1, i3 FROM t1 JOIN t2 ON i1 IS NOT DISTINCT FROM i2"; assertQuery(sql) .matches(QueryChecker.containsIndexScan("PUBLIC", "T2", "T2_IDX")) @@ -238,8 +235,7 @@ public void testIsNotDistinctFrom() { .check(); // Collapse expanded IS_NOT_DISTINCT_FROM. - sql = "SELECT /*+ DISABLE_RULE('NestedLoopJoinConverter', 'MergeJoinConverter') */ i1, i3 " + - "FROM t1 JOIN t2 ON i1 = i2 OR (i1 IS NULL AND i2 IS NULL)"; + sql = "SELECT /*+ CNL_JOIN */ i1, i3 FROM t1 JOIN t2 ON i1 = i2 OR (i1 IS NULL AND i2 IS NULL)"; assertQuery(sql) .matches(QueryChecker.containsIndexScan("PUBLIC", "T2", "T2_IDX")) diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/IndexSpoolIntegrationTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/IndexSpoolIntegrationTest.java index 61360d212f9fa..e0db2f517f2ab 100644 --- a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/IndexSpoolIntegrationTest.java +++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/IndexSpoolIntegrationTest.java @@ -136,9 +136,7 @@ public void test() throws Exception { List>> cursors = engine.query( null, "PUBLIC", - "SELECT /*+ DISABLE_RULE('NestedLoopJoinConverter', 'MergeJoinConverter') */" + - "T0.val, T1.val FROM TEST0 as T0 " + - "JOIN TEST1 as T1 on T0.jid = T1.jid ", + "SELECT /*+ CNL_JOIN */ T0.val, T1.val FROM TEST0 as T0 JOIN TEST1 as T1 on T0.jid = T1.jid ", X.EMPTY_OBJECT_ARRAY ); diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/MemoryQuotasIntegrationTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/MemoryQuotasIntegrationTest.java index b5c02fadc1a19..f5c684e59cf98 100644 --- a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/MemoryQuotasIntegrationTest.java +++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/MemoryQuotasIntegrationTest.java @@ -240,8 +240,7 @@ public void testNestedLoopJoinNode() { for (int i = 0; i < 800; i++) sql("INSERT INTO tbl2 VALUES (?, ?)", i, new byte[1000]); - assertQuery("SELECT /*+ DISABLE_RULE('CorrelatedNestedLoopJoin', 'MergeJoinConverter') */ " + - "tbl.id, tbl.b, tbl2.id, tbl2.b FROM tbl JOIN tbl2 USING (id)") + assertQuery("SELECT /*+ NL_JOIN */ tbl.id, tbl.b, tbl2.id, tbl2.b FROM tbl JOIN tbl2 USING (id)") .matches(QueryChecker.containsSubPlan("IgniteNestedLoopJoin")) .resultSize(800) .check(); @@ -249,8 +248,7 @@ public void testNestedLoopJoinNode() { for (int i = 800; i < 1000; i++) sql("INSERT INTO tbl2 VALUES (?, ?)", i, new byte[1000]); - assertThrows("SELECT /*+ DISABLE_RULE('CorrelatedNestedLoopJoin', 'MergeJoinConverter') */" + - "tbl.id, tbl.b, tbl2.id, tbl2.b FROM tbl JOIN tbl2 USING (id)", + assertThrows("SELECT /*+ NL_JOIN */ tbl.id, tbl.b, tbl2.id, tbl2.b FROM tbl JOIN tbl2 USING (id)", IgniteException.class, "Query quota exceeded"); } diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/jdbc/JdbcQueryTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/jdbc/JdbcQueryTest.java index ee0a1adc61370..b82c4b35d98cb 100644 --- a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/jdbc/JdbcQueryTest.java +++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/jdbc/JdbcQueryTest.java @@ -247,7 +247,7 @@ public void testEnforcedJoinOrder() throws Exception { assertTrue(plan.contains("joinType=[left]")); } - try (ResultSet rs = stmt.executeQuery("EXPLAIN PLAN FOR SELECT /*+ DISABLE_RULE('NestedLoopJoinConverter') */ " + + try (ResultSet rs = stmt.executeQuery("EXPLAIN PLAN FOR SELECT /*+ NO_NL_JOIN */ " + "p2.Name from Person2 p2 RIGHT JOIN Person1 p1 on p2.NAME=p1.NAME")) { assertTrue(rs.next()); diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/hints/HintsTestSuite.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/hints/HintsTestSuite.java index f54352e0fb0d7..aaddbc3d95391 100644 --- a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/hints/HintsTestSuite.java +++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/hints/HintsTestSuite.java @@ -29,6 +29,7 @@ NoIndexHintPlannerTest.class, ForceIndexHintPlannerTest.class, JoinOrderHintsPlannerTest.class, + JoinTypeHintPlannerTest.class, }) public class HintsTestSuite { } diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/hints/JoinOrderHintsPlannerTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/hints/JoinOrderHintsPlannerTest.java index a0a9a54ea9d2e..18971d79faf44 100644 --- a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/hints/JoinOrderHintsPlannerTest.java +++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/hints/JoinOrderHintsPlannerTest.java @@ -75,17 +75,14 @@ public class JoinOrderHintsPlannerTest extends AbstractPlannerTest { */ @Test public void testDisabledJoinPushThroughJoinLeft() throws Exception { - // Disabling some join rules simplifies exposing of commuted and/or re-ordered joins. - String disabledRules = "DISABLE_RULE('MergeJoinConverter', 'CorrelatedNestedLoopJoin')"; - // Tests swapping of joins is disabled and the order appears in the query, 'TBL3 -> TBL2 -> TBL1': // Join // Join // TableScan(TBL3) // TableScan(TBL2) // TableScan(TBL1) - String sql = String.format("select /*+ %s, %s */ t3.* from TBL3 t3, TBL2 t2, TBL1 t1 where t1.v1=t3.v1 and " + - "t1.v2=t2.v2", HintDefinition.ENFORCE_JOIN_ORDER.name(), disabledRules); + String sql = String.format("select /*+ %s, NL_JOIN */ t3.* from TBL3 t3, TBL2 t2, TBL1 t1 where t1.v1=t3.v1 " + + "and t1.v2=t2.v2", HintDefinition.ENFORCE_JOIN_ORDER.name()); assertPlan(sql, schema, hasChildThat(isInstanceOf(Join.class) .and(input(0, isInstanceOf(Join.class) @@ -116,12 +113,9 @@ public void testDisabledRightJoinTypeCommuting() throws Exception { * @param joinType LEFT or RIGHT JOIN type to test in upper case. */ private void doTestDisabledJoinTypeCommuting(String joinType) throws Exception { - // Disabling some join rules simplifies exposing of commuted and/or re-ordered joins. - String disabledRules = "DISABLE_RULE('MergeJoinConverter', 'CorrelatedNestedLoopJoin')"; - // Tests commuting of the join type is disabled. - String sql = String.format("select /*+ %s, %s */ t3.* from TBL2 t2 %s JOIN TBL1 t1 on t2.v2=t1.v1 %s JOIN " + - "TBL3 t3 on t2.v1=t3.v3", HintDefinition.ENFORCE_JOIN_ORDER.name(), disabledRules, joinType, joinType); + String sql = String.format("select /*+ %s, NL_JOIN */ t3.* from TBL2 t2 %s JOIN TBL1 t1 on t2.v2=t1.v1 %s JOIN " + + "TBL3 t3 on t2.v1=t3.v3", HintDefinition.ENFORCE_JOIN_ORDER.name(), joinType, joinType); assertPlan(sql, schema, hasChildThat(isInstanceOf(Join.class) .and(j -> j.getJoinType() != JoinRelType.valueOf(joinType))).negate()); @@ -132,17 +126,14 @@ private void doTestDisabledJoinTypeCommuting(String joinType) throws Exception { */ @Test public void testDisabledJoinPushThroughJoinRight() throws Exception { - // Disabling some join rules simplifies exposing of commuted and/or re-ordered joins. - String disabledRules = "DISABLE_RULE('MergeJoinConverter', 'CorrelatedNestedLoopJoin')"; - // Tests the swapping of joins is disabled and the order appears as in the query, 'TBL1->TBL2->TBL3': // Join // Join // TableScan(TBL1) // TableScan(TBL2) // TableScan(TBL3) - String sql = String.format("select /*+ %s, %s */ t3.* from TBL1 t1, TBL2 t2, TBL3 t3 where t1.v1=t3.v1 and " + - "t1.v2=t2.v2", HintDefinition.ENFORCE_JOIN_ORDER.name(), disabledRules); + String sql = String.format("select /*+ %s, NL_JOIN */ t3.* from TBL1 t1, TBL2 t2, TBL3 t3 where t1.v1=t3.v1 " + + "and t1.v2=t2.v2", HintDefinition.ENFORCE_JOIN_ORDER.name()); assertPlan(sql, schema, hasChildThat(isInstanceOf(Join.class) .and(input(0, isInstanceOf(Join.class) @@ -156,11 +147,8 @@ public void testDisabledJoinPushThroughJoinRight() throws Exception { */ @Test public void testDisabledCommutingOfJoinInputs() throws Exception { - // Disabling some join rules simplifies exposing of commuted and/or re-ordered joins. - String disabledRules = "DISABLE_RULE('MergeJoinConverter', 'CorrelatedNestedLoopJoin')"; - - String sql = String.format("select /*+ %s, %s */ t3.* from TBL1 t1 JOIN TBL3 t3 on t1.v1=t3.v3 JOIN TBL2 t2 on " + - "t2.v2=t1.v1", HintDefinition.ENFORCE_JOIN_ORDER.name(), disabledRules); + String sql = String.format("select /*+ %s, NL_JOIN */ t3.* from TBL1 t1 JOIN TBL3 t3 on t1.v1=t3.v3 JOIN TBL2 t2 on " + + "t2.v2=t1.v1", HintDefinition.ENFORCE_JOIN_ORDER.name()); // Tests the plan has no commuted join inputs. assertPlan(sql, schema, nodeOrAnyChild(isInstanceOf(Join.class) diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/hints/JoinTypeHintPlannerTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/hints/JoinTypeHintPlannerTest.java new file mode 100644 index 0000000000000..06e6b4e25cc1a --- /dev/null +++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/hints/JoinTypeHintPlannerTest.java @@ -0,0 +1,525 @@ +/* + * 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.planner.hints; + +import java.util.Arrays; +import java.util.function.Predicate; +import org.apache.calcite.rel.RelNode; +import org.apache.ignite.internal.processors.query.QueryUtils; +import org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition; +import org.apache.ignite.internal.processors.query.calcite.planner.AbstractPlannerTest; +import org.apache.ignite.internal.processors.query.calcite.planner.TestTable; +import org.apache.ignite.internal.processors.query.calcite.rel.AbstractIgniteJoin; +import org.apache.ignite.internal.processors.query.calcite.rel.IgniteCorrelatedNestedLoopJoin; +import org.apache.ignite.internal.processors.query.calcite.rel.IgniteMergeJoin; +import org.apache.ignite.internal.processors.query.calcite.rel.IgniteNestedLoopJoin; +import org.apache.ignite.internal.processors.query.calcite.schema.IgniteSchema; +import org.apache.ignite.internal.processors.query.calcite.trait.IgniteDistributions; +import org.apache.ignite.testframework.LogListener; +import org.apache.ignite.testframework.junits.logger.GridTestLog4jLogger; +import org.apache.logging.log4j.Level; +import org.junit.Test; + +import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.CNL_JOIN; +import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.MERGE_JOIN; +import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.NL_JOIN; +import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.NO_CNL_JOIN; +import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.NO_MERGE_JOIN; +import static org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.NO_NL_JOIN; + +/** + * Planner test for index hints. + */ +public class JoinTypeHintPlannerTest extends AbstractPlannerTest { + /** */ + private static final String[] CORE_JOIN_REORDER_RULES = {"JoinCommuteRule", "JoinPushThroughJoinRule:left", + "JoinPushThroughJoinRule:right"}; + + /** */ + private IgniteSchema schema; + + /** {@inheritDoc} */ + @Override protected void afterTest() throws Exception { + super.afterTest(); + + ((GridTestLog4jLogger)log).setLevel(Level.INFO); + } + + /** {@inheritDoc} */ + @Override public void setup() { + super.setup(); + + TestTable[] tables = new TestTable[5]; + + for (int i = 0; i < tables.length; ++i) { + tables[i] = createTable("TBL" + (i + 1), (int)Math.min(1_000_000, Math.pow(10, i)), + IgniteDistributions.broadcast(), "ID", Integer.class, "V1", Integer.class, "V2", Integer.class, + "V3", Integer.class).addIndex(QueryUtils.PRIMARY_KEY_INDEX, 0); + } + + schema = createSchema(tables); + } + + /** + * Tests nested loop join is disabled by hint. + */ + @Test + public void testDisableCNLJoin() throws Exception { + for (HintDefinition hint : Arrays.asList(NO_CNL_JOIN, NL_JOIN, MERGE_JOIN)) { + doTestDisableJoinTypeWith("TBL1", "TBL5", "INNER", IgniteCorrelatedNestedLoopJoin.class, + hint); + + doTestDisableJoinTypeWith("TBL1", "TBL5", "LEFT", IgniteCorrelatedNestedLoopJoin.class, + hint); + } + } + + /** */ + @Test + public void testHintsErrors() throws Exception { + ((GridTestLog4jLogger)log).setLevel(Level.DEBUG); + + // Leading hint must disable inconsistent follower. + LogListener lsnr = LogListener.matches("Skipped hint '" + CNL_JOIN.name() + "'") + .andMatches("This join type is already disabled or forced to use before").build(); + + lsnrLog.registerListener(lsnr); + + physicalPlan("SELECT /*+ " + NO_CNL_JOIN + ',' + CNL_JOIN + " */ t1.v1, t2.v2 FROM TBL1 t1 JOIN " + + "TBL2 t2 on t1.v3=t2.v3", schema); + + assertTrue(lsnr.check()); + + // Wrong table name must not affect next hint. + lsnrLog.clearListeners(); + + lsnr = LogListener.matches("Skipped hint '" + MERGE_JOIN.name() + "'").build(); + + lsnrLog.registerListener(lsnr); + + physicalPlan("SELECT /*+ " + NL_JOIN + "(UNEXISTING), " + MERGE_JOIN + "(TBL2) */ t1.v1, t2.v2 FROM " + + "TBL1 t1 JOIN TBL2 t2 on t1.v3=t2.v3", schema); + + assertTrue(!lsnr.check()); + + // Following hint must not override leading. + lsnrLog.clearListeners(); + + lsnr = LogListener.matches("Skipped hint '" + NL_JOIN.name() + "'") + .andMatches("This join type is already disabled or forced to use before").build(); + + lsnrLog.registerListener(lsnr); + + physicalPlan("SELECT /*+ " + MERGE_JOIN + "(TBL1)," + NL_JOIN + "(TBL1,TBL2) */ t1.v1, t2.v2 FROM TBL1 " + + "t1 JOIN TBL2 t2 on t1.v3=t2.v3", schema); + + assertTrue(lsnr.check()); + + // Inner hint must override heading. Second inner hint must not override first inner hint. + lsnrLog.clearListeners(); + + lsnr = LogListener.matches("Skipped hint '" + CNL_JOIN.name() + "' with options 'TBL1','TBL3'") + .andMatches("Skipped hint '" + NL_JOIN.name() + "' with options 'TBL1'").build(); + + lsnrLog.registerListener(lsnr); + + physicalPlan("SELECT /*+ " + NL_JOIN + "(TBL1) */ t1.v1, t2.v2 FROM TBL1 " + + "t1 JOIN TBL2 t2 on t1.v3=t2.v3 where t2.v1 in " + + "(SELECT /*+ " + MERGE_JOIN + "(TBL1), " + CNL_JOIN + "(TBL1,TBL3) */ t3.v3 from TBL3 t3 JOIN TBL1 t4 " + + "on t3.v2=t4.v2)", schema, CORE_JOIN_REORDER_RULES); + + assertTrue(lsnr.check()); + + // The same params should hot be warned. + lsnrLog.clearListeners(); + + lsnr = LogListener.matches("Skipped hint").build(); + + lsnrLog.registerListener(lsnr); + + physicalPlan("SELECT /*+ " + NL_JOIN + "(TBL1, TBL1, TBL2, TBL2, UNEXISTING) */ t1.v1, t2.v2 FROM TBL1 " + + "t1 JOIN TBL2 t2 on t1.v3=t2.v3", schema); + + assertTrue(!lsnr.check()); + + lsnrLog.clearListeners(); + + lsnr = LogListener.matches("Skipped hint").build(); + + lsnrLog.registerListener(lsnr); + + physicalPlan("SELECT /*+ " + MERGE_JOIN + "(TBL1)," + MERGE_JOIN + "(TBL1,TBL2,UNEXISTING) */ t1.v1, " + + "t2.v2 FROM TBL1 t1 JOIN TBL2 t2 on t1.v3=t2.v3", schema); + + assertTrue(!lsnr.check()); + } + + /** + * Tests nested loop join is disabled by hints. + */ + @Test + public void testDisableNLJoin() throws Exception { + for (HintDefinition hint : Arrays.asList(NO_NL_JOIN, CNL_JOIN, MERGE_JOIN)) { + doTestDisableJoinTypeWith("TBL5", "TBL4", "INNER", IgniteNestedLoopJoin.class, + NO_NL_JOIN, "MergeJoinConverter"); + + doTestDisableJoinTypeWith("TBL3", "TBL1", "LEFT", IgniteNestedLoopJoin.class, + NO_NL_JOIN); + + // Correlated nested loop join supports only INNER and LEFT join types. + if (hint != CNL_JOIN) { + doTestDisableJoinTypeWith("TBL1", "TBL3", "RIGHT", IgniteNestedLoopJoin.class, + NO_NL_JOIN); + + doTestDisableJoinTypeWith("TBL1", "TBL2", "FULL", IgniteNestedLoopJoin.class, + NO_NL_JOIN); + } + } + } + + /** + * Tests merge join is disabled by hints. + */ + @Test + public void testDisableMergeJoin() throws Exception { + for (HintDefinition hint : Arrays.asList(NO_MERGE_JOIN, NL_JOIN, CNL_JOIN)) { + doTestDisableJoinTypeWith("TBL4", "TBL2", "INNER", IgniteMergeJoin.class, hint); + + doTestDisableJoinTypeWith("TBL4", "TBL2", "LEFT", IgniteMergeJoin.class, hint); + + // Correlated nested loop join supports only INNER and LEFT join types. + if (hint != CNL_JOIN) { + doTestDisableJoinTypeWith("TBL4", "TBL2", "RIGHT", IgniteMergeJoin.class, hint); + + doTestDisableJoinTypeWith("TBL4", "TBL2", "FULL", IgniteMergeJoin.class, hint); + } + } + } + + /** + * Tests the merge join is enabled by the hint instead of the other joins. + */ + @Test + public void testMergeJoinEnabled() throws Exception { + doTestCertainJoinTypeEnabled("TBL1", "INNER", "TBL2", IgniteCorrelatedNestedLoopJoin.class, + MERGE_JOIN, IgniteMergeJoin.class); + + doTestCertainJoinTypeEnabled("TBL1", "RIGHT", "TBL2", IgniteNestedLoopJoin.class, + MERGE_JOIN, IgniteMergeJoin.class); + + doTestCertainJoinTypeEnabled("TBL1", "INNER", "TBL2", IgniteCorrelatedNestedLoopJoin.class, + MERGE_JOIN, IgniteMergeJoin.class, CORE_JOIN_REORDER_RULES); + + doTestCertainJoinTypeEnabled("TBL1", "RIGHT", "TBL2", IgniteNestedLoopJoin.class, + MERGE_JOIN, IgniteMergeJoin.class, CORE_JOIN_REORDER_RULES); + } + + /** + * Tests the nested loop join is enabled by the hint instead of the other joins. + */ + @Test + public void testNLJoinEnabled() throws Exception { + doTestCertainJoinTypeEnabled("TBL2", "INNER", "TBL1", IgniteCorrelatedNestedLoopJoin.class, + NL_JOIN, IgniteNestedLoopJoin.class); + + doTestCertainJoinTypeEnabled("TBL5", "INNER", "TBL4", IgniteMergeJoin.class, + NL_JOIN, IgniteNestedLoopJoin.class); + + doTestCertainJoinTypeEnabled("TBL1", "LEFT", "TBL2", IgniteCorrelatedNestedLoopJoin.class, + NL_JOIN, IgniteNestedLoopJoin.class, CORE_JOIN_REORDER_RULES); + + doTestCertainJoinTypeEnabled("TBL5", "INNER", "TBL4", IgniteMergeJoin.class, + NL_JOIN, IgniteNestedLoopJoin.class, CORE_JOIN_REORDER_RULES); + } + + /** */ + @Test + public void testSelfJoin() throws Exception { + String sqlTpl = "SELECT %s t1.v1, t2.v2 FROM TBL1 t1 JOIN TBL1 t2 on t1.v3=t2.v2 where t2.v3=4"; + + assertPlan(String.format(sqlTpl, "/*+ " + MERGE_JOIN + "(TBL1) */"), schema, + nodeOrAnyChild(isInstanceOf(IgniteCorrelatedNestedLoopJoin.class).negate()) + .and(nodeOrAnyChild(isInstanceOf(IgniteMergeJoin.class).and(hasNestedTableScan("TBL1"))))); + + assertPlan(String.format(sqlTpl, "/*+ " + NO_CNL_JOIN + "(TBL1) */"), schema, + nodeOrAnyChild(isInstanceOf(IgniteCorrelatedNestedLoopJoin.class).negate()) + .and(nodeOrAnyChild(isInstanceOf(IgniteNestedLoopJoin.class).and(hasNestedTableScan("TBL1"))))); + } + + /** + * Tests the correlated nested loop join is enabled by the hint instead of the other joins. + */ + @Test + public void testCNLJoinEnabled() throws Exception { + doTestCertainJoinTypeEnabled("TBL2", "LEFT", "TBL1", IgniteNestedLoopJoin.class, + CNL_JOIN, IgniteCorrelatedNestedLoopJoin.class); + + doTestCertainJoinTypeEnabled("TBL5", "INNER", "TBL4", IgniteMergeJoin.class, + CNL_JOIN, IgniteCorrelatedNestedLoopJoin.class); + + // Even CNL join doesn't support RIGHT join, join type and join inputs might be switched by Calcite. + doTestCertainJoinTypeEnabled("TBL1", "RIGHT", "TBL2", IgniteNestedLoopJoin.class, + CNL_JOIN, IgniteCorrelatedNestedLoopJoin.class); + + doTestCertainJoinTypeEnabled("TBL2", "LEFT", "TBL1", IgniteNestedLoopJoin.class, + CNL_JOIN, IgniteCorrelatedNestedLoopJoin.class, CORE_JOIN_REORDER_RULES); + + doTestCertainJoinTypeEnabled("TBL5", "INNER", "TBL4", IgniteMergeJoin.class, + CNL_JOIN, IgniteCorrelatedNestedLoopJoin.class, CORE_JOIN_REORDER_RULES); + } + + /** + * Checks {@code expectedJoin} switches to {@code newJoin} by {@code hint} in the simple query + * 'SELECT /*+ {@code hint} */ t1.v1, t2.v2 FROM {@code tbl1} t1 {@code joinType} JOIN {@code tbl2} t2 on + * t1.v3=t2.v3'. + */ + private void doTestCertainJoinTypeEnabled( + String tbl1, + String joinType, + String tbl2, + Class expectedJoin, + HintDefinition hint, + Class newJoin, + String... disabledRules + ) throws Exception { + String sqlTpl = String.format("SELECT %%s t1.v1, t2.v2 FROM %s t1 %s JOIN %s t2 on t1.v3=t2.v3", tbl1, + joinType, tbl2); + + // Not using table name. + assertPlan(String.format(sqlTpl, "/*+ " + hint.name() + "(UNEXISTING) */"), schema, + nodeOrAnyChild(isInstanceOf(expectedJoin).and(hasNestedTableScan(tbl1)) + .and(hasNestedTableScan(tbl2))).and(nodeOrAnyChild(isInstanceOf(newJoin)).negate()), disabledRules); + + for (String t : Arrays.asList("", tbl1, tbl2)) { + assertPlan(String.format(sqlTpl, "/*+ " + hint.name() + "(" + t + ") */"), schema, + nodeOrAnyChild(isInstanceOf(expectedJoin).negate()) + .and(nodeOrAnyChild(isInstanceOf(newJoin).and(hasNestedTableScan(tbl1) + .and(hasNestedTableScan(tbl2))))), disabledRules); + + if (t.isEmpty()) + continue; + + // Wrong tbl names must not affect. + assertPlan(String.format(sqlTpl, "/*+ " + hint.name() + "(UNEXISTING)," + hint.name() + "(UNEXISTING," + + t + ",UNEXISTING) */"), schema, nodeOrAnyChild(isInstanceOf(expectedJoin).negate()) + .and(nodeOrAnyChild(isInstanceOf(newJoin).and(hasNestedTableScan(tbl1) + .and(hasNestedTableScan(tbl2))))), disabledRules); + } + } + + /** */ + private void doTestDisableJoinTypeWith( + String tbl1, + String tbl2, + String sqlJoinType, + Class joinRel, + HintDefinition hint, + String... disabledRules + ) throws Exception { + String sqlTpl = String.format("SELECT %%s t1.v1, t2.v2 FROM %s t1 %s JOIN %s t2 on t1.v3=t2.v3", tbl1, + sqlJoinType, tbl2); + + String hintPref = "/*+ " + hint.name(); + + // Hint with no options. + assertPlan(String.format(sqlTpl, hintPref + " */"), schema, nodeOrAnyChild(isInstanceOf(joinRel)).negate(), + disabledRules); + + // Hint with tbl1. + assertPlan(String.format(sqlTpl, hintPref + '(' + tbl1 + " ) */"), schema, + nodeOrAnyChild(isInstanceOf(joinRel)).negate(), disabledRules); + + // Hint with tbl2. + assertPlan(String.format(sqlTpl, hintPref + '(' + tbl2 + " ) */"), schema, + nodeOrAnyChild(isInstanceOf(joinRel)).negate(), disabledRules); + + // Hint with both tables. + assertPlan(String.format(sqlTpl, hintPref + '(' + tbl2 + ',' + tbl1 + " ) */"), schema, + nodeOrAnyChild(isInstanceOf(joinRel)).negate(), disabledRules); + + // Hint with wrong table. + assertPlan(String.format(sqlTpl, hintPref + "('UNEXISTING') */"), schema, + nodeOrAnyChild(isInstanceOf(joinRel)), disabledRules); + + // Hint with correct and incorrect tbl. + assertPlan(String.format(sqlTpl, hintPref + '(' + tbl1 + ",UNEXISTING) */"), schema, + nodeOrAnyChild(isInstanceOf(joinRel)).negate(), disabledRules); + } + + /** + * Tests disable-join-hint works for a sub-query. + */ + @Test + public void testDisableJoinTypeInSubquery() throws Exception { + String sqlTpl = "SELECT %s t1.v1, t2.v2 FROM TBL2 t1 JOIN TBL1 t2 on t1.v3=t2.v3 where t2.v3 in " + + "(select %s t3.v3 from TBL3 t3 JOIN TBL4 t4 on t3.v1=t4.v1)"; + + for (String tbl : Arrays.asList("TBL3", "TBL4")) { + assertPlan(String.format(sqlTpl, "/*+ " + NO_MERGE_JOIN + "(" + tbl + ") */", ""), schema, + nodeOrAnyChild(isInstanceOf(IgniteMergeJoin.class) + .and(input(0, noJoinChildren())) + .and(input(1, noJoinChildren())) + .and(hasNestedTableScan(tbl)) + ).negate(), CORE_JOIN_REORDER_RULES); + + // Hint in the sub-query. + assertPlan(String.format(sqlTpl, "", "/*+ " + NO_MERGE_JOIN + "(" + tbl + ") */"), schema, + nodeOrAnyChild(isInstanceOf(IgniteMergeJoin.class) + .and(input(0, noJoinChildren())) + .and(input(1, noJoinChildren())) + .and(hasNestedTableScan(tbl)) + ).negate(), CORE_JOIN_REORDER_RULES); + + // Also NO-NL-JOIN hint in the sub-query. Must not affect the parent query. + assertPlan(String.format(sqlTpl, "", "/*+ " + NO_MERGE_JOIN + ',' + NO_NL_JOIN + " */"), schema, + nodeOrAnyChild(isInstanceOf(IgniteNestedLoopJoin.class) + .and(input(0, noJoinChildren())) + .and(input(1, noJoinChildren())) + .and(hasNestedTableScan("TBL2")) + .and(hasNestedTableScan("TBL1")) + ).and(nodeOrAnyChild(isInstanceOf(IgniteMergeJoin.class) + .and(input(0, noJoinChildren())) + .and(input(1, noJoinChildren())) + .and(hasNestedTableScan(tbl)) + ).negate()), CORE_JOIN_REORDER_RULES); + } + } + + /** */ + @Test + public void testNestedHintOverrides() throws Exception { + assertPlan("SELECT /*+ " + MERGE_JOIN + "(TBL1) */ t1.v1, t2.v2 FROM TBL1 t1 JOIN TBL2 t2 on t1.v3=t2.v3 " + + "where t2.v1 in (SELECT t3.v3 from TBL3 t3 JOIN TBL1 t4 on t3.v2=t4.v2)", schema, + nodeOrAnyChild(isInstanceOf(IgniteNestedLoopJoin.class)).negate() + .and(nodeOrAnyChild(isInstanceOf(IgniteMergeJoin.class))), CORE_JOIN_REORDER_RULES); + + assertPlan("SELECT /*+ " + MERGE_JOIN + "(TBL1) */ t1.v1, t2.v2 FROM TBL1 " + + "t1 JOIN TBL2 t2 on t1.v3=t2.v3 where t2.v1 in " + + "(SELECT /*+ " + CNL_JOIN + "(TBL1) */ t3.v3 from TBL3 t3 JOIN TBL1 t4 on t3.v2=t4.v2)", schema, + nodeOrAnyChild(isInstanceOf(IgniteMergeJoin.class).and(hasNestedTableScan("TBL1"))) + .and(nodeOrAnyChild(isInstanceOf(IgniteCorrelatedNestedLoopJoin.class) + .and(hasNestedTableScan("TBL3")).and(hasNestedTableScan("TBL1")))), CORE_JOIN_REORDER_RULES); + } + + /** + * Tests that several disable join types are allowed. + */ + @Test + public void testSeveralDisables() throws Exception { + String sqlTpl = "SELECT %s t1.v1, t2.v2 FROM TBL1 t1, TBL2 t2 where t1.v3=t2.v3"; + + assertPlan(String.format(sqlTpl, "/*+ " + NO_CNL_JOIN + ',' + NO_NL_JOIN + " */"), schema, + nodeOrAnyChild(isInstanceOf(IgniteCorrelatedNestedLoopJoin.class)).negate() + .and(nodeOrAnyChild(isInstanceOf(IgniteNestedLoopJoin.class)).negate()) + .and(nodeOrAnyChild(isInstanceOf(IgniteMergeJoin.class).and(hasNestedTableScan("TBL1")) + .and(hasNestedTableScan("TBL2")))), CORE_JOIN_REORDER_RULES); + + assertPlan(String.format(sqlTpl, "/*+ " + NO_CNL_JOIN + "(TBL1)," + NO_NL_JOIN + "(TBL2) */"), schema, + nodeOrAnyChild(isInstanceOf(IgniteCorrelatedNestedLoopJoin.class)).negate() + .and(nodeOrAnyChild(isInstanceOf(IgniteNestedLoopJoin.class)).negate()) + .and(nodeOrAnyChild(isInstanceOf(IgniteMergeJoin.class).and(hasNestedTableScan("TBL1")) + .and(hasNestedTableScan("TBL2")))), CORE_JOIN_REORDER_RULES); + + // Check with forcing in the middle. + assertPlan(String.format(sqlTpl, "/*+ " + NO_CNL_JOIN + ',' + NL_JOIN + ',' + NO_NL_JOIN + " */"), schema, + nodeOrAnyChild(isInstanceOf(IgniteCorrelatedNestedLoopJoin.class)).negate() + .and(nodeOrAnyChild(isInstanceOf(IgniteNestedLoopJoin.class).and(hasNestedTableScan("TBL1")) + .and(hasNestedTableScan("TBL2")))), CORE_JOIN_REORDER_RULES); + + // Check with forcing in the middle with the table name. + assertPlan(String.format(sqlTpl, "/*+ " + NO_CNL_JOIN + "(TBL1)," + NL_JOIN + "(TBl1)," + NO_NL_JOIN + " */"), + schema, nodeOrAnyChild(isInstanceOf(IgniteCorrelatedNestedLoopJoin.class)).negate() + .and(nodeOrAnyChild(isInstanceOf(IgniteNestedLoopJoin.class).and(hasNestedTableScan("TBL1")) + .and(hasNestedTableScan("TBL2")))), CORE_JOIN_REORDER_RULES); + + // Wrong tbl name. + assertPlan(String.format(sqlTpl, "/*+ " + NO_CNL_JOIN + ',' + NO_NL_JOIN + "(UNEXISTING) */"), schema, + nodeOrAnyChild(isInstanceOf(IgniteCorrelatedNestedLoopJoin.class)).negate() + .and(nodeOrAnyChild(isInstanceOf(IgniteNestedLoopJoin.class).and(hasNestedTableScan("TBL1")) + .and(hasNestedTableScan("TBL2")))), CORE_JOIN_REORDER_RULES); + + // Disabling of all joins is prohibited. Last merge must work. + assertPlan(String.format(sqlTpl, "/*+ " + NO_CNL_JOIN + ',' + NO_NL_JOIN + ',' + NO_MERGE_JOIN + " */"), schema, + nodeOrAnyChild(isInstanceOf(IgniteCorrelatedNestedLoopJoin.class)).negate() + .and(nodeOrAnyChild(isInstanceOf(IgniteNestedLoopJoin.class)).negate()) + .and(nodeOrAnyChild(isInstanceOf(IgniteMergeJoin.class).and(hasNestedTableScan("TBL1")) + .and(hasNestedTableScan("TBL2")))), CORE_JOIN_REORDER_RULES); + + // Check many duplicated disables doesn't erase other disables. + sqlTpl = "SELECT %s t1.v1, t2.v2, t3.v3, t4.v1 FROM TBL1 t1, TBL2 t2, TBL3 t3, TBL4 t4 where " + + "t1.v1=t2.v1 and t1.v2=t2.v2 and t1.v3=t3.v3 and t1.v1=t4.v1"; + + String hints = "/*+ " + NO_CNL_JOIN + ',' + NO_CNL_JOIN + "(TBL1), " + NO_CNL_JOIN + "(TBL1,TBL2), " + NO_CNL_JOIN + + "(TBL1,TBL4), " + NO_MERGE_JOIN + "(TBL1) */"; + + assertPlan(String.format(sqlTpl, hints), schema, + nodeOrAnyChild(isInstanceOf(IgniteMergeJoin.class).and(hasChildThat(isTableScan("TBL1")))).negate() + .and(nodeOrAnyChild(isInstanceOf(IgniteCorrelatedNestedLoopJoin.class) + .and(hasChildThat(isTableScan("TBL1")))).negate()), + CORE_JOIN_REORDER_RULES); + } + + /** */ + @Test + public void testWithAdditionalSelect() throws Exception { + String sqlTpl = "SELECT %s t1.v1, t2.v2 FROM TBL1 t1, (SELECT t3.v2, t3.v3 FROM TBL3 t3 where t3.v1=5) t2 " + + "where t1.v3=t2.v3"; + + assertPlan(String.format(sqlTpl, "/*+ " + MERGE_JOIN + "(TBL1) */"), schema, + nodeOrAnyChild(isInstanceOf(IgniteCorrelatedNestedLoopJoin.class)).negate() + .and(nodeOrAnyChild(isInstanceOf(IgniteMergeJoin.class).and(hasNestedTableScan("TBL3")) + .and(hasNestedTableScan("TBL1")))), CORE_JOIN_REORDER_RULES); + + assertPlan(String.format(sqlTpl, "/*+ " + MERGE_JOIN + "(TBL3) */"), schema, + nodeOrAnyChild(isInstanceOf(IgniteCorrelatedNestedLoopJoin.class)).negate() + .and(nodeOrAnyChild(isInstanceOf(IgniteMergeJoin.class).and(hasNestedTableScan("TBL3")) + .and(hasNestedTableScan("TBL1")))), CORE_JOIN_REORDER_RULES); + } + + /** */ + @Test + public void testDisableMergeJoinWith3Tables() throws Exception { + String sqlTpl = "SELECT %s t1.v1, t2.v2, t3.v3 FROM TBL3 t1 JOIN TBL4 t2 on t1.v1=t2.v2 JOIN TBL5 t3 on " + + "t2.v2=t3.v3"; + + assertPlan(String.format(sqlTpl, "/*+ " + NO_MERGE_JOIN + " */"), schema, + nodeOrAnyChild(isInstanceOf(AbstractIgniteJoin.class)) + .and(nodeOrAnyChild(isInstanceOf(AbstractIgniteJoin.class) + .and(j -> j instanceof IgniteMergeJoin)).negate())); + + for (String tbl : Arrays.asList("TBL3", "TBL4", "TBL5")) { + assertPlan(String.format(sqlTpl, "/*+ " + NO_MERGE_JOIN + "(" + tbl + ") */"), schema, + nodeOrAnyChild(isInstanceOf(IgniteMergeJoin.class) + .and(input(0, noJoinChildren())) + .and(input(1, noJoinChildren())) + .and(hasNestedTableScan(tbl)) + ).negate()); + } + } + + /** */ + private Predicate hasNestedTableScan(String tbl) { + return input(0, nodeOrAnyChild(isTableScan(tbl))) + .or(input(1, nodeOrAnyChild(isTableScan(tbl)))); + } + + /** */ + private Predicate noJoinChildren() { + return nodeOrAnyChild(isInstanceOf(AbstractIgniteJoin.class)).negate(); + } +} diff --git a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/rules/JoinCommuteRulesTest.java b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/rules/JoinCommuteRulesTest.java index 7d076eadc93af..8292f99749d10 100644 --- a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/rules/JoinCommuteRulesTest.java +++ b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/rules/JoinCommuteRulesTest.java @@ -60,8 +60,7 @@ public class JoinCommuteRulesTest extends GridCommonAbstractTest { /** */ @Test public void testCommuteOuter() { - String sql = "SELECT /*+ DISABLE_RULE('CorrelatedNestedLoopJoin', 'MergeJoinConverter') */ " + - "COUNT(*) FROM SMALL s RIGHT JOIN HUGE h on h.id = s.id"; + String sql = "SELECT /*+ NL_JOIN */ COUNT(*) FROM SMALL s RIGHT JOIN HUGE h on h.id = s.id"; checkQuery(sql) .matches(containsTableScan("PUBLIC", "HUGE")) @@ -69,7 +68,7 @@ public void testCommuteOuter() { .matches(containsSubPlan("IgniteNestedLoopJoin(condition=[=($0, $1)], joinType=[left]")) .check(); - sql = "SELECT /*+ DISABLE_RULE('CorrelatedNestedLoopJoin', 'MergeJoinConverter', 'JoinCommuteRule') */ " + + sql = "SELECT /*+ NL_JOIN, DISABLE_RULE('JoinCommuteRule') */ " + "COUNT(*) FROM SMALL s RIGHT JOIN HUGE h on h.id = s.id"; checkQuery(sql) @@ -82,8 +81,7 @@ public void testCommuteOuter() { /** */ @Test public void testCommuteInner() { - String sql = "SELECT /*+ DISABLE_RULE('CorrelatedNestedLoopJoin', 'MergeJoinConverter') */ " + - "COUNT(*) FROM SMALL s JOIN HUGE h on h.id = s.id"; + String sql = "SELECT /*+ NL_JOIN */ COUNT(*) FROM SMALL s JOIN HUGE h on h.id = s.id"; checkQuery(sql) .matches(containsTableScan("PUBLIC", "HUGE")) @@ -91,8 +89,7 @@ public void testCommuteInner() { .matches(containsSubPlan("IgniteNestedLoopJoin(condition=[=($0, $1)], joinType=[inner]")) .check(); - sql = "SELECT /*+ DISABLE_RULE('CorrelatedNestedLoopJoin', 'MergeJoinConverter', 'JoinCommuteRule') */ " + - "COUNT(*) FROM SMALL s JOIN HUGE h on h.id = s.id"; + sql = "SELECT /*+ NL_JOIN, DISABLE_RULE('JoinCommuteRule') */ COUNT(*) FROM SMALL s JOIN HUGE h on h.id = s.id"; checkQuery(sql) .matches(containsTableScan("PUBLIC", "HUGE")) diff --git a/modules/calcite/src/test/sql/sqlite/join/join1.test b/modules/calcite/src/test/sql/sqlite/join/join1.test index 5fcd4d1507cae..d1f33fa306274 100644 --- a/modules/calcite/src/test/sql/sqlite/join/join1.test +++ b/modules/calcite/src/test/sql/sqlite/join/join1.test @@ -24,19 +24,19 @@ statement ok INSERT INTO t2 VALUES(3,4,5); query III rowsort -SELECT /*+ DISABLE_RULE('NestedLoopJoinConverter', 'MergeJoinConverter') */ t2.* FROM t2 NATURAL JOIN t1; +SELECT /*+ CNL_JOIN */ t2.* FROM t2 NATURAL JOIN t1; ---- 2 3 4 3 4 5 query III rowsort -SELECT /*+ DISABLE_RULE('CorrelatedNestedLoopJoin', 'MergeJoinConverter') */ t2.* FROM t2 NATURAL JOIN t1; +SELECT /*+ CNL_JOIN */ t2.* FROM t2 NATURAL JOIN t1; ---- 2 3 4 3 4 5 query III rowsort -SELECT /*+ DISABLE_RULE('CorrelatedNestedLoopJoin', 'NestedLoopJoinConverter') */ t2.* FROM t2 NATURAL JOIN t1; +SELECT /*+ MERGE_JOIN */ t2.* FROM t2 NATURAL JOIN t1; ---- 2 3 4 3 4 5 diff --git a/modules/calcite/src/test/sql/sqlite/join/join1.test_ignore b/modules/calcite/src/test/sql/sqlite/join/join1.test_ignore index 3a8cd0bf52e8d..55572548b5ed5 100644 --- a/modules/calcite/src/test/sql/sqlite/join/join1.test_ignore +++ b/modules/calcite/src/test/sql/sqlite/join/join1.test_ignore @@ -25,19 +25,19 @@ statement ok INSERT INTO t2 VALUES(3,4,5); query III rowsort -SELECT /*+ DISABLE_RULE('NestedLoopJoinConverter', 'MergeJoinConverter') */ t2.* FROM t2 NATURAL JOIN t1; +SELECT /*+ CNL_JOIN */ t2.* FROM t2 NATURAL JOIN t1; ---- 2 3 4 3 4 5 query III rowsort -SELECT /*+ DISABLE_RULE('CorrelatedNestedLoopJoin', 'MergeJoinConverter') */ t2.* FROM t2 NATURAL JOIN t1; +SELECT /*+ NL_JOIN */ t2.* FROM t2 NATURAL JOIN t1; ---- 2 3 4 3 4 5 query III rowsort -SELECT /*+ DISABLE_RULE('CorrelatedNestedLoopJoin', 'NestedLoopJoinConverter') */ t2.* FROM t2 NATURAL JOIN t1; +SELECT /*+ MERGE_JOIN */ t2.* FROM t2 NATURAL JOIN t1; ---- 2 3 4 3 4 5