This is an automated email from the ASF dual-hosted git repository.
alexpl pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ignite.git
The following commit(s) were added to refs/heads/master by this push:
new 5d22d1dd53b IGNITE-24624 SQL Calcite: Add heuristics to optimize join
order - Fixes #11935.
5d22d1dd53b is described below
commit 5d22d1dd53bf8b4b2404e78593eccc800a85e8c6
Author: Vladimir Steshin <[email protected]>
AuthorDate: Tue Apr 8 09:37:07 2025 +0300
IGNITE-24624 SQL Calcite: Add heuristics to optimize join order - Fixes
#11935.
Signed-off-by: Aleksey Plekhanov <[email protected]>
---
.../query/calcite/hint/HintDefinition.java | 8 +-
.../query/calcite/prepare/IgnitePlanner.java | 6 +
.../query/calcite/prepare/IgnitePrograms.java | 12 +-
.../query/calcite/prepare/PlannerHelper.java | 236 ++++++++++-
.../query/calcite/prepare/PlannerPhase.java | 22 +
.../rule/logical/IgniteMultiJoinOptimizeRule.java | 446 +++++++++++++++++++++
.../processors/query/calcite/QueryChecker.java | 32 +-
.../query/calcite/RuleApplyListener.java | 76 ++++
.../integration/AbstractBasicIntegrationTest.java | 28 ++
.../query/calcite/planner/AbstractPlannerTest.java | 60 ++-
.../calcite/planner/JoinCommutePlannerTest.java | 132 +++---
.../planner/hints/JoinTypeHintPlannerTest.java | 168 +++++++-
.../calcite/rules/JoinOrderOptimizationTest.java | 234 +++++++++++
.../ignite/testsuites/IntegrationTestSuite.java | 2 +
14 files changed, 1392 insertions(+), 70 deletions(-)
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 949c2990bbf..a32c028482a 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
@@ -25,6 +25,7 @@ import org.apache.calcite.rel.hint.HintPredicate;
import org.apache.calcite.rel.hint.HintPredicates;
import org.apache.calcite.rel.rules.CoreRules;
import org.apache.calcite.rel.rules.JoinPushThroughJoinRule;
+import
org.apache.ignite.internal.processors.query.calcite.rule.logical.IgniteMultiJoinOptimizeRule;
/**
* Holds supported SQL hints and their settings.
@@ -67,7 +68,12 @@ public enum HintDefinition {
/** {@inheritDoc} */
@Override public Collection<RelOptRule> disabledRules() {
// CoreRules#JOIN_COMMUTE also disables
CoreRules.JOIN_COMMUTE_OUTER.
- return Arrays.asList(CoreRules.JOIN_COMMUTE,
JoinPushThroughJoinRule.LEFT, JoinPushThroughJoinRule.RIGHT);
+ // Disabling JoinToMultiJoinRule also disables
IgniteMultiJoinOptimizeRule because it won't
+ // receive a MultiJoin. Disabling IgniteMultiJoinOptimizeRule in
this way doesn't actually work because
+ // MultiJoin, unfortunately, is not a Hintable and
HintStrategyTable ignores it. We keep it here as slight
+ // optimization, to remove more unnecessary rules from the
planning.
+ return Arrays.asList(CoreRules.JOIN_COMMUTE,
JoinPushThroughJoinRule.LEFT, JoinPushThroughJoinRule.RIGHT,
+ CoreRules.JOIN_TO_MULTI_JOIN,
IgniteMultiJoinOptimizeRule.INSTANCE);
}
},
diff --git
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgnitePlanner.java
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgnitePlanner.java
index a9f80e7464c..376220ae426 100644
---
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgnitePlanner.java
+++
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgnitePlanner.java
@@ -35,6 +35,7 @@ import org.apache.calcite.plan.RelOptCluster;
import org.apache.calcite.plan.RelOptCost;
import org.apache.calcite.plan.RelOptCostFactory;
import org.apache.calcite.plan.RelOptLattice;
+import org.apache.calcite.plan.RelOptListener;
import org.apache.calcite.plan.RelOptMaterialization;
import org.apache.calcite.plan.RelOptPlanner;
import org.apache.calcite.plan.RelOptRule;
@@ -395,6 +396,11 @@ public class IgnitePlanner implements Planner,
RelOptTable.ViewExpander {
for (RelTraitDef<?> def : traitDefs)
this.planner.addRelTraitDef(def);
+
+ RelOptListener planLsnr = ctx.unwrap(RelOptListener.class);
+
+ if (planLsnr != null)
+ planner.addListener(planLsnr);
}
return planner;
diff --git
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgnitePrograms.java
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgnitePrograms.java
index 838fea05657..c4e81e0f08e 100644
---
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgnitePrograms.java
+++
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgnitePrograms.java
@@ -21,9 +21,11 @@ import java.util.ArrayList;
import java.util.List;
import org.apache.calcite.plan.RelOptLattice;
+import org.apache.calcite.plan.RelOptListener;
import org.apache.calcite.plan.RelOptMaterialization;
import org.apache.calcite.plan.RelOptRule;
import org.apache.calcite.plan.hep.HepPlanner;
+import org.apache.calcite.plan.hep.HepProgram;
import org.apache.calcite.plan.hep.HepProgramBuilder;
import org.apache.calcite.tools.Program;
import org.apache.calcite.tools.Programs;
@@ -40,11 +42,14 @@ public class IgnitePrograms {
* @param rules Rules.
* @return New program.
*/
- public static Program hep(RuleSet rules) {
+ public static Program hep(RuleSet rules, HepProgram... subProgram) {
return (planner, rel, traits, materializations, lattices) -> {
final HepProgramBuilder builder = new HepProgramBuilder();
final List<RelOptRule> ruleList = new ArrayList<>();
+ for (HepProgram sub : subProgram)
+ builder.addSubprogram(sub);
+
for (RelOptRule rule : rules)
ruleList.add(rule);
@@ -53,6 +58,11 @@ public class IgnitePrograms {
final HepPlanner hepPlanner = new HepPlanner(builder.build(),
Commons.context(rel), true,
null, Commons.context(rel).config().getCostFactory());
+ RelOptListener relOptListener =
planner.getContext().unwrap(RelOptListener.class);
+
+ if (relOptListener != null)
+ hepPlanner.addListener(relOptListener);
+
hepPlanner.setExecutor(planner.getExecutor());
for (RelOptMaterialization materialization : materializations)
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 285c2e43d71..2f3cde82aee 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
@@ -17,19 +17,25 @@
package org.apache.ignite.internal.processors.query.calcite.prepare;
+import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
+import java.util.Deque;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.google.common.collect.ImmutableSet;
+import org.apache.calcite.plan.RelOptRule;
import org.apache.calcite.plan.RelOptUtil;
import org.apache.calcite.plan.RelTraitSet;
import org.apache.calcite.rel.RelCollations;
+import org.apache.calcite.rel.RelHomogeneousShuttle;
import org.apache.calcite.rel.RelNode;
import org.apache.calcite.rel.RelRoot;
+import org.apache.calcite.rel.RelShuttle;
+import org.apache.calcite.rel.core.Join;
import org.apache.calcite.rel.core.SetOp;
import org.apache.calcite.rel.core.Spool;
import org.apache.calcite.rel.core.TableScan;
@@ -38,11 +44,14 @@ import org.apache.calcite.rel.hint.RelHint;
import org.apache.calcite.rel.rules.CoreRules;
import org.apache.calcite.rel.rules.JoinCommuteRule;
import org.apache.calcite.rel.rules.JoinPushThroughJoinRule;
+import org.apache.calcite.rel.rules.JoinToMultiJoinRule;
+import org.apache.calcite.rel.rules.MultiJoin;
import org.apache.calcite.rex.RexBuilder;
import org.apache.calcite.rex.RexNode;
import org.apache.calcite.sql.SqlKind;
import org.apache.calcite.sql.SqlNode;
import org.apache.calcite.util.Pair;
+import org.apache.calcite.util.Util;
import org.apache.ignite.IgniteLogger;
import org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition;
import org.apache.ignite.internal.processors.query.calcite.hint.HintUtils;
@@ -54,6 +63,7 @@ import
org.apache.ignite.internal.processors.query.calcite.rel.IgniteRel;
import
org.apache.ignite.internal.processors.query.calcite.rel.IgniteTableModify;
import org.apache.ignite.internal.processors.query.calcite.rel.IgniteTableScan;
import
org.apache.ignite.internal.processors.query.calcite.rel.IgniteTableSpool;
+import
org.apache.ignite.internal.processors.query.calcite.rule.logical.IgniteMultiJoinOptimizeRule;
import
org.apache.ignite.internal.processors.query.calcite.schema.ColumnDescriptor;
import org.apache.ignite.internal.processors.query.calcite.schema.IgniteTable;
import
org.apache.ignite.internal.processors.query.calcite.trait.IgniteDistributions;
@@ -74,6 +84,15 @@ public class PlannerHelper {
*/
public static final int MAX_JOINS_TO_COMMUTE_INPUTS = 5;
+ /**
+ * Mininal joins number to launch {@link IgniteMultiJoinOptimizeRule}.
Calcite's default join order optimization rules
+ * like {@link JoinCommuteRule} or {@link JoinPushThroughJoinRule} take
time but can give us better plans. They produce
+ * more join variants. And we estimate their physical costs. While the
joins count is small, let's use the default rules.
+ *
+ * @see #optimizeJoinsOrder(IgnitePlanner, RelNode, List)
+ */
+ public static final int JOINS_COUNT_FOR_HEURISTIC_ORDER = 3;
+
/**
* Default constructor.
*/
@@ -112,9 +131,15 @@ public class PlannerHelper {
rel = planner.transform(PlannerPhase.HEP_FILTER_PUSH_DOWN,
rel.getTraitSet(), rel);
+ // The following pushed down project can erase top-level hints. We
store them to reassign hints for join nodes.
+ // Clear the inherit pathes to consider the hints as not
propogated ones.
+ List<RelHint> topHints = HintUtils.allRelHints(rel).stream().map(h
-> h.inheritPath.isEmpty()
+ ? h
+ :
h.copy(Collections.emptyList())).collect(Collectors.toList());
+
rel = planner.transform(PlannerPhase.HEP_PROJECT_PUSH_DOWN,
rel.getTraitSet(), rel);
- fastenJoinsOrder(planner, rel);
+ rel = optimizeJoinsOrder(planner, rel, topHints);
RelTraitSet desired = rel.getCluster().traitSet()
.replace(IgniteConvention.INSTANCE)
@@ -150,8 +175,10 @@ public class PlannerHelper {
/**
* To prevent long join order planning, disables {@link JoinCommuteRule}
and/or {@link JoinPushThroughJoinRule} rules
* if the joins count reaches the thresholds.
+ *
+ * @return Original {@code rel}.
*/
- private static void fastenJoinsOrder(IgnitePlanner planner, RelNode rel) {
+ private static RelNode checkJoinsCommutes(IgnitePlanner planner, RelNode
rel) {
int joinsCnt = RelOptUtil.countJoins(rel);
if (joinsCnt > MAX_JOINS_TO_COMMUTE)
@@ -161,6 +188,179 @@ public class PlannerHelper {
planner.addDisabledRules(Arrays.asList(JoinPushThroughJoinRule.LEFT.toString(),
JoinPushThroughJoinRule.RIGHT.toString()));
}
+
+ return rel;
+ }
+
+ /**
+ * Tries to optimize joins order.
+ *
+ * @see JoinToMultiJoinRule
+ * @see IgniteMultiJoinOptimizeRule
+ *
+ * @return An node with optimized joins or original {@code root} if didn't
optimize.
+ */
+ private static RelNode optimizeJoinsOrder(IgnitePlanner planner, RelNode
root, List<RelHint> topLevelHints) {
+ List<Join> joins = findNodes(root, Join.class, false);
+
+ if (joins.isEmpty())
+ return checkJoinsCommutes(planner, root);
+
+ int disabledCnt = 0;
+
+ // If all the joins have the forced order, no need to optimize the
joins order at all.
+ for (RelNode join : joins) {
+ for (RelHint hint : ((Hintable)join).getHints()) {
+ if
(HintDefinition.ENFORCE_JOIN_ORDER.name().equals(hint.hintName)) {
+ ++disabledCnt;
+
+ break;
+ }
+ }
+ }
+
+ if (joins.size() - disabledCnt < JOINS_COUNT_FOR_HEURISTIC_ORDER)
+ return checkJoinsCommutes(planner, root);
+
+ RelNode res = planner.transform(PlannerPhase.HEP_OPTIMIZE_JOIN_ORDER,
root.getTraitSet(), root);
+
+ // Still has a MultiJoin, didn't manage to collect one flat join to
optimize.
+ if (!findNodes(res, MultiJoin.class, true).isEmpty())
+ return checkJoinsCommutes(planner, root);
+
+ // If a new joins order was proposed, no need to launch another join
order optimizations.
+
planner.addDisabledRules(HintDefinition.ENFORCE_JOIN_ORDER.disabledRules().stream().map(RelOptRule::toString)
+ .collect(Collectors.toSet()));
+
+ if (!topLevelHints.isEmpty()) {
+ res = actualTopLevelJoinTypeHints(res, topLevelHints,
joins.get(0));
+
+ restoreJoinTypeHints(res);
+ }
+
+ return res;
+ }
+
+ /** */
+ private static RelNode actualTopLevelJoinTypeHints(RelNode rel,
List<RelHint> topLevelHints, Join filterNode) {
+ assert rel instanceof Hintable;
+
+ // Ignore inheritance to compare hints type and options.
+ List<RelHint> filteredRelHints = ((Hintable)rel).getHints().stream()
+ .map(h -> h.inheritPath.isEmpty() ? h :
h.copy(Collections.emptyList())).collect(Collectors.toList());
+
+ List<RelHint> res = new ArrayList<>(topLevelHints.size());
+
+ for (RelHint topHint : topLevelHints) {
+ assert topHint.inheritPath.isEmpty();
+
+ boolean storeHint = true;
+
+ for (RelHint curHint : filteredRelHints) {
+ if (topHint.equals(curHint)) {
+ storeHint = false;
+
+ break;
+ }
+ }
+
+ if (storeHint)
+ res.add(topHint);
+ }
+
+ // Keep hints only for joins.
+ res =
Commons.context(filterNode).config().getSqlToRelConverterConfig().getHintStrategyTable().apply(res,
filterNode);
+
+ if (!res.isEmpty())
+ rel = ((Hintable)rel).attachHints(res);
+
+ return rel;
+ }
+
+ /**
+ * A join type hint might be assigned to a query root (top-level hint) or
to a table. Originally, SELECT-level hints
+ * are propagated and assigned to following Joins and TableScans. We lose
assigned to Join nodes ones
+ * in {@link JoinToMultiJoinRule} and have to reassign them from top-level
hints.
+ */
+ private static void restoreJoinTypeHints(RelNode root) {
+ RelShuttle visitor = new RelHomogeneousShuttle() {
+ /** Hints to assign on current tree level. */
+ private final Deque<List<RelHint>> hintsStack = new ArrayDeque<>();
+
+ /** Current hint inheritance path. It is important for hint
priority. */
+ private final List<Integer> inputsStack = new ArrayList<>();
+
+ /** {@inheritDoc} */
+ @Override public RelNode visit(RelNode rel) {
+ // Leaf scans has no inputs. And we are interrested only in
Joins.
+ if (rel.getInputs().isEmpty())
+ return rel;
+
+ List<RelHint> curHints = Collections.emptyList();
+
+ if ((rel instanceof Hintable) && !(rel instanceof Join) &&
!((Hintable)rel).getHints().isEmpty()) {
+ for (RelHint hint : ((Hintable)rel).getHints()) {
+ // Reassign only top-level hints (without the inherit
path).
+ if (!hint.inheritPath.isEmpty())
+ continue;
+
+ if (curHints == Collections.EMPTY_LIST)
+ curHints = new ArrayList<>();
+
+ curHints.add(hint);
+ }
+ }
+
+ // We may find additional top-level hints in a subquery. From
this point, we need to combine them.
+ if (!stack.isEmpty()) {
+ List<RelHint> prevHints = hintsStack.peekLast();
+
+ if (!curHints.isEmpty() && !prevHints.isEmpty())
+ curHints.addAll(prevHints);
+ else if (curHints.isEmpty())
+ curHints = prevHints;
+
+ assert curHints.size() >= hintsStack.peekLast().size();
+ }
+
+ hintsStack.add(curHints);
+
+ RelNode res = super.visit(rel);
+
+ hintsStack.removeLast();
+
+ return res;
+ }
+
+ /** {@inheritDoc} */
+ @Override protected RelNode visitChild(RelNode parent, int i,
RelNode child) {
+ inputsStack.add(i);
+
+ if (child instanceof Join && !hintsStack.isEmpty()) {
+ List<RelHint> curHints = hintsStack.peekLast();
+
+ if (!curHints.isEmpty()) {
+ assert
Commons.context(child).config().getSqlToRelConverterConfig().getHintStrategyTable()
+ .apply(curHints, child).size() == curHints.size()
: "Not all hints are applicable.";
+
+ curHints = curHints.stream().map(h ->
h.copy(inputsStack)).collect(Collectors.toList());
+
+ // Join is a Hintable.
+ child = ((Hintable)child).attachHints(curHints);
+
+ parent.replaceInput(i, child);
+ }
+ }
+
+ RelNode res = super.visitChild(parent, i, child);
+
+ inputsStack.remove(inputsStack.size() - 1);
+
+ return res;
+ }
+ };
+
+ root.accept(visitor);
}
/**
@@ -326,4 +526,36 @@ public class PlannerHelper {
return modifyNode.isInsert();
}
}
+
+ /**
+ * Searches tree {@code root} for nodes of {@code nodeType}.
+ *
+ * @return Nodes matching {@code nodeType}. An empty list if none matches.
A single value list if a node
+ * found and {@code stopOnFirst} is {@code true}.
+ */
+ public static <T extends RelNode> List<T> findNodes(RelNode root, Class<T>
nodeType, boolean stopOnFirst) {
+ List<T> rels = new ArrayList<>();
+
+ try {
+ RelShuttle visitor = new RelHomogeneousShuttle() {
+ @Override public RelNode visit(RelNode node) {
+ if (nodeType.isAssignableFrom(node.getClass())) {
+ rels.add((T)node);
+
+ if (stopOnFirst)
+ throw Util.FoundOne.NULL;
+ }
+
+ return super.visit(node);
+ }
+ };
+
+ root.accept(visitor);
+ }
+ catch (Util.FoundOne ignored) {
+ // No-op.
+ }
+
+ return rels;
+ }
}
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 7dc61997a0d..a268f92fc6e 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
@@ -18,6 +18,9 @@
package org.apache.ignite.internal.processors.query.calcite.prepare;
import org.apache.calcite.plan.RelRule;
+import org.apache.calcite.plan.hep.HepMatchOrder;
+import org.apache.calcite.plan.hep.HepProgram;
+import org.apache.calcite.plan.hep.HepProgramBuilder;
import org.apache.calcite.rel.core.Aggregate;
import org.apache.calcite.rel.logical.LogicalAggregate;
import org.apache.calcite.rel.logical.LogicalFilter;
@@ -64,6 +67,7 @@ import
org.apache.ignite.internal.processors.query.calcite.rule.UnionConverterRu
import
org.apache.ignite.internal.processors.query.calcite.rule.ValuesConverterRule;
import
org.apache.ignite.internal.processors.query.calcite.rule.logical.ExposeIndexRule;
import
org.apache.ignite.internal.processors.query.calcite.rule.logical.FilterScanMergeRule;
+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;
@@ -140,6 +144,24 @@ public enum PlannerPhase {
}
},
+ /** */
+ HEP_OPTIMIZE_JOIN_ORDER("Heuristic phase to optimize joins order") {
+ /** {@inheritDoc} */
+ @Override public RuleSet getRules(PlanningContext ctx) {
+ return
ctx.rules(RuleSets.ofList(IgniteMultiJoinOptimizeRule.INSTANCE));
+ }
+
+ /** {@inheritDoc} */
+ @Override public Program getProgram(PlanningContext ctx) {
+ HepProgram sub = new HepProgramBuilder()
+ .addMatchOrder(HepMatchOrder.BOTTOM_UP)
+ .addRuleInstance(CoreRules.JOIN_TO_MULTI_JOIN)
+ .build();
+
+ return hep(getRules(ctx), sub);
+ }
+ },
+
/** */
OPTIMIZATION("Main optimization phase") {
/** {@inheritDoc} */
diff --git
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/logical/IgniteMultiJoinOptimizeRule.java
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/logical/IgniteMultiJoinOptimizeRule.java
new file mode 100644
index 00000000000..d3c717c10f2
--- /dev/null
+++
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/logical/IgniteMultiJoinOptimizeRule.java
@@ -0,0 +1,446 @@
+/*
+ * 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.ArrayList;
+import java.util.BitSet;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.IdentityHashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+import org.apache.calcite.plan.RelOptRuleCall;
+import org.apache.calcite.plan.RelRule;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.core.JoinRelType;
+import org.apache.calcite.rel.hint.Hintable;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.rules.LoptMultiJoin;
+import org.apache.calcite.rel.rules.MultiJoin;
+import org.apache.calcite.rel.rules.TransformationRule;
+import org.apache.calcite.rex.RexBuilder;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexPermuteInputsShuttle;
+import org.apache.calcite.rex.RexUtil;
+import org.apache.calcite.sql.SqlKind;
+import org.apache.calcite.tools.RelBuilder;
+import org.apache.calcite.util.mapping.Mappings;
+import org.apache.calcite.util.mapping.Mappings.TargetMapping;
+import org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition;
+import org.apache.ignite.internal.util.collection.IntMap;
+import org.apache.ignite.internal.util.collection.IntRWHashMap;
+import org.immutables.value.Value;
+import org.jetbrains.annotations.Nullable;
+
+import static org.apache.ignite.internal.util.IgniteUtils.isPow2;
+
+/**
+ * A rule for optimization of flat multi-join using a bushy trees.
+ *
+ * <p>This is an implementation of subset-driven enumeration algorithm (By T.
Neumann. and G. Moerkotte. Analysis of
+ * Two Existing and One New Dynamic Programming Algorithm for the Generation
of Optimal Bushy Join Trees without Cross Products).
+ *
+ * <p>The main loop enumerates all relations and guarantees that for every
emitted set {@code S} any split of this set
+ * will produce subset which have been already processed.
+ *
+ * <p>The inner loop enumerates all possible splits of given subset {@code S}
on disjoint subset
+ * {@code lhs} and {@code rhs} such that {@code lhs ∪ rhs = S} (B. Vance and
D. Maier. Rapid bushy join-order optimization
+ * with cartesian products).
+ *
+ * <p>Finally, if the initial set is not connected, the algorithm crates
cartesian join from the best plan until
+ * all the relations are connected.
+ *
+ * <p>Limitations:
+ * <ol>
+ * <li>Only INNER joins are supported</li>
+ * <li>Disjunctive predicate (condition) is not considered as
connections.</li>
+ * <li>The maximal number of relations is 20. The value is based on time
and memory consumption.</li>
+ * </ol>
+ */
[email protected]
+public class IgniteMultiJoinOptimizeRule extends
RelRule<IgniteMultiJoinOptimizeRule.Config> implements TransformationRule {
+ /** */
+ public static final IgniteMultiJoinOptimizeRule INSTANCE = new
IgniteMultiJoinOptimizeRule(Config.DEFAULT);
+
+ /** */
+ private static final int MAX_JOIN_SIZE = 20;
+
+ /** Vertexes comparator. Better vertex incorporate more relations or costs
less. */
+ private static final Comparator<Vertex> VERTEX_COMPARATOR =
Comparator.<Vertex>comparingInt(v -> v.size).reversed()
+ .thenComparingDouble(v -> v.cost);
+
+ /** Creates the rule. */
+ private IgniteMultiJoinOptimizeRule(Config cfg) {
+ super(cfg);
+ }
+
+ /** {@inheritDoc} */
+ @Override public void onMatch(RelOptRuleCall call) {
+ MultiJoin multiJoinRel = call.rel(0);
+
+ // Currently, only INNER JOIN is supported.
+ if (multiJoinRel.isFullOuterJoin())
+ return;
+
+ int relCnt = multiJoinRel.getInputs().size();
+
+ if (relCnt > MAX_JOIN_SIZE)
+ return;
+
+ for (JoinRelType joinType : multiJoinRel.getJoinTypes()) {
+ if (joinType != JoinRelType.INNER)
+ return;
+ }
+
+ LoptMultiJoin multiJoin = new LoptMultiJoin(multiJoinRel);
+
+ RexBuilder rexBuilder = multiJoinRel.getCluster().getRexBuilder();
+ RelBuilder relBuilder = call.builder();
+ RelMetadataQuery callMeta = call.getMetadataQuery();
+
+ List<RexNode> unusedConditions = new ArrayList<>();
+
+ // Edges by vertex (rel.) number in pow2 starting.
+ IntMap<List<Edge>> edges = collectEdges(multiJoin, unusedConditions);
+ BitSet connections = new BitSet(1 << relCnt);
+ IntMap<Vertex> bestPlan = new IntRWHashMap<>();
+
+ int fldMappingOffset = 0;
+ int relNumPow2 = 1;
+
+ for (RelNode input : multiJoinRel.getInputs()) {
+ TargetMapping fldMapping = Mappings.offsetSource(
+ Mappings.createIdentity(input.getRowType().getFieldCount()),
+ fldMappingOffset,
+ multiJoin.getNumTotalFields()
+ );
+
+ bestPlan.put(relNumPow2, new Vertex(relNumPow2,
callMeta.getRowCount(input), input, fldMapping));
+
+ connections.set(relNumPow2);
+
+ relNumPow2 <<= 1;
+
+ fldMappingOffset += input.getRowType().getFieldCount();
+ }
+
+ Vertex bestPlanSoFar = null;
+
+ for (int s = 0b11, cnt = 1 << relCnt; s < cnt; ++s) {
+ // Pow2-value refers to an initial relation. They are already
processed at the first phase.
+ if (isPow2(s))
+ continue;
+
+ int lhs = Integer.lowestOneBit(s);
+
+ while (lhs < (s / 2) + 1) {
+ int rhs = s - lhs;
+
+ List<Edge> edges0 = connections.get(lhs) &&
connections.get(rhs)
+ ? findEdges(lhs, rhs, edges)
+ : Collections.emptyList();
+
+ if (!edges0.isEmpty()) {
+ connections.set(s);
+
+ Vertex leftPlan = bestPlan.get(lhs);
+ Vertex rightPlan = bestPlan.get(rhs);
+
+ Vertex newPlan = createJoin(leftPlan, rightPlan, edges0,
callMeta, relBuilder, rexBuilder);
+
+ Vertex curBestPlan = bestPlan.get(s);
+
+ if (curBestPlan == null || curBestPlan.cost >
newPlan.cost) {
+ bestPlan.put(s, newPlan);
+
+ bestPlanSoFar = best(bestPlanSoFar, newPlan);
+ }
+
+ aggregateEdges(edges, lhs, rhs);
+ }
+
+ lhs = s & (lhs - s);
+ }
+ }
+
+ int allRelationsMask = (1 << relCnt) - 1;
+
+ Vertex res;
+
+ if (bestPlanSoFar == null || bestPlanSoFar.id != allRelationsMask)
+ res = composeCartesianJoin(allRelationsMask, bestPlan, edges,
bestPlanSoFar, callMeta, relBuilder, rexBuilder);
+ else
+ res = bestPlanSoFar;
+
+ RelNode result = relBuilder
+ .push(res.rel)
+ .filter(RexUtil.composeConjunction(rexBuilder,
unusedConditions).accept(new RexPermuteInputsShuttle(res.mapping, res.rel)))
+ .project(relBuilder.fields(res.mapping))
+ .build();
+
+ call.transformTo(result);
+ }
+
+ /** */
+ private static void aggregateEdges(IntMap<List<Edge>> edges, int lhs, int
rhs) {
+ int id = lhs | rhs;
+
+ if (!edges.containsKey(id)) {
+ Set<Edge> used = Collections.newSetFromMap(new
IdentityHashMap<>());
+
+ List<Edge> union = new ArrayList<>(edges.computeIfAbsent(lhs, k ->
Collections.emptyList()));
+
+ used.addAll(union);
+
+ edges.computeIfAbsent(rhs, k ->
Collections.emptyList()).forEach(edge -> {
+ if (used.add(edge))
+ union.add(edge);
+ });
+
+ if (!union.isEmpty())
+ edges.put(id, union);
+ }
+ }
+
+ /** */
+ private static Vertex composeCartesianJoin(
+ int allRelationsMask,
+ IntMap<Vertex> bestPlan,
+ IntMap<List<Edge>> edges,
+ @Nullable Vertex bestSoFar,
+ RelMetadataQuery mq,
+ RelBuilder relBuilder,
+ RexBuilder rexBuilder
+ ) {
+ List<Vertex> options;
+
+ if (bestSoFar != null) {
+ options = new ArrayList<>();
+
+ for (Vertex option : bestPlan.values()) {
+ if ((option.id & bestSoFar.id) == 0)
+ options.add(option);
+ }
+ }
+ else
+ options = new ArrayList<>(bestPlan.values());
+
+ options.sort(VERTEX_COMPARATOR);
+
+ Iterator<Vertex> it = options.iterator();
+
+ if (bestSoFar == null)
+ bestSoFar = it.next();
+
+ while (it.hasNext() && bestSoFar.id != allRelationsMask) {
+ Vertex input = it.next();
+
+ if ((bestSoFar.id & input.id) != 0)
+ continue;
+
+ List<Edge> edges0 = findEdges(bestSoFar.id, input.id, edges);
+
+ aggregateEdges(edges, bestSoFar.id, input.id);
+
+ bestSoFar = createJoin(bestSoFar, input, edges0, mq, relBuilder,
rexBuilder);
+ }
+
+ assert bestSoFar.id == allRelationsMask;
+
+ return bestSoFar;
+ }
+
+ /** */
+ private static Vertex best(@Nullable Vertex curBest, Vertex candidate) {
+ return curBest == null || VERTEX_COMPARATOR.compare(curBest,
candidate) > 0
+ ? candidate
+ : curBest;
+ }
+
+ /** */
+ private static IntMap<List<Edge>> collectEdges(LoptMultiJoin multiJoin,
List<RexNode> unusedConditions) {
+ IntMap<List<Edge>> edges = new IntRWHashMap();
+
+ for (RexNode joinCondition : multiJoin.getJoinFilters()) {
+ // Involved rel numbers starting from 0.
+ int[] joinRelNums =
multiJoin.getFactorsRefByJoinFilter(joinCondition).toArray();
+
+ // Skip conditions involving a single table. The main loop looks
only for edges connecting two subsets.
+ // A condition referring to a single table never meet this
requirement. Also, for inner join such conditions must
+ // be pushed down already.
+ if (joinRelNums.length < 2) {
+ unusedConditions.add(joinCondition);
+
+ continue;
+ }
+
+ // TODO: support with adoption of IGNITE-24210
+ if (joinCondition.isA(SqlKind.OR)) {
+ unusedConditions.add(joinCondition);
+
+ continue;
+ }
+
+ int connectedInputs = 0;
+
+ for (int i : joinRelNums)
+ connectedInputs |= 1 << i;
+
+ Edge edge = new Edge(connectedInputs, joinCondition);
+
+ for (int i : joinRelNums)
+ edges.computeIfAbsent(1 << i, k -> new
ArrayList<>()).add(edge);
+ }
+
+ return edges;
+ }
+
+ /** */
+ private static Vertex createJoin(
+ Vertex lhs,
+ Vertex rhs,
+ List<Edge> edges,
+ RelMetadataQuery metadataQry,
+ RelBuilder relBuilder,
+ RexBuilder rexBuilder
+ ) {
+ List<RexNode> conditions = new ArrayList<>();
+
+ edges.forEach(e -> conditions.add(e.condition));
+
+ Vertex majorFactor;
+ Vertex minorFactor;
+
+ // Let's put bigger input on left side, because right side will
probably be materialized.
+ if (metadataQry.getRowCount(lhs.rel) >=
metadataQry.getRowCount(rhs.rel)) {
+ majorFactor = lhs;
+ minorFactor = rhs;
+ }
+ else {
+ majorFactor = rhs;
+ minorFactor = lhs;
+ }
+
+ TargetMapping fldMapping = Mappings.merge(
+ majorFactor.mapping,
+ Mappings.offsetTarget(minorFactor.mapping,
majorFactor.rel.getRowType().getFieldCount())
+ );
+
+ RexNode newCondition = RexUtil.composeConjunction(rexBuilder,
conditions)
+ .accept(new RexPermuteInputsShuttle(fldMapping, majorFactor.rel,
minorFactor.rel));
+
+ RelNode join =
relBuilder.push(majorFactor.rel).push(minorFactor.rel).join(JoinRelType.INNER,
newCondition).build();
+
+ assert ((Hintable)join).getHints().stream().noneMatch(h ->
h.hintName.equals(HintDefinition.ENFORCE_JOIN_ORDER.name()))
+ : "Joins with enforced order must not be optimized.";
+
+ double cost = metadataQry.getRowCount(join) + lhs.cost + rhs.cost;
+
+ return new Vertex(lhs.id | rhs.id, cost, join, fldMapping);
+ }
+
+ /**
+ * Finds all edges which connect given subsets.
+ *
+ * <p>Returned edges will satisfy two conditions:<ol>
+ * <li>At least one relation from each side will be covered by
edge.</li>
+ * <li>No any other relations outside of {@code lhs ∪ rhs} will be
covered by edge.</li>
+ * </ol>
+ *
+ * @param lhs Left subtree.
+ * @param rhs Right subtree.
+ * @param edges All the edges.
+ * @return Edges connecting given subtrees.
+ */
+ private static List<Edge> findEdges(int lhs, int rhs, IntMap<List<Edge>>
edges) {
+ List<Edge> result = new ArrayList<>();
+
+ List<Edge> fromLeft = edges.computeIfAbsent(lhs, k ->
Collections.emptyList());
+
+ for (Edge edge : fromLeft) {
+ int requiredInputs = edge.connectedInputs & ~lhs;
+
+ if (requiredInputs == 0 || edge.connectedInputs == requiredInputs)
+ continue;
+
+ requiredInputs &= ~rhs;
+
+ if (requiredInputs == 0)
+ result.add(edge);
+ }
+
+ return result;
+ }
+
+ /** */
+ private static final class Edge {
+ /** Bitmap of all inputs connected by condition. */
+ private final int connectedInputs;
+
+ /** Join condition. */
+ private final RexNode condition;
+
+ /** */
+ private Edge(int connectedInputs, RexNode condition) {
+ this.connectedInputs = connectedInputs;
+
+ this.condition = condition;
+ }
+ }
+
+ /** Root vertex (rel) of a tree. */
+ private static class Vertex {
+ /** Bitmap of inputs joined together with current vertex. */
+ private final int id;
+
+ /** Number of inputs joined together with current vertex. */
+ private final byte size;
+
+ /** Total cost of this tree. */
+ private final double cost;
+
+ /** Join mapping. */
+ private final TargetMapping mapping;
+
+ /** Node of this vertex. */
+ private final RelNode rel;
+
+ /** */
+ private Vertex(int id, double cost, RelNode rel, TargetMapping
mapping) {
+ this.id = id;
+ this.size = (byte)Integer.bitCount(id);
+ this.cost = cost;
+ this.rel = rel;
+ this.mapping = mapping;
+ }
+ }
+
+ /** Rule configuration. */
+ @Value.Immutable
+ public interface Config extends RelRule.Config {
+ /** */
+ Config DEFAULT = ImmutableIgniteMultiJoinOptimizeRule.Config.of()
+ .withOperandSupplier(b -> b.operand(MultiJoin.class).anyInputs());
+
+ /** {@inheritDoc} */
+ @Override default IgniteMultiJoinOptimizeRule toRule() {
+ return INSTANCE;
+ }
+ }
+}
diff --git
a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/QueryChecker.java
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/QueryChecker.java
index 76a12cbe10b..86d789c8159 100644
---
a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/QueryChecker.java
+++
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/QueryChecker.java
@@ -25,9 +25,11 @@ import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
+import java.util.function.Consumer;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
+import org.apache.calcite.plan.RelOptListener;
import org.apache.calcite.tools.FrameworkConfig;
import org.apache.ignite.Ignite;
import org.apache.ignite.cache.query.FieldsQueryCursor;
@@ -308,6 +310,12 @@ public abstract class QueryChecker {
/** */
private FrameworkConfig frameworkCfg;
+ /** */
+ private RelOptListener planLsnr;
+
+ /** */
+ private Consumer<List<List<?>>> resultChecker;
+
/** */
public QueryChecker(String qry) {
this(qry, null, SqlTransactionMode.NONE);
@@ -350,6 +358,20 @@ public abstract class QueryChecker {
return this;
}
+ /** */
+ public QueryChecker withPlannerListener(RelOptListener lsnr) {
+ this.planLsnr = lsnr;
+
+ return this;
+ }
+
+ /** */
+ public QueryChecker withResultChecker(Consumer<List<List<?>>>
resultChecker) {
+ this.resultChecker = resultChecker;
+
+ return this;
+ }
+
/** */
public QueryChecker returns(Object... res) {
if (expectedResult == null)
@@ -402,7 +424,9 @@ public abstract class QueryChecker {
? ((TransactionProxyImpl)tx).tx().xidVersion()
: null;
- QueryContext ctx = (frameworkCfg != null || txVer != null) ?
QueryContext.of(frameworkCfg, txVer) : null;
+ QueryContext ctx = (frameworkCfg != null || txVer != null || planLsnr
!= null)
+ ? QueryContext.of(frameworkCfg, txVer, planLsnr)
+ : null;
List<FieldsQueryCursor<List<?>>> explainCursors =
engine.query(ctx, "PUBLIC", "EXPLAIN PLAN FOR " + qry, params);
@@ -421,8 +445,7 @@ public abstract class QueryChecker {
assertEquals(exactPlan, actualPlan);
// Check result.
- List<FieldsQueryCursor<List<?>>> cursors =
- engine.query(ctx, "PUBLIC", qry, params);
+ List<FieldsQueryCursor<List<?>>> cursors = engine.query(ctx, "PUBLIC",
qry, params);
FieldsQueryCursor<List<?>> cur = cursors.get(0);
@@ -454,6 +477,9 @@ public abstract class QueryChecker {
assertEqualsCollections(expectedResult, res);
}
+
+ if (resultChecker != null)
+ resultChecker.accept(res);
}
/** */
diff --git
a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/RuleApplyListener.java
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/RuleApplyListener.java
new file mode 100644
index 00000000000..5b33ffb1016
--- /dev/null
+++
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/RuleApplyListener.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;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import org.apache.calcite.plan.RelOptListener;
+import org.apache.calcite.plan.RelOptRule;
+import org.apache.calcite.plan.RelOptRuleCall;
+import org.apache.calcite.plan.RuleEventLogger;
+
+/** Watches appliying of a rule. */
+public class RuleApplyListener extends RuleEventLogger {
+ /** */
+ private final RelOptRule rule;
+
+ /** */
+ public final Collection<RelOptListener.RuleAttemptedEvent> tries = new
ArrayList<>();
+
+ /** */
+ private final Collection<RelOptRuleCall> products = new HashSet<>();
+
+ /** */
+ public RuleApplyListener(RelOptRule rule) {
+ this.rule = rule;
+ }
+
+ /** {@inheritDoc} */
+ @Override public void ruleAttempted(RuleAttemptedEvent evt) {
+ assert evt.getRuleCall().getRule() == rule ||
!rule.getClass().isAssignableFrom(evt.getRuleCall().getRule().getClass());
+
+ if (evt.getRuleCall().getRule() == rule && !evt.isBefore())
+ tries.add(evt);
+
+ super.ruleAttempted(evt);
+ }
+
+ /** {@inheritDoc} */
+ @Override public void ruleProductionSucceeded(RuleProductionEvent evt) {
+ assert evt.getRuleCall().getRule() == rule ||
!rule.getClass().isAssignableFrom(evt.getRuleCall().getRule().getClass());
+
+ if (evt.getRuleCall().getRule() == rule && !evt.isBefore())
+ products.add(evt.getRuleCall());
+
+ super.ruleProductionSucceeded(evt);
+ }
+
+ /**
+ * Checks wheter the rule was sucessfully applied. Resets the listener.
+ *
+ * @return {@code True} if rule has worked and realeased a product.
+ */
+ public boolean ruleSucceeded() {
+ boolean res = !tries.isEmpty() && tries.stream().allMatch(e ->
products.contains(e.getRuleCall()));
+
+ tries.clear();
+ products.clear();
+
+ return res;
+ }
+}
diff --git
a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/AbstractBasicIntegrationTest.java
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/AbstractBasicIntegrationTest.java
index be3496844ae..bdf1904609d 100644
---
a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/AbstractBasicIntegrationTest.java
+++
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/AbstractBasicIntegrationTest.java
@@ -83,6 +83,8 @@ public class AbstractBasicIntegrationTest extends
GridCommonAbstractTest {
/** {@inheritDoc} */
@Override protected void afterTest() throws Exception {
+ super.afterTest();
+
// Wait for pending queries before destroying caches. If some error
occurs during query execution, client code
// can get control earlier than query leave the running queries
registry (need some time for async message
// exchange), but eventually, all queries should be closed.
@@ -245,6 +247,32 @@ public class AbstractBasicIntegrationTest extends
GridCommonAbstractTest {
}
}
+ /** */
+ protected void gatherStatistics() throws Exception {
+ for (List<?> lst : sql("SELECT TABLE_NAME FROM SYS.TABLES")) {
+ assert lst.size() == 1 : "Single table name expected";
+
+ String tbl = lst.get(0).toString().toUpperCase();
+
+ sql("ANALYZE " + tbl);
+
+ waitForCondition(
+ () -> {
+ for (Ignite node : G.allGrids()) {
+ if (node.configuration().isClientMode())
+ continue;
+
+ if (F.isEmpty(sql((IgniteEx)node, "select * from
sys.statistics_local_data where name = ?", tbl)))
+ return false;
+ }
+
+ return true;
+ },
+ getTestTimeout()
+ );
+ }
+ }
+
/** */
public static class DelegatingIgniteIndex implements IgniteIndex {
/** */
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 a3cb34dd355..a7b569d2ca4 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
@@ -29,6 +29,8 @@ import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import com.google.common.collect.ImmutableSet;
+import org.apache.calcite.plan.Contexts;
+import org.apache.calcite.plan.RelOptListener;
import org.apache.calcite.plan.RelOptTable;
import org.apache.calcite.plan.RelOptUtil;
import org.apache.calcite.plan.RelTraitSet;
@@ -222,13 +224,28 @@ public abstract class AbstractPlannerTest extends
GridCommonAbstractTest {
/** */
protected PlanningContext plannerCtx(String sql, IgniteSchema
publicSchema, String... disabledRules) {
- return plannerCtx(sql, Collections.singleton(publicSchema),
disabledRules);
+ return plannerCtx(sql, publicSchema, null, disabledRules);
}
/** */
- protected PlanningContext plannerCtx(String sql, Collection<IgniteSchema>
schemas, String... disabledRules) {
+ protected PlanningContext plannerCtx(
+ String sql,
+ IgniteSchema publicSchema,
+ @Nullable RelOptListener planLsnr,
+ String... disabledRules
+ ) {
+ return plannerCtx(sql, Collections.singleton(publicSchema), planLsnr,
disabledRules);
+ }
+
+ /** */
+ protected PlanningContext plannerCtx(
+ String sql,
+ Collection<IgniteSchema> schemas,
+ @Nullable RelOptListener planLsnr,
+ String... disabledRules
+ ) {
PlanningContext ctx = PlanningContext.builder()
- .parentContext(baseQueryContext(schemas))
+ .parentContext(Contexts.of(baseQueryContext(schemas), planLsnr))
.query(sql)
.build();
@@ -243,12 +260,17 @@ public abstract class AbstractPlannerTest extends
GridCommonAbstractTest {
/** */
protected IgniteRel physicalPlan(String sql, IgniteSchema publicSchema,
String... disabledRules) throws Exception {
- return physicalPlan(plannerCtx(sql, publicSchema, disabledRules));
+ return physicalPlan(sql, publicSchema, null, disabledRules);
}
/** */
- protected IgniteRel physicalPlan(String sql, Collection<IgniteSchema>
schemas, String... disabledRules) throws Exception {
- return physicalPlan(plannerCtx(sql, schemas, disabledRules));
+ protected IgniteRel physicalPlan(
+ String sql,
+ IgniteSchema publicSchema,
+ @Nullable RelOptListener planLsnr,
+ String... disabledRules
+ ) throws Exception {
+ return physicalPlan(plannerCtx(sql, publicSchema, planLsnr,
disabledRules));
}
/** */
@@ -437,7 +459,7 @@ public abstract class AbstractPlannerTest extends
GridCommonAbstractTest {
Predicate<T> predicate,
String... disabledRules
) throws Exception {
- assertPlan(sql, Collections.singleton(schema), predicate,
disabledRules);
+ assertPlan(sql, schema, null, predicate, disabledRules);
}
/** */
@@ -447,7 +469,18 @@ public abstract class AbstractPlannerTest extends
GridCommonAbstractTest {
Predicate<T> predicate,
String... disabledRules
) throws Exception {
- IgniteRel plan = physicalPlan(sql, schemas, disabledRules);
+ assertPlan(sql, schemas, null, predicate, disabledRules);
+ }
+
+ /** */
+ protected <T extends RelNode> void assertPlan(
+ String sql,
+ Collection<IgniteSchema> schemas,
+ @Nullable RelOptListener planLsnr,
+ Predicate<T> predicate,
+ String... disabledRules
+ ) throws Exception {
+ IgniteRel plan = physicalPlan(plannerCtx(sql, schemas, planLsnr,
disabledRules));
checkSplitAndSerialization(plan, schemas);
@@ -459,6 +492,17 @@ public abstract class AbstractPlannerTest extends
GridCommonAbstractTest {
}
}
+ /** */
+ protected <T extends RelNode> void assertPlan(
+ String sql,
+ IgniteSchema schema,
+ @Nullable RelOptListener planLsnr,
+ Predicate<T> predicate,
+ String... disabledRules
+ ) throws Exception {
+ assertPlan(sql, Collections.singletonList(schema), planLsnr,
predicate, disabledRules);
+ }
+
/**
* Predicate builder for "Instance of class" condition.
*/
diff --git
a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/JoinCommutePlannerTest.java
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/JoinCommutePlannerTest.java
index b3f22a9a709..997e4ceaea8 100644
---
a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/JoinCommutePlannerTest.java
+++
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/JoinCommutePlannerTest.java
@@ -31,21 +31,20 @@ import org.apache.calcite.rel.core.JoinRelType;
import org.apache.calcite.rel.rules.CoreRules;
import org.apache.calcite.rel.rules.JoinCommuteRule;
import org.apache.calcite.rel.rules.JoinPushThroughJoinRule;
-import org.apache.calcite.rel.type.RelDataTypeFactory;
+import org.apache.ignite.internal.processors.query.calcite.RuleApplyListener;
import
org.apache.ignite.internal.processors.query.calcite.prepare.PlannerHelper;
import
org.apache.ignite.internal.processors.query.calcite.prepare.PlanningContext;
import
org.apache.ignite.internal.processors.query.calcite.rel.IgniteNestedLoopJoin;
import org.apache.ignite.internal.processors.query.calcite.rel.IgniteProject;
import org.apache.ignite.internal.processors.query.calcite.rel.IgniteRel;
import org.apache.ignite.internal.processors.query.calcite.rel.IgniteTableScan;
+import
org.apache.ignite.internal.processors.query.calcite.rule.logical.IgniteMultiJoinOptimizeRule;
import org.apache.ignite.internal.processors.query.calcite.schema.IgniteSchema;
import
org.apache.ignite.internal.processors.query.calcite.trait.IgniteDistribution;
import
org.apache.ignite.internal.processors.query.calcite.trait.IgniteDistributions;
-import
org.apache.ignite.internal.processors.query.calcite.type.IgniteTypeFactory;
-import
org.apache.ignite.internal.processors.query.calcite.type.IgniteTypeSystem;
import org.junit.Test;
-/** Tests correctness applying of JOIN_COMMUTE* rules. */
+/** Tests correctness applying of JOIN_COMMUTE* and {@link
IgniteMultiJoinOptimizeRule} rules. */
public class JoinCommutePlannerTest extends AbstractPlannerTest {
/** */
private static IgniteSchema publicSchema;
@@ -54,34 +53,12 @@ public class JoinCommutePlannerTest extends
AbstractPlannerTest {
@Override protected void beforeTestsStarted() throws Exception {
super.beforeTestsStarted();
- publicSchema = new IgniteSchema("PUBLIC");
+ IgniteDistribution distr = IgniteDistributions.affinity(0, "HUGE",
"hash");
- IgniteTypeFactory f = new IgniteTypeFactory(IgniteTypeSystem.INSTANCE);
-
- publicSchema.addTable(
- "HUGE",
- new TestTable(
- new RelDataTypeFactory.Builder(f)
- .add("ID", f.createJavaType(Integer.class))
- .build(), 1_000) {
-
- @Override public IgniteDistribution distribution() {
- return IgniteDistributions.affinity(0, "HUGE", "hash");
- }
- }
- );
-
- publicSchema.addTable(
- "SMALL",
- new TestTable(
- new RelDataTypeFactory.Builder(f)
- .add("ID", f.createJavaType(Integer.class))
- .build(), 10) {
-
- @Override public IgniteDistribution distribution() {
- return IgniteDistributions.affinity(0, "SMALL", "hash");
- }
- }
+ publicSchema = createSchema(
+ createTable("SMALL", 10, distr, "ID", Integer.class),
+ createTable("AVERAGE", 100, distr, "ID", Integer.class),
+ createTable("HUGE", 1_000, distr, "ID", Integer.class)
);
}
@@ -164,29 +141,36 @@ public class JoinCommutePlannerTest extends
AbstractPlannerTest {
String sql = "SELECT " + select.substring(0, select.length() - 2) +
"\nFROM " + joins;
- PlanningContext ctx = plannerCtx(sql, publicSchema);
+ // The join order optimization should not work. It disables the
default join commute rules too.
+ RuleApplyListener orderOptmzLsnr = new
RuleApplyListener(IgniteMultiJoinOptimizeRule.INSTANCE);
- RuleMatchVisualizer lsnr = new RuleMatchVisualizer() {
+ PlanningContext ctx = plannerCtx(sql, publicSchema, orderOptmzLsnr);
+
+ RuleMatchVisualizer commuteLsnr = new RuleMatchVisualizer() {
@Override public void ruleAttempted(RuleAttemptedEvent evt) {
- if (rules.contains(evt.getRuleCall().getRule()))
+ if (rules.contains(evt.getRuleCall().getRule()) &&
!evt.isBefore())
assertTrue(rulesExpected);
super.ruleAttempted(evt);
}
};
- lsnr.attachTo(ctx.cluster().getPlanner());
+ commuteLsnr.attachTo(ctx.cluster().getPlanner());
+ ctx.cluster().getPlanner().addListener(commuteLsnr);
physicalPlan(ctx);
+
+ assertFalse(orderOptmzLsnr.ruleSucceeded());
}
/** */
@Test
public void testOuterCommute() throws Exception {
+ RuleApplyListener planLsnr = new
RuleApplyListener(IgniteMultiJoinOptimizeRule.INSTANCE);
+
String sql = "SELECT COUNT(*) FROM SMALL s RIGHT JOIN HUGE h on h.id =
s.id";
- IgniteRel phys = physicalPlan(sql, publicSchema,
- "MergeJoinConverter", "CorrelatedNestedLoopJoin");
+ IgniteRel phys = physicalPlan(sql, publicSchema, planLsnr,
"MergeJoinConverter", "CorrelatedNestedLoopJoin");
assertNotNull(phys);
@@ -198,7 +182,7 @@ public class JoinCommutePlannerTest extends
AbstractPlannerTest {
assertEquals(JoinRelType.LEFT, join.getJoinType());
- PlanningContext ctx = plannerCtx(sql, publicSchema,
"MergeJoinConverter",
+ PlanningContext ctx = plannerCtx(sql, publicSchema, planLsnr,
"MergeJoinConverter",
"CorrelatedNestedLoopJoin");
RelOptPlanner pl = ctx.cluster().getPlanner();
@@ -209,8 +193,7 @@ public class JoinCommutePlannerTest extends
AbstractPlannerTest {
assertNotNull(phys);
- phys = physicalPlan(sql, publicSchema,
- "MergeJoinConverter", "CorrelatedNestedLoopJoin",
"JoinCommuteRule");
+ phys = physicalPlan(sql, publicSchema, planLsnr, "MergeJoinConverter",
"CorrelatedNestedLoopJoin", "JoinCommuteRule");
join = findFirstNode(phys, byClass(IgniteNestedLoopJoin.class));
@@ -221,8 +204,7 @@ public class JoinCommutePlannerTest extends
AbstractPlannerTest {
// no commute
assertEquals(JoinRelType.RIGHT, join.getJoinType());
- ctx = plannerCtx(sql, publicSchema,
- "MergeJoinConverter", "CorrelatedNestedLoopJoin",
"JoinCommuteRule");
+ ctx = plannerCtx(sql, publicSchema, planLsnr, "MergeJoinConverter",
"CorrelatedNestedLoopJoin", "JoinCommuteRule");
pl = ctx.cluster().getPlanner();
@@ -231,15 +213,66 @@ public class JoinCommutePlannerTest extends
AbstractPlannerTest {
System.out.println("plan: " + RelOptUtil.toString(phys));
assertTrue(costWithCommute.isLt(costWithoutCommute));
+
+ assertFalse(planLsnr.ruleSucceeded());
+ }
+
+ /** Tests that {@link IgniteMultiJoinOptimizeRule} is enabled if joins
number reaches the threshold. */
+ @Test
+ public void testHeuristicReorderingIsEnabledByJoinsCount() throws
Exception {
+ assert PlannerHelper.JOINS_COUNT_FOR_HEURISTIC_ORDER >= 3;
+
+ String sql = "SELECT COUNT(*) FROM SMALL s JOIN HUGE h on h.id = s.id
JOIN AVERAGE a on a.id = h.id";
+
+ RuleApplyListener planLsnr = new
RuleApplyListener(IgniteMultiJoinOptimizeRule.INSTANCE);
+
+ physicalPlan(sql, publicSchema, planLsnr);
+
+ assertFalse(planLsnr.ruleSucceeded());
+
+ // Joins number is enough. But the optimization is disabled by the
hint.
+ sql = "SELECT /*+ ENFORCE_JOIN_ORDER */ s.id, h.id, a.id FROM SMALL s
JOIN HUGE h on h.id = s.id " +
+ "JOIN AVERAGE a on a.id = h.id JOIN SMALL ss on ss.id = a.id";
+
+ physicalPlan(sql, publicSchema, planLsnr);
+
+ assertFalse(planLsnr.ruleSucceeded());
+
+ // Joins number is enough. But the optimization is disabled by the
hint for some joins.
+ sql = "SELECT s.id, j.b, j.c FROM SMALL s JOIN " +
+ "(SELECT /*+ ENFORCE_JOIN_ORDER */ h.id as a, a.id as b, s2.id as
c FROM HUGE h JOIN AVERAGE a on h.id=a.id " +
+ "JOIN SMALL s2 ON a.id=s2.id) j ON s.id=j.a";
+
+ physicalPlan(sql, publicSchema, planLsnr);
+
+ assertFalse(planLsnr.ruleSucceeded());
+
+ // Joins number is enough and nothing is disabled.
+ sql = "SELECT s.id, j.b, j.c FROM SMALL s JOIN " +
+ "(SELECT h.id as a, a.id as b, s2.id as c FROM HUGE h JOIN AVERAGE
a on h.id=a.id " +
+ "JOIN SMALL s2 ON a.id=s2.id) j ON s.id=j.a";
+
+ physicalPlan(sql, publicSchema, planLsnr);
+
+ assertTrue(planLsnr.ruleSucceeded());
+
+ // Joins number is enough and nothing is disabled.
+ sql = "SELECT s.id, h.id, a.id FROM SMALL s JOIN HUGE h on h.id = s.id
JOIN AVERAGE a on a.id = h.id " +
+ "JOIN SMALL ss on ss.id = a.id";
+
+ physicalPlan(sql, publicSchema, planLsnr);
+
+ assertTrue(planLsnr.ruleSucceeded());
}
/** */
@Test
public void testInnerCommute() throws Exception {
+ RuleApplyListener planLsnr = new
RuleApplyListener(IgniteMultiJoinOptimizeRule.INSTANCE);
+
String sql = "SELECT COUNT(*) FROM SMALL s JOIN HUGE h on h.id = s.id";
- IgniteRel phys = physicalPlan(sql, publicSchema,
- "MergeJoinConverter", "CorrelatedNestedLoopJoin");
+ IgniteRel phys = physicalPlan(sql, publicSchema, planLsnr,
"MergeJoinConverter", "CorrelatedNestedLoopJoin");
assertNotNull(phys);
@@ -266,8 +299,7 @@ public class JoinCommutePlannerTest extends
AbstractPlannerTest {
assertEquals(JoinRelType.INNER, join.getJoinType());
- PlanningContext ctx = plannerCtx(sql, publicSchema,
"MergeJoinConverter",
- "CorrelatedNestedLoopJoin");
+ PlanningContext ctx = plannerCtx(sql, publicSchema, planLsnr,
"MergeJoinConverter", "CorrelatedNestedLoopJoin");
RelOptPlanner pl = ctx.cluster().getPlanner();
@@ -277,8 +309,7 @@ public class JoinCommutePlannerTest extends
AbstractPlannerTest {
assertNotNull(phys);
- phys = physicalPlan(sql, publicSchema,
- "MergeJoinConverter", "CorrelatedNestedLoopJoin",
"JoinCommuteRule");
+ phys = physicalPlan(sql, publicSchema, planLsnr, "MergeJoinConverter",
"CorrelatedNestedLoopJoin", "JoinCommuteRule");
join = findFirstNode(phys, byClass(IgniteNestedLoopJoin.class));
proj = findFirstNode(phys, byClass(IgniteProject.class));
@@ -304,8 +335,7 @@ public class JoinCommutePlannerTest extends
AbstractPlannerTest {
// no commute
assertEquals(JoinRelType.INNER, join.getJoinType());
- ctx = plannerCtx(sql, publicSchema,
- "MergeJoinConverter", "CorrelatedNestedLoopJoin",
"JoinCommuteRule");
+ ctx = plannerCtx(sql, publicSchema, planLsnr, "MergeJoinConverter",
"CorrelatedNestedLoopJoin", "JoinCommuteRule");
pl = ctx.cluster().getPlanner();
@@ -314,5 +344,7 @@ public class JoinCommutePlannerTest extends
AbstractPlannerTest {
System.out.println("plan: " + RelOptUtil.toString(phys));
assertTrue(costWithCommute.isLt(costWithoutCommute));
+
+ assertFalse(planLsnr.ruleSucceeded());
}
}
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
index b03ba932e72..66af9f8a106 100644
---
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
@@ -19,15 +19,22 @@ package
org.apache.ignite.internal.processors.query.calcite.planner.hints;
import java.util.Arrays;
import java.util.function.Predicate;
+import java.util.stream.Stream;
+import org.apache.calcite.plan.RelOptRule;
import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.rules.CoreRules;
+import org.apache.calcite.rel.rules.JoinPushThroughJoinRule;
import org.apache.ignite.internal.processors.query.QueryUtils;
+import org.apache.ignite.internal.processors.query.calcite.RuleApplyListener;
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.prepare.PlannerHelper;
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.rule.logical.IgniteMultiJoinOptimizeRule;
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;
@@ -46,9 +53,12 @@ import static
org.apache.ignite.internal.processors.query.calcite.hint.HintDefin
* Planner test for index hints.
*/
public class JoinTypeHintPlannerTest extends AbstractPlannerTest {
- /** */
- private static final String[] CORE_JOIN_REORDER_RULES =
{"JoinCommuteRule", "JoinPushThroughJoinRule:left",
- "JoinPushThroughJoinRule:right"};
+ /** All the join order optimization rules (not only default/core). Should
be renamed in the future. */
+ private static final String[] CORE_JOIN_REORDER_RULES = Stream.of(
+ CoreRules.JOIN_COMMUTE,
+ JoinPushThroughJoinRule.LEFT, JoinPushThroughJoinRule.RIGHT,
+ CoreRules.JOIN_TO_MULTI_JOIN, IgniteMultiJoinOptimizeRule.INSTANCE
+ ).map(RelOptRule::toString).toArray(String[]::new);
/** */
private IgniteSchema schema;
@@ -76,7 +86,7 @@ public class JoinTypeHintPlannerTest extends
AbstractPlannerTest {
}
/**
- * Tests nested loop join is disabled by hint.
+ * Tests correlated nested loop join is disabled by hint.
*/
@Test
public void testDisableCNLJoin() throws Exception {
@@ -378,7 +388,7 @@ public class JoinTypeHintPlannerTest extends
AbstractPlannerTest {
*/
@Test
public void testTableHints() throws Exception {
- String sqlTpl = "SELECT %s A2.A, T3.V3, T1.V2 FROM (SELECT 1 AS A, 2
AS B) A2 JOIN TBL3 %s T3 ON A2.B=A2.B " +
+ String sqlTpl = "SELECT %s A2.A, T3.V3, T1.V2 FROM (SELECT 1 AS A, 2
AS B) A2 JOIN TBL3 %s T3 ON A2.B=T3.V2 " +
"JOIN TBL1 %s T1 on T3.V3=T1.V1 where T1.V2=5";
assertPlan(String.format(sqlTpl, "", "/*+ " + NL_JOIN + " */", "/*+ "
+ MERGE_JOIN + " */"), schema,
@@ -404,6 +414,65 @@ public class JoinTypeHintPlannerTest extends
AbstractPlannerTest {
.and(input(1, isTableScan("TBL3")))), CORE_JOIN_REORDER_RULES);
}
+ /**
+ * Test table join hints with join reordering.
+ */
+ @Test
+ public void testTableHintsWithReordering() throws Exception {
+ assert PlannerHelper.JOINS_COUNT_FOR_HEURISTIC_ORDER >= 3;
+
+ RuleApplyListener planLsnr = new
RuleApplyListener(IgniteMultiJoinOptimizeRule.INSTANCE);
+
+ // TBL3 leads, gets NL. TBL1 follows, gets MERGE. TBL5 is to keep
joins order enough to launch the optimization.
+ assertPlan("SELECT A2.A, T3.V3, T1.V2, T5.V3 FROM (SELECT 1 AS A, 2 AS
B) A2 JOIN TBL3 /*+ NL_JOIN */ T3 ON A2.B=T3.V2 " +
+ "JOIN TBL1 /*+ MERGE_JOIN */ T1 on T3.V1=T1.V1 JOIN TBL5 t5 on
T1.V1=T5.V1 where T1.V3=5 and T5.V1=1", schema, planLsnr,
+ checkJoinTypeAndInputScan(IgniteNestedLoopJoin.class, "TBL3")
+ .and(checkJoinTypeAndInputScan(IgniteMergeJoin.class, "TBL1"))
+ );
+
+ assertTrue(planLsnr.ruleSucceeded());
+
+ // TBL3 leads, gets top-level NL. TBL1 follows, gets MERGE. TBL5 is to
keep joins order enough to launch the optimization.
+ assertPlan("SELECT /*+ NL_JOIN */ A2.A, T3.V3, T1.V2, T5.V3 FROM
(SELECT 1 AS A, 2 AS B) A2 JOIN TBL3 T3 ON A2.B=T3.V2 " +
+ "JOIN TBL1 /*+ MERGE_JOIN */ T1 on T3.V1=T1.V1 JOIN TBL5 t5 on
T1.V1=T5.V1 where T1.V3=5 and T5.V1=1", schema, planLsnr,
+ checkJoinTypeAndInputScan(IgniteNestedLoopJoin.class, "TBL3")
+ .and(checkJoinTypeAndInputScan(IgniteMergeJoin.class, "TBL1"))
+ );
+
+ assertTrue(planLsnr.ruleSucceeded());
+
+ // TBL1 leads, gets NL. TBL3 follows, gets MERGE. TBL5 is to keep
joins order enough to launch the optimization.
+ assertPlan("SELECT A2.A, T1.V3, T3.V2, T5.V3 FROM (SELECT 1 AS A, 2 AS
B) A2 JOIN TBL1 /*+ NL_JOIN */ T1 ON A2.B=T1.V2 " +
+ "JOIN TBL3 /*+ MERGE_JOIN */ T3 on T1.V1=T3.V1 JOIN TBL5 t5 on
T3.V3=T5.V3 where T3.V3=5 and T5.V1=1", schema, planLsnr,
+ checkJoinTypeAndInputScan(IgniteNestedLoopJoin.class, "TBL1")
+ .and(checkJoinTypeAndInputScan(IgniteMergeJoin.class, "TBL3"))
+ );
+
+ assertTrue(planLsnr.ruleSucceeded());
+
+ // TBL1 leads, gets top-level NL. TBL3 follows, gets MERGE. TBL5 is to
keep joins order enough to launch the optimization.
+ assertPlan("SELECT /*+ NL_JOIN */ A2.A, T1.V3, T3.V2, T5.V1 FROM
(SELECT 1 AS A, 2 AS B) A2 JOIN TBL1 T1 ON A2.B=T1.V2 " +
+ "JOIN TBL3 /*+ MERGE_JOIN */ T3 on T1.V1=T3.V1 JOIN TBL5 t5 on
T3.V3=T5.V3 where T3.V3=5 and T5.V1=1", schema, planLsnr,
+ checkJoinTypeAndInputScan(IgniteNestedLoopJoin.class, "TBL1")
+ .and(checkJoinTypeAndInputScan(IgniteMergeJoin.class, "TBL3"))
+ );
+
+ assertTrue(planLsnr.ruleSucceeded());
+ }
+
+ /**
+ * Searches for a table scan named {@code tblName} in any input of join of
type {@code jType} without other joins
+ * in the middle.
+ */
+ private Predicate<RelNode> checkJoinTypeAndInputScan(Class<? extends
AbstractIgniteJoin> jType, String tblName) {
+ return nodeOrAnyChild(isInstanceOf(jType).and(
+ input(0,
nodeOrAnyChild(isInstanceOf(AbstractIgniteJoin.class)).negate())
+ .and(input(0, nodeOrAnyChild(isTableScan(tblName))))
+ .or(input(1,
nodeOrAnyChild(isInstanceOf(AbstractIgniteJoin.class)).negate())
+ .and(input(1, nodeOrAnyChild(isTableScan(tblName)))))
+ ));
+ }
+
/**
* Tests disable-join-hint works for a sub-query.
*/
@@ -468,6 +537,95 @@ public class JoinTypeHintPlannerTest extends
AbstractPlannerTest {
schema, predicateForNestedHintOverrides(true),
CORE_JOIN_REORDER_RULES);
}
+ /** */
+ @Test
+ public void testNestedHintOverridesWithReordering() throws Exception {
+ assert PlannerHelper.JOINS_COUNT_FOR_HEURISTIC_ORDER >= 3;
+
+ RuleApplyListener planLsnr = new
RuleApplyListener(IgniteMultiJoinOptimizeRule.INSTANCE);
+
+ assertPlan("SELECT /*+ " + MERGE_JOIN + " */ t1.v1, t2.v2 FROM TBL1 t1
JOIN TBL2 t2 on t1.v3=t2.v3 " +
+ "where t2.v1 in (SELECT /*+ " + NL_JOIN + "(TBL1) */ t3.v3
from TBL3 t3 JOIN TBL1 t4 on t3.v2=t4.v2)",
+ schema, planLsnr,
nodeOrAnyChild(isInstanceOf(IgniteMergeJoin.class)
+
.and(hasChildThat(isInstanceOf(IgniteNestedLoopJoin.class).and(hasNestedTableScan("TBL1")))))
+ );
+
+ assertTrue(planLsnr.ruleSucceeded());
+
+ assertPlan("SELECT /*+ " + MERGE_JOIN + " */ t1.v1, t2.v2 FROM TBL1 t1
JOIN TBL2 t2 on t1.v3=t2.v3 " +
+ "where t2.v1 in (SELECT /*+ " + NL_JOIN + "(TBL3) */ t3.v3
from TBL3 t3 JOIN TBL1 t4 on t3.v2=t4.v2)",
+ schema, planLsnr,
nodeOrAnyChild(isInstanceOf(IgniteMergeJoin.class)
+
.and(hasChildThat(isInstanceOf(IgniteNestedLoopJoin.class).and(hasNestedTableScan("TBL3")))))
+ );
+
+ assertTrue(planLsnr.ruleSucceeded());
+
+ assertPlan("SELECT /*+ " + MERGE_JOIN + " */ t1.v1, t2.v2 FROM TBL1 t1
JOIN TBL2 t2 on t1.v3=t2.v3 " +
+ "where t2.v1 in (SELECT /*+ " + NL_JOIN + " */ t3.v3 from TBL3
t3 JOIN TBL1 t4 on t3.v2=t4.v2)",
+ schema, planLsnr,
nodeOrAnyChild(isInstanceOf(IgniteMergeJoin.class)
+ .and(hasChildThat(isInstanceOf(IgniteNestedLoopJoin.class)
+ .and(hasNestedTableScan("TBL1"))
+ .and(hasNestedTableScan("TBL3")))))
+ );
+
+ assertTrue(planLsnr.ruleSucceeded());
+
+ assertPlan("SELECT /*+ " + NL_JOIN + " */ t1.v1, t2.v2 FROM TBL1 t1
JOIN TBL2 t2 on t1.v3=t2.v3 " +
+ "where t2.v1 in (SELECT /*+ " + MERGE_JOIN + "(TBL1) */ t3.v3
from TBL3 t3 JOIN TBL1 t4 on t3.v2=t4.v2)",
+ schema, planLsnr,
nodeOrAnyChild(isInstanceOf(IgniteNestedLoopJoin.class)
+ .and(hasChildThat(isInstanceOf(IgniteMergeJoin.class)
+ .and(hasNestedTableScan("TBL1")))))
+ );
+
+ assertTrue(planLsnr.ruleSucceeded());
+
+ assertPlan("SELECT /*+ " + NL_JOIN + " */ t1.v1, t2.v2 FROM TBL1 t1
JOIN TBL2 t2 on t1.v3=t2.v3 " +
+ "where t2.v1 in (SELECT /*+ " + MERGE_JOIN + "(TBL3) */ t3.v3
from TBL3 t3 JOIN TBL1 t4 on t3.v2=t4.v2)",
+ schema, planLsnr,
nodeOrAnyChild(isInstanceOf(IgniteNestedLoopJoin.class)
+
.and(hasChildThat(isInstanceOf(IgniteMergeJoin.class).and(hasNestedTableScan("TBL3")))))
+ );
+
+ assertTrue(planLsnr.ruleSucceeded());
+
+ assertPlan("SELECT /*+ " + NL_JOIN + " */ t1.v1, t2.v2 FROM TBL1 t1
JOIN TBL2 t2 on t1.v3=t2.v3 " +
+ "where t2.v1 in (SELECT /*+ " + MERGE_JOIN + " */ t3.v3 from
TBL3 t3 JOIN TBL1 t4 on t3.v2=t4.v2)",
+ schema, planLsnr,
nodeOrAnyChild(isInstanceOf(IgniteNestedLoopJoin.class)
+
.and(hasChildThat(isInstanceOf(IgniteMergeJoin.class).and(hasNestedTableScan("TBL1"))
+ .and(hasNestedTableScan("TBL3")))))
+ );
+
+ assertTrue(planLsnr.ruleSucceeded());
+
+ // Produces unsupported left join.
+ assertPlan("SELECT /*+ " + NL_JOIN + " */ t1.v1, t2.v2 FROM TBL1 t1
JOIN TBL2 t2 on t1.v3=t2.v3 " +
+ "where t2.v1 not in (SELECT /*+ " + MERGE_JOIN + " */ t3.v3
from TBL3 t3 JOIN TBL1 t4 on t3.v2=t4.v2)",
+ schema, planLsnr,
nodeOrAnyChild(isInstanceOf(IgniteNestedLoopJoin.class)
+
.and(hasChildThat(isInstanceOf(IgniteMergeJoin.class).and(hasNestedTableScan("TBL1"))
+ .and(hasNestedTableScan("TBL3")))))
+ );
+
+ assertFalse(planLsnr.ruleSucceeded());
+
+ // Produces unsupported outer join.
+ assertPlan("SELECT /*+ " + NL_JOIN + " */ t1.v1, t2.v2 FROM TBL1 t1
FULL OUTER JOIN TBL2 t2 on t1.v3=t2.v3 " +
+ "where t2.v1 in (SELECT /*+ " + MERGE_JOIN + " */ t3.v3 from
TBL3 t3 JOIN TBL1 t4 on t3.v2=t4.v2)",
+ schema, planLsnr,
nodeOrAnyChild(isInstanceOf(IgniteNestedLoopJoin.class)
+
.and(hasChildThat(isInstanceOf(IgniteMergeJoin.class).and(hasNestedTableScan("TBL1"))
+ .and(hasNestedTableScan("TBL3")))))
+ );
+
+ assertFalse(planLsnr.ruleSucceeded());
+
+ // Produces unsupported outer join.
+ assertPlan("SELECT /*+ " + MERGE_JOIN + " */ t1.v1, t2.v2 FROM TBL1 t1
JOIN TBL2 t2 on t1.v3=t2.v3 where " +
+ "t2.v1 in (SELECT /*+ " + NL_JOIN + "(TBL3) */ t3.v3 from TBL3
t3 FULL OUTER JOIN TBL1 t4 on t3.v2=t4.v2)",
+ schema, planLsnr,
nodeOrAnyChild(isInstanceOf(IgniteMergeJoin.class)
+
.and(hasChildThat(isInstanceOf(IgniteNestedLoopJoin.class).and(hasNestedTableScan("TBL3")))))
+ );
+
+ assertFalse(planLsnr.ruleSucceeded());
+ }
+
/**
* @return A {@link Predicate} for {@link #testNestedHintOverrides()}
*/
diff --git
a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/rules/JoinOrderOptimizationTest.java
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/rules/JoinOrderOptimizationTest.java
new file mode 100644
index 00000000000..c5396eb3767
--- /dev/null
+++
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/rules/JoinOrderOptimizationTest.java
@@ -0,0 +1,234 @@
+/*
+ * 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.rules;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import org.apache.calcite.rel.rules.JoinToMultiJoinRule;
+import org.apache.ignite.internal.processors.query.calcite.QueryChecker;
+import org.apache.ignite.internal.processors.query.calcite.RuleApplyListener;
+import
org.apache.ignite.internal.processors.query.calcite.integration.AbstractBasicIntegrationTest;
+import
org.apache.ignite.internal.processors.query.calcite.rule.logical.IgniteMultiJoinOptimizeRule;
+import org.apache.ignite.internal.util.typedef.F;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import static
org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition.ENFORCE_JOIN_ORDER;
+
+/**
+ * Test of joins order heuristic optimization.
+ *
+ * @see JoinToMultiJoinRule
+ * @see IgniteMultiJoinOptimizeRule
+ */
+@RunWith(Parameterized.class)
+public class JoinOrderOptimizationTest extends AbstractBasicIntegrationTest {
+ /** Test query. */
+ @Parameterized.Parameter
+ public String qry;
+
+ /** Test queries. */
+ @Parameterized.Parameters
+ public static Collection<String> runConfig() {
+ return testQueries();
+ }
+
+ /** {@inheritDoc} */
+ @Override protected void beforeTestsStarted() throws Exception {
+ super.beforeTestsStarted();
+
+ initSchema();
+
+ gatherStatistics();
+ }
+
+ /** {@inheritDoc} */
+ @Override protected boolean destroyCachesAfterTest() {
+ return false;
+ }
+
+ /** */
+ private void initSchema() {
+ int warehsSz = 50;
+ int catgSz = 100;
+ int reviewSz = 50000;
+ int prodSz = 100;
+ int discSz = 2000;
+ int shipSz = 15000;
+ int usrSz = 10000;
+ int ordSz = 20000;
+ int ordDetSz = 100000;
+
+ sql("CREATE TABLE Products (ProdId INT PRIMARY KEY, ProdNm
VARCHAR(100), Price DECIMAL(10, 2)) WITH \"VALUE_TYPE='PROD'\"");
+ sql("INSERT INTO Products SELECT x, 'Product_' || x::VARCHAR, 100.0 +
x % 100.0 FROM system_range(1, ?)",
+ prodSz);
+
+ sql("CREATE TABLE ProductCategories (ProdCatgId INT PRIMARY KEY,
ProdId INT, CatgId INT)");
+ sql("INSERT INTO ProductCategories SELECT x, 1 + RAND_INTEGER(?), 1 +
RAND_INTEGER(?) FROM system_range(1, ?)",
+ prodSz - 1, catgSz - 1, prodSz);
+
+ sql("CREATE TABLE Discounts (DiscId INT PRIMARY KEY, ProdId INT,
DiscPercent DECIMAL(5, 2), ValidUntil DATE)");
+ sql("INSERT INTO Discounts SELECT x, 1 + RAND_INTEGER(?), 1 + x % 15,
date '2025-02-10' + (x % 365)::INTERVAL DAYS " +
+ "FROM system_range(1, ?)", prodSz - 1, discSz);
+
+ sql("CREATE TABLE Shipping (ShippId INT PRIMARY KEY, OrdId INT,
ShippDate DATE, ShippAddrs VARCHAR(255))");
+ sql("INSERT INTO Shipping SELECT x, 1 + RAND_INTEGER(?), date
'2020-01-01' + RAND_INTEGER(365)::INTERVAL DAYS, "
+ + " 'Addrs_' || x::VARCHAR FROM system_range(1, ?)", ordSz - 1,
shipSz);
+
+ sql("CREATE TABLE Users (UsrId INT PRIMARY KEY, UsrNm VARCHAR(100),
Email VARCHAR(100)) WITH \"VALUE_TYPE='USR'\"");
+ sql("INSERT INTO Users SELECT x, 'User_' || x::VARCHAR, 'email_' ||
x::VARCHAR || '@nowhere.xyz' FROM system_range(1, ?)",
+ usrSz);
+
+ sql("CREATE TABLE Orders (OrdId INT PRIMARY KEY, UsrId INT, ProdId
INT, OrdDate DATE, TotalAmount DECIMAL(10, 2)) " +
+ "WITH \"VALUE_TYPE='ORD'\"");
+ sql("INSERT INTO Orders SELECT x, 1 + RAND_INTEGER(?), 1 +
RAND_INTEGER(?), date '2025-02-10' + (x % 365)::INTERVAL DAYS, " +
+ "1 + x % 10 FROM system_range(1, ?)", usrSz - 1, prodSz, ordSz);
+
+ sql("CREATE TABLE OrderDetails (OrdDetId INT PRIMARY KEY, OrdId INT,
ProdId INT, Qnty INT) WITH \"VALUE_TYPE='ORD_DET'\"");
+ sql("INSERT INTO OrderDetails SELECT x, 1 + RAND_INTEGER(?), 1 +
RAND_INTEGER(?), 1 + x % 10 FROM system_range(1, ?)",
+ ordSz - 1, prodSz - 1, ordDetSz);
+
+ sql("CREATE TABLE Warehouses (WrhId INT PRIMARY KEY, WrhNm
VARCHAR(100), LocNm VARCHAR(100))");
+ sql("INSERT INTO Warehouses SELECT x, 'Wrh_' || x::VARCHAR,
'Location_' || x::VARCHAR FROM system_range(1, ?)", warehsSz);
+
+ sql("CREATE TABLE Categories (CatgId INT PRIMARY KEY, CatgName
VARCHAR(100))");
+ sql("INSERT INTO Categories SELECT x, 'Category_' || x::VARCHAR FROM
system_range(1, ?)", catgSz);
+
+ sql("CREATE TABLE Reviews (RevId INT PRIMARY KEY, ProdId INT, usrId
INT, RevTxt VARCHAR, Rating INT)");
+ sql("INSERT INTO Reviews SELECT x, 1 + RAND_INTEGER(?), 1 +
RAND_INTEGER(?), 'Prod. review ' || x::VARCHAR, 1 + RAND_INTEGER(4) " +
+ "FROM system_range(1, ?)", prodSz - 1, usrSz - 1, reviewSz);
+ }
+
+ /** Tests that query result doesn't change with the joins order
optimization. */
+ @Test
+ public void testTheSameResults() {
+ assert !qry.contains(ENFORCE_JOIN_ORDER.name());
+ assert qry.startsWith("SELECT ");
+
+ String qryFixedJoins = qry.replaceAll("SELECT", "SELECT /*+ " +
ENFORCE_JOIN_ORDER + " */");
+
+ RuleApplyListener planLsnr = new
RuleApplyListener(IgniteMultiJoinOptimizeRule.INSTANCE);
+
+ // First, call with fixed join order.
+ List<List<?>> expectedResult = new ArrayList<>();
+
+
assertQuery(qryFixedJoins).withPlannerListener(planLsnr).withResultChecker(expectedResult::addAll).check();
+
+ // Ensure that the optimization rule wasn't fired.
+ assertFalse(planLsnr.ruleSucceeded());
+
+ assertFalse(expectedResult.isEmpty());
+
+ QueryChecker checker = assertQuery(qry).withPlannerListener(planLsnr);
+
+ // Make sure that the optimized query has the same results.
+ expectedResult.forEach(row -> checker.returns(row.toArray()));
+
+ checker.check();
+
+ // Ensure that the optimization rule has worked.
+ assertTrue(planLsnr.ruleSucceeded());
+ }
+
+ /** */
+ private static Collection<String> testQueries() {
+ return F.asList(
+ // User orders with products in multiple categories.
+ "SELECT \n"
+ + " U.UsrNm, O.OrdId, COUNT(DISTINCT C.CatgName) AS
Categories\n"
+ + " FROM Users U, Orders O, OrderDetails OD, Products P,
ProductCategories PC, Categories C\n"
+ + " WHERE U.UsrId = O.UsrId\n"
+ + " AND O.OrdId = OD.OrdId\n"
+ + " AND OD.ProdId = P.ProdId\n"
+ + " AND P.ProdId = PC.ProdId\n"
+ + " AND PC.CatgId = C.CatgId\n"
+ + " GROUP BY U.UsrNm, O.OrdId",
+
+ // Orders with total revenue and shipping details.
+ "SELECT \n"
+ + " O.OrdId, O.OrdDate, S.ShippAddrs, SUM(OD.Qnty *
P.Price) AS TotalOrderValue\n"
+ + " FROM Orders O, Shipping S, OrderDetails OD, Products P\n"
+ + "WHERE O.OrdId = S.OrdId\n"
+ + " AND O.OrdId = OD.OrdId\n"
+ + " AND OD.ProdId = P.ProdId\n"
+ + "GROUP BY O.OrdId, O.OrdDate, S.ShippAddrs",
+
+ // Products in warehouses by category.
+ "SELECT \n"
+ + " W.WrhNm, C.CatgName, P.ProdNm\n"
+ + " FROM Warehouses W, Products P, ProductCategories PC,
Categories C\n"
+ + "WHERE W.WrhId = (P.ProdId % 5 + 1)\n"
+ + " AND P.ProdId = PC.ProdId\n"
+ + " AND PC.CatgId = C.CatgId",
+
+ // Average product rating by category.
+ "SELECT \n"
+ + " C.CatgName, P.ProdNm, AVG(R.Rating) AS AvgRating\n"
+ + " FROM Categories C, ProductCategories PC, Products P,
Reviews R\n"
+ + "WHERE C.CatgId = PC.CatgId\n"
+ + " AND PC.ProdId = P.ProdId\n"
+ + " AND P.ProdId = R.ProdId\n"
+ + "GROUP BY C.CatgName, P.ProdNm",
+
+ // Products ordered by user.
+ "SELECT \n"
+ + " U.UsrNm, P.ProdNm, SUM(OD.Qnty) AS TotalQnty\n"
+ + " FROM Users U, Orders O, OrderDetails OD, Products P\n"
+ + "WHERE U.UsrId = O.UsrId\n"
+ + " AND O.OrdId = OD.OrdId\n"
+ + " AND OD.ProdId = P.ProdId\n"
+ + "GROUP BY U.UsrNm, P.ProdNm",
+
+ // Joining of all the tables.
+ "SELECT \n"
+ + " U.UsrId, U.UsrNm, O.OrdId, O.OrdDate, P.ProdNm,
OD.Qnty, \n"
+ + " C.CatgName, S.ShippAddrs, R.Rating, D.DiscPercent,
W.WrhNm\n"
+ + " FROM Users U, Orders O, OrderDetails OD, Products P,
ProductCategories PC, Categories C, \n"
+ + " Shipping S, Reviews R, Discounts D, Warehouses W\n"
+ + "WHERE U.UsrId = O.UsrId\n"
+ + " AND O.OrdId = OD.OrdId\n"
+ + " AND OD.ProdId = P.ProdId\n"
+ + " AND P.ProdId = PC.ProdId\n"
+ + " AND PC.CatgId = C.CatgId\n"
+ + " AND O.OrdId = S.OrdId\n"
+ + " AND P.ProdId = R.ProdId"
+ + " AND U.UsrId = R.UsrId\n"
+ + " AND P.ProdId = D.ProdId\n"
+ + " AND W.WrhId = (P.ProdId % 5 + 1)",
+
+ // Correlated query.
+ "SELECT OD.OrdDetId, O.OrdId, P.ProdId, U.UsrNm, " +
+ "(SELECT MAX(C.ShippDate) from Shipping C where
C.OrdId=OD.OrdId) as corr " +
+ "from OrderDetails OD " +
+ "JOIN Orders O on O.OrdId = OD.OrdId " +
+ "JOIN Products P on P.ProdId = OD.ProdId " +
+ "JOIN Users U on O.UsrId = U.UsrId",
+
+ // Yet another correlated query.
+ "SELECT S.ShippDate, O.OrdId, P.ProdId, U.UsrNm, " +
+ "(SELECT MAX(C.Qnty) from OrderDetails C where C.OrdId=O.OrdId
and C.ProdId=O.ProdId) as corr, " +
+ "(SELECT AVG(C2.TotalAmount) from Orders C2 where
C2.ProdId=O.ProdId) as corr2 " +
+ "from Shipping S " +
+ "JOIN Orders O on O.OrdId = S.OrdId " +
+ "JOIN Products P on P.ProdId = O.ProdId " +
+ "JOIN Users U on O.UsrId = U.UsrId"
+ );
+ }
+}
diff --git
a/modules/calcite/src/test/java/org/apache/ignite/testsuites/IntegrationTestSuite.java
b/modules/calcite/src/test/java/org/apache/ignite/testsuites/IntegrationTestSuite.java
index f7b3744b789..954d7a4a883 100644
---
a/modules/calcite/src/test/java/org/apache/ignite/testsuites/IntegrationTestSuite.java
+++
b/modules/calcite/src/test/java/org/apache/ignite/testsuites/IntegrationTestSuite.java
@@ -80,6 +80,7 @@ import
org.apache.ignite.internal.processors.query.calcite.integration.tpch.Tpch
import
org.apache.ignite.internal.processors.query.calcite.jdbc.JdbcCrossEngineTest;
import org.apache.ignite.internal.processors.query.calcite.jdbc.JdbcQueryTest;
import
org.apache.ignite.internal.processors.query.calcite.rules.JoinCommuteRulesTest;
+import
org.apache.ignite.internal.processors.query.calcite.rules.JoinOrderOptimizationTest;
import
org.apache.ignite.internal.processors.query.calcite.rules.OrToUnionRuleTest;
import
org.apache.ignite.internal.processors.query.calcite.rules.ProjectScanMergeRuleTest;
import
org.apache.ignite.internal.processors.query.calcite.thin.MultiLineQueryTest;
@@ -125,6 +126,7 @@ import org.junit.runners.Suite;
UnstableTopologyIntegrationTest.class,
PartitionsReservationIntegrationTest.class,
JoinCommuteRulesTest.class,
+ JoinOrderOptimizationTest.class,
ServerStatisticsIntegrationTest.class,
JoinIntegrationTest.class,
IntervalTest.class,