Skip to content

Commit

Permalink
IGNITE-20323 SQL Calcite: Add SQL hint for join type - Fixes #10918.
Browse files Browse the repository at this point in the history
Signed-off-by: Aleksey Plekhanov <[email protected]>
  • Loading branch information
Vladsz83 authored and alex-plekhanov committed Dec 29, 2023
1 parent 69f052e commit c94a2d5
Show file tree
Hide file tree
Showing 22 changed files with 882 additions and 76 deletions.
37 changes: 34 additions & 3 deletions docs/_docs/SQL/sql-calcite.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
};

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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<LogicalJoin> {
/** Known join type hints and the opposite hints. */
private static final EnumMap<HintDefinition, HintDefinition> 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<String, Collection<HintDefinition>> hintedTables = new HashMap<>();

Set<String> joinTbls = joinTblNames(join);

assert joinTbls.size() < 3;

for (RelHint hint : HintUtils.hints(join, ALL_HINTS)) {
Set<String> 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<HintDefinition> prevTblHints = hintedTables.get(tbl);

if (prevTblHints == null)
continue;

Set<HintDefinition> 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<String> joinTblNames(Join join) {
Set<String> 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<LogicalJoin> {
public class CorrelatedNestedLoopJoinRule extends AbstractIgniteJoinConverterRule {
/** */
public static final RelOptRule INSTANCE = new CorrelatedNestedLoopJoinRule(1);

Expand All @@ -57,7 +58,7 @@ public class CorrelatedNestedLoopJoinRule extends AbstractIgniteConverterRule<Lo

/** */
public CorrelatedNestedLoopJoinRule(int batchSize) {
super(LogicalJoin.class, "CorrelatedNestedLoopJoin");
super("CorrelatedNestedLoopJoin", HintDefinition.CNL_JOIN);

this.batchSize = batchSize;
}
Expand Down Expand Up @@ -141,9 +142,14 @@ public CorrelatedNestedLoopJoinRule(int batchSize) {
}

/** {@inheritDoc} */
@Override public boolean matches(RelOptRuleCall call) {
@Override public boolean matchesJoin(RelOptRuleCall call) {
LogicalJoin join = call.rel(0);

return join.getJoinType() == JoinRelType.INNER || join.getJoinType() == JoinRelType.LEFT;
return supportedJoinType(join.getJoinType());
}

/** */
private static boolean supportedJoinType(JoinRelType type) {
return type == JoinRelType.INNER || type == JoinRelType.LEFT;
}
}
Loading

0 comments on commit c94a2d5

Please sign in to comment.