blob: 92b99a831258effb015d967e795c8605931e8247 [file] [log] [blame]
/*
* 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.sql.engine.prepare;
import static org.apache.ignite.internal.sql.engine.hint.IgniteHint.DISABLE_RULE;
import static org.apache.ignite.internal.sql.engine.hint.IgniteHint.ENFORCE_JOIN_ORDER;
import static org.apache.ignite.internal.sql.engine.util.Commons.shortRuleName;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.apache.calcite.plan.RelOptPlanner.CannotPlanException;
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.logical.LogicalCorrelate;
import org.apache.calcite.rel.rules.CoreRules;
import org.apache.calcite.rex.RexBuilder;
import org.apache.calcite.rex.RexNode;
import org.apache.calcite.sql.SqlNode;
import org.apache.calcite.util.Pair;
import org.apache.ignite.internal.logger.IgniteLogger;
import org.apache.ignite.internal.logger.Loggers;
import org.apache.ignite.internal.sql.engine.hint.Hints;
import org.apache.ignite.internal.sql.engine.rel.IgniteConvention;
import org.apache.ignite.internal.sql.engine.rel.IgniteProject;
import org.apache.ignite.internal.sql.engine.rel.IgniteRel;
import org.apache.ignite.internal.sql.engine.trait.IgniteDistributions;
import org.apache.ignite.lang.ErrorGroups.Common;
import org.apache.ignite.sql.SqlException;
/**
* Utility class that encapsulates the query optimization pipeline.
*/
public final class PlannerHelper {
/**
* Maximum number of tables in join supported for join order optimization.
*
* <p>If query joins more table than specified, then rules {@link CoreRules#JOIN_COMMUTE} and
* {@link CoreRules#JOIN_COMMUTE_OUTER} will be disabled, tables will be joined in the order
* of enumeration in the query.
*/
private static final int MAX_SIZE_OF_JOIN_TO_OPTIMIZE = 5;
private static final IgniteLogger LOG = Loggers.forClass(PlannerHelper.class);
/**
* Default constructor.
*/
private PlannerHelper() {
}
/**
* Optimizes a given query.
*
* <p>That is, it passes a given AST through the optimization pipeline,
* applying different rule sets step by step, and returns the optimized
* physical tree of Ignite relations.
*
* @param sqlNode Validated AST of the query to optimize.
* @param planner A planner used to apply a rule set to the query tree.
* @return An optimized physical tree of Ignite relations.
*/
public static IgniteRel optimize(SqlNode sqlNode, IgnitePlanner planner) {
try {
// Convert to Relational operators graph
RelRoot root = planner.rel(sqlNode);
RelNode rel = root.rel;
Hints hints = Hints.parse(root.hints);
List<String> disableRuleParams = hints.params(DISABLE_RULE);
if (!disableRuleParams.isEmpty()) {
planner.setDisabledRules(Set.copyOf(disableRuleParams));
}
// Transformation chain
rel = planner.transform(PlannerPhase.HEP_SUBQUERIES_TO_CORRELATES, rel.getTraitSet(), rel);
rel = RelOptUtil.propagateRelHints(rel, false);
rel = planner.replaceCorrelatesCollisions(rel);
rel = planner.trimUnusedFields(root.withRel(rel)).rel;
boolean amountOfJoinsAreBig = hasTooMuchJoins(rel);
boolean enforceJoinOrder = hints.present(ENFORCE_JOIN_ORDER);
if (amountOfJoinsAreBig || enforceJoinOrder) {
Set<String> disabledRules = new HashSet<>(disableRuleParams);
disabledRules.add(shortRuleName(CoreRules.JOIN_COMMUTE));
disabledRules.add(shortRuleName(CoreRules.JOIN_COMMUTE_OUTER));
planner.setDisabledRules(Set.copyOf(disabledRules));
}
rel = planner.transform(PlannerPhase.HEP_FILTER_PUSH_DOWN, rel.getTraitSet(), rel);
rel = planner.transform(PlannerPhase.HEP_PROJECT_PUSH_DOWN, rel.getTraitSet(), rel);
{
// the sole purpose of this code block is to limit scope of `simpleOperation` variable.
// The result of `HEP_TO_SIMPLE_KEY_VALUE_OPERATION` phase MUST NOT be passed to next stage,
// thus if result meets our expectation, then return the result, otherwise discard it and
// proceed with regular flow
RelNode simpleOperation = planner.transform(PlannerPhase.HEP_TO_SIMPLE_KEY_VALUE_OPERATION, rel.getTraitSet(), rel);
if (simpleOperation instanceof IgniteRel) {
return (IgniteRel) simpleOperation;
}
}
RelTraitSet desired = rel.getCluster().traitSet()
.replace(IgniteConvention.INSTANCE)
.replace(IgniteDistributions.single())
.replace(root.collation == null ? RelCollations.EMPTY : root.collation)
.simplify();
IgniteRel igniteRel = planner.transform(PlannerPhase.OPTIMIZATION, desired, rel);
if (!root.isRefTrivial()) {
List<RexNode> projects = new ArrayList<>();
RexBuilder rexBuilder = igniteRel.getCluster().getRexBuilder();
for (int field : Pair.left(root.fields)) {
projects.add(rexBuilder.makeInputRef(igniteRel, field));
}
igniteRel = new IgniteProject(igniteRel.getCluster(), desired, igniteRel, projects, root.validatedRowType);
}
return igniteRel;
} catch (Throwable ex) {
if (LOG.isDebugEnabled()) {
LOG.debug("Unexpected error at query optimizer", ex);
LOG.debug(planner.dump());
}
if (ex instanceof CannotPlanException) {
throw ex;
} else if (ex.getClass() == RuntimeException.class && ex.getCause() instanceof SqlException) {
SqlException sqlEx = (SqlException) ex.getCause();
throw new SqlException(sqlEx.traceId(), sqlEx.code(), sqlEx.getMessage(), ex);
} else {
throw new SqlException(Common.INTERNAL_ERR, "Unable to optimize plan due to internal error", ex);
}
}
}
private static boolean hasTooMuchJoins(RelNode rel) {
JoinSizeFinder joinSizeFinder = new JoinSizeFinder();
joinSizeFinder.visit(rel);
return joinSizeFinder.sizeOfBiggestJoin() > MAX_SIZE_OF_JOIN_TO_OPTIMIZE;
}
/**
* A shuttle to estimate a biggest join to optimize.
*
* <p>There are only two rules: <ol>
* <li>Each achievable leaf node contribute to join complexity, thus must be counted</li>
* <li>If this shuttle reach the {@link LogicalCorrelate} node, only left shoulder will be
* analysed by this shuttle. For right shoulder a new shuttle will be created, and maximum
* of previously found subquery and current one will be saved</li>
* </ol>
*/
private static class JoinSizeFinder extends RelHomogeneousShuttle {
private int countOfSources = 0;
private int maxCountOfSourcesInSubQuery = 0;
/** {@inheritDoc} */
@Override
public RelNode visit(RelNode other) {
if (other.getInputs().isEmpty()) {
countOfSources++;
return other;
}
return super.visit(other);
}
/** {@inheritDoc} */
@Override
public RelNode visit(LogicalCorrelate correlate) {
JoinSizeFinder inSubquerySizeFinder = new JoinSizeFinder();
inSubquerySizeFinder.visit(correlate.getInput(1));
maxCountOfSourcesInSubQuery = Math.max(
maxCountOfSourcesInSubQuery,
inSubquerySizeFinder.sizeOfBiggestJoin()
);
return visitChild(correlate, 0, correlate.getInput(0));
}
int sizeOfBiggestJoin() {
return Math.max(countOfSources, maxCountOfSourcesInSubQuery);
}
}
}