blob: 9b3459e36b4c1bf996374996efaf76169c2bdcd3 [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.doris.planner;
import org.apache.doris.analysis.Analyzer;
import org.apache.doris.analysis.Expr;
import org.apache.doris.analysis.ExprSubstitutionMap;
import org.apache.doris.analysis.SlotId;
import org.apache.doris.analysis.TupleDescriptor;
import org.apache.doris.analysis.TupleId;
import org.apache.doris.common.AnalysisException;
import org.apache.doris.common.TreeNode;
import org.apache.doris.common.UserException;
import org.apache.doris.thrift.TExplainLevel;
import org.apache.doris.thrift.TPlan;
import org.apache.doris.thrift.TPlanNode;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.common.math.LongMath;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
/**
* Each PlanNode represents a single relational operator
* and encapsulates the information needed by the planner to
* make optimization decisions.
* <p/>
* finalize(): Computes internal state, such as keys for scan nodes; gets called once on
* the root of the plan tree before the call to toThrift(). Also finalizes the set
* of conjuncts, such that each remaining one requires all of its referenced slots to
* be materialized (ie, can be evaluated by calling GetValue(), rather than being
* implicitly evaluated as part of a scan key).
* <p/>
* conjuncts: Each node has a list of conjuncts that can be executed in the context of
* this node, ie, they only reference tuples materialized by this node or one of
* its children (= are bound by tupleIds).
*/
abstract public class PlanNode extends TreeNode<PlanNode> {
private final static Logger LOG = LogManager.getLogger(PlanNode.class);
protected String planNodeName;
protected PlanNodeId id; // unique w/in plan tree; assigned by planner
protected PlanFragmentId fragmentId; // assigned by planner after fragmentation step
protected long limit; // max. # of rows to be returned; 0: no limit
// ids materialized by the tree rooted at this node
protected ArrayList<TupleId> tupleIds;
// ids of the TblRefs "materialized" by this node; identical with tupleIds_
// if the tree rooted at this node only materializes BaseTblRefs;
// useful during plan generation
protected ArrayList<TupleId> tblRefIds;
// A set of nullable TupleId produced by this node. It is a subset of tupleIds.
// A tuple is nullable within a particular plan tree if it's the "nullable" side of
// an outer join, which has nothing to do with the schema.
protected Set<TupleId> nullableTupleIds = Sets.newHashSet();
protected List<Expr> conjuncts = Lists.newArrayList();
// Fragment that this PlanNode is executed in. Valid only after this PlanNode has been
// assigned to a fragment. Set and maintained by enclosing PlanFragment.
protected PlanFragment fragment_;
// estimate of the output cardinality of this node; set in computeStats();
// invalid: -1
protected long cardinality;
// number of nodes on which the plan tree rooted at this node would execute;
// set in computeStats(); invalid: -1
protected int numNodes;
// sum of tupleIds' avgSerializedSizes; set in computeStats()
protected float avgRowSize;
// Node should compact data.
protected boolean compactData;
protected int numInstances;
protected PlanNode(PlanNodeId id, ArrayList<TupleId> tupleIds, String planNodeName) {
this.id = id;
this.limit = -1;
// make a copy, just to be on the safe side
this.tupleIds = Lists.newArrayList(tupleIds);
this.tblRefIds = Lists.newArrayList(tupleIds);
this.cardinality = -1;
this.planNodeName = planNodeName;
this.numInstances = 1;
}
protected PlanNode(PlanNodeId id, String planNodeName) {
this.id = id;
this.limit = -1;
this.tupleIds = Lists.newArrayList();
this.tblRefIds = Lists.newArrayList();
this.cardinality = -1;
this.planNodeName = planNodeName;
this.numInstances = 1;
}
/**
* Copy c'tor. Also passes in new id.
*/
protected PlanNode(PlanNodeId id, PlanNode node, String planNodeName) {
this.id = id;
this.limit = node.limit;
this.tupleIds = Lists.newArrayList(node.tupleIds);
this.tblRefIds = Lists.newArrayList(node.tblRefIds);
this.nullableTupleIds = Sets.newHashSet(node.nullableTupleIds);
this.conjuncts = Expr.cloneList(node.conjuncts, null);
this.cardinality = -1;
this.compactData = node.compactData;
this.planNodeName = planNodeName;
this.numInstances = 1;
}
/**
* Sets tblRefIds_, tupleIds_, and nullableTupleIds_.
* The default implementation is a no-op.
*/
public void computeTupleIds() {
Preconditions.checkState(children.isEmpty() || !tupleIds.isEmpty());
}
/**
* Clears tblRefIds_, tupleIds_, and nullableTupleIds_.
*/
protected void clearTupleIds() {
tblRefIds.clear();
tupleIds.clear();
nullableTupleIds.clear();
}
protected void setPlanNodeName(String s) {
this.planNodeName = s;
}
public PlanNodeId getId() {
return id;
}
public void setId(PlanNodeId id) {
Preconditions.checkState(this.id == null);
this.id = id;
}
public PlanFragmentId getFragmentId() {
return fragment_.getFragmentId();
}
public void setFragmentId(PlanFragmentId id) {
fragmentId = id;
}
public void setFragment(PlanFragment fragment) { fragment_ = fragment; }
public PlanFragment getFragment() { return fragment_; }
public long getLimit() {
return limit;
}
/**
* Set the limit to the given limit only if the limit hasn't been set, or the new limit
* is lower.
*
* @param limit
*/
public void setLimit(long limit) {
if (this.limit == -1 || (limit != -1 && this.limit > limit)) {
this.limit = limit;
}
}
public boolean hasLimit() {
return limit > -1;
}
public long getCardinality() {
return cardinality;
}
public int getNumNodes() {
return numNodes;
}
public float getAvgRowSize() {
return avgRowSize;
}
/**
* Set the value of compactData in all children.
*/
public void setCompactData(boolean on) {
this.compactData = on;
for (PlanNode child : this.getChildren()) {
child.setCompactData(on);
}
}
public void unsetLimit() {
limit = -1;
}
public ArrayList<TupleId> getTupleIds() {
Preconditions.checkState(tupleIds != null);
return tupleIds;
}
public ArrayList<TupleId> getTblRefIds() {
return tblRefIds;
}
public void setTblRefIds(ArrayList<TupleId> ids) {
tblRefIds = ids;
}
public Set<TupleId> getNullableTupleIds() {
Preconditions.checkState(nullableTupleIds != null);
return nullableTupleIds;
}
public List<Expr> getConjuncts() {
return conjuncts;
}
public void addConjuncts(List<Expr> conjuncts) {
if (conjuncts == null) {
return;
}
this.conjuncts.addAll(conjuncts);
}
public void transferConjuncts(PlanNode recipient) {
recipient.conjuncts.addAll(conjuncts);
conjuncts.clear();
}
/**
* Call computeMemLayout() for all materialized tuples.
*/
protected void computeMemLayout(Analyzer analyzer) {
for (TupleId id: tupleIds) {
analyzer.getDescTbl().getTupleDesc(id).computeMemLayout();
}
}
/**
* Computes and returns the sum of two cardinalities. If an overflow occurs,
* the maximum Long value is returned (Long.MAX_VALUE).
*/
public static long addCardinalities(long a, long b) {
try {
return LongMath.checkedAdd(a, b);
} catch (ArithmeticException e) {
LOG.warn("overflow when adding cardinalities: " + a + ", " + b);
return Long.MAX_VALUE;
}
}
public String getExplainString() {
return getExplainString("", "", TExplainLevel.VERBOSE);
}
/**
* Generate the explain plan tree. The plan will be in the form of:
* <p/>
* root
* |
* |----child 2
* | limit:1
* |
* |----child 3
* | limit:2
* |
* child 1
* <p/>
* The root node header line will be prefixed by rootPrefix and the remaining plan
* output will be prefixed by prefix.
*/
protected final String getExplainString(String rootPrefix, String prefix,
TExplainLevel detailLevel) {
StringBuilder expBuilder = new StringBuilder();
String detailPrefix = prefix;
boolean traverseChildren = children != null
&& children.size() > 0
&& !(this instanceof ExchangeNode);
// if (children != null && children.size() > 0) {
if (traverseChildren) {
detailPrefix += "| ";
} else {
detailPrefix += " ";
}
// Print the current node
// The plan node header line will be prefixed by rootPrefix and the remaining details
// will be prefixed by detailPrefix.
expBuilder.append(rootPrefix + id.asInt() + ":" + planNodeName + "\n");
expBuilder.append(getNodeExplainString(detailPrefix, detailLevel));
if (limit != -1) {
expBuilder.append(detailPrefix + "limit: " + limit + "\n");
}
// Output Tuple Ids only when explain plan level is set to verbose
if (detailLevel.equals(TExplainLevel.VERBOSE)) {
expBuilder.append(detailPrefix + "tuple ids: ");
for (TupleId tupleId : tupleIds) {
String nullIndicator = nullableTupleIds.contains(tupleId) ? "N" : "";
expBuilder.append(tupleId.asInt() + nullIndicator + " ");
}
expBuilder.append("\n");
}
// Print the children
// if (children != null && children.size() > 0) {
if (traverseChildren) {
expBuilder.append(detailPrefix + "\n");
String childHeadlinePrefix = prefix + "|----";
String childDetailPrefix = prefix + "| ";
for (int i = 1; i < children.size(); ++i) {
expBuilder.append(
children.get(i).getExplainString(childHeadlinePrefix, childDetailPrefix,
detailLevel));
expBuilder.append(childDetailPrefix + "\n");
}
expBuilder.append(children.get(0).getExplainString(prefix, prefix, detailLevel));
}
return expBuilder.toString();
}
/**
* Return the node-specific details.
* Subclass should override this function.
* Each line should be prefix by detailPrefix.
*/
protected String getNodeExplainString(String prefix, TExplainLevel detailLevel) {
return "";
}
// Convert this plan node, including all children, to its Thrift representation.
public TPlan treeToThrift() {
TPlan result = new TPlan();
treeToThriftHelper(result);
return result;
}
// Append a flattened version of this plan node, including all children, to 'container'.
private void treeToThriftHelper(TPlan container) {
TPlanNode msg = new TPlanNode();
msg.node_id = id.asInt();
msg.num_children = children.size();
msg.limit = limit;
for (TupleId tid : tupleIds) {
msg.addToRow_tuples(tid.asInt());
msg.addToNullable_tuples(nullableTupleIds.contains(tid));
}
for (Expr e : conjuncts) {
msg.addToConjuncts(e.treeToThrift());
}
msg.compact_data = compactData;
toThrift(msg);
container.addToNodes(msg);
if (this instanceof ExchangeNode) {
msg.num_children = 0;
return;
} else {
msg.num_children = children.size();
for (PlanNode child: children) {
child.treeToThriftHelper(container);
}
}
}
/**
* Computes internal state, including planner-relevant statistics.
* Call this once on the root of the plan tree before calling toThrift().
* Subclasses need to override this.
*/
public void finalize(Analyzer analyzer) throws UserException {
for (PlanNode child : children) {
child.finalize(analyzer);
}
computeStats(analyzer);
}
/**
* Computes planner statistics: avgRowSize, numNodes, cardinality.
* Subclasses need to override this.
* Assumes that it has already been called on all children.
* This is broken out of finalize() so that it can be called separately
* from finalize() (to facilitate inserting additional nodes during plan
* partitioning w/o the need to call finalize() recursively on the whole tree again).
*/
protected void computeStats(Analyzer analyzer) {
avgRowSize = 0.0F;
for (TupleId tid : tupleIds) {
TupleDescriptor desc = analyzer.getTupleDesc(tid);
avgRowSize += desc.getAvgSerializedSize();
}
if (!children.isEmpty()) {
numNodes = getChild(0).numNodes;
}
}
/**
* Compute the product of the selectivies of all conjuncts.
*/
protected double computeSelectivity() {
double prod = 1.0;
for (Expr e : conjuncts) {
prod *= e.getSelectivity();
}
return prod;
}
protected ExprSubstitutionMap outputSmap;
protected ExprSubstitutionMap withoutTupleIsNullOutputSmap;
public ExprSubstitutionMap getOutputSmap() {
return outputSmap;
}
public void setOutputSmap(ExprSubstitutionMap smap) {
outputSmap = smap;
}
public void setWithoutTupleIsNullOutputSmap(ExprSubstitutionMap smap) {
withoutTupleIsNullOutputSmap = smap;
}
public ExprSubstitutionMap getWithoutTupleIsNullOutputSmap() {
return withoutTupleIsNullOutputSmap == null ? outputSmap : withoutTupleIsNullOutputSmap;
}
/**
* Marks all slots referenced in exprs as materialized.
*/
protected void markSlotsMaterialized(Analyzer analyzer, List<Expr> exprs) {
List<SlotId> refdIdList = Lists.newArrayList();
for (Expr expr: exprs) {
expr.getIds(null, refdIdList);
}
analyzer.materializeSlots(exprs);
}
public void init(Analyzer analyzer) throws UserException {
assignConjuncts(analyzer);
computeStats(analyzer);
createDefaultSmap(analyzer);
}
/**
* Assign remaining unassigned conjuncts.
*/
protected void assignConjuncts(Analyzer analyzer) {
List<Expr> unassigned = analyzer.getUnassignedConjuncts(this);
conjuncts.addAll(unassigned);
analyzer.markConjunctsAssigned(unassigned);
}
/**
* Returns an smap that combines the childrens' smaps.
*/
protected ExprSubstitutionMap getCombinedChildSmap() {
if (getChildren().size() == 0) {
return new ExprSubstitutionMap();
}
if (getChildren().size() == 1) {
return getChild(0).getOutputSmap();
}
ExprSubstitutionMap result = ExprSubstitutionMap.combine(
getChild(0).getOutputSmap(), getChild(1).getOutputSmap());
for (int i = 2; i < getChildren().size(); ++i) {
result = ExprSubstitutionMap.combine(result, getChild(i).getOutputSmap());
}
return result;
}
protected ExprSubstitutionMap getCombinedChildWithoutTupleIsNullSmap() {
if (getChildren().size() == 0) {
return new ExprSubstitutionMap();
}
if (getChildren().size() == 1) {
return getChild(0).getWithoutTupleIsNullOutputSmap();
}
ExprSubstitutionMap result = ExprSubstitutionMap.combine(
getChild(0).getWithoutTupleIsNullOutputSmap(),
getChild(1).getWithoutTupleIsNullOutputSmap());
for (int i = 2; i < getChildren().size(); ++i) {
result = ExprSubstitutionMap.combine(
result, getChild(i).getWithoutTupleIsNullOutputSmap());
}
return result;
}
/**
* Sets outputSmap_ to compose(existing smap, combined child smap). Also
* substitutes conjuncts_ using the combined child smap.
* @throws AnalysisException
*/
protected void createDefaultSmap(Analyzer analyzer) throws UserException {
ExprSubstitutionMap combinedChildSmap = getCombinedChildSmap();
outputSmap =
ExprSubstitutionMap.compose(outputSmap, combinedChildSmap, analyzer);
conjuncts = Expr.substituteList(conjuncts, outputSmap, analyzer, false);
}
/**
* Appends ids of slots that need to be materialized for this tree of nodes.
* By default, only slots referenced by conjuncts need to be materialized
* (the rationale being that only conjuncts need to be evaluated explicitly;
* exprs that are turned into scan predicates, etc., are evaluated implicitly).
*/
public void getMaterializedIds(Analyzer analyzer, List<SlotId> ids) {
for (PlanNode childNode : children) {
childNode.getMaterializedIds(analyzer, ids);
}
Expr.getIds(getConjuncts(), null, ids);
}
// Convert this plan node into msg (excluding children), which requires setting
// the node type and the node-specific field.
protected abstract void toThrift(TPlanNode msg);
protected String debugString() {
// not using Objects.toStrHelper because
StringBuilder output = new StringBuilder();
output.append("preds=" + Expr.debugString(conjuncts));
output.append(" limit=" + Long.toString(limit));
return output.toString();
}
protected String getExplainString(List<? extends Expr> exprs) {
if (exprs == null) {
return "";
}
StringBuilder output = new StringBuilder();
for (int i = 0; i < exprs.size(); ++i) {
if (i > 0) {
output.append(", ");
}
output.append(exprs.get(i).toSql());
}
return output.toString();
}
/**
* Returns true if stats-related variables are valid.
*/
protected boolean hasValidStats() {
return (numNodes == -1 || numNodes >= 0) && (cardinality == -1 || cardinality >= 0);
}
public int getNumInstances() {
return numInstances;
}
public void setNumInstances(int numInstances) {
this.numInstances = numInstances;
}
public void appendTrace(StringBuilder sb) {
sb.append(planNodeName);
if (!children.isEmpty()) {
sb.append("(");
int idx = 0;
for (PlanNode child : children) {
if (idx++ != 0) {
sb.append(",");
}
child.appendTrace(sb);
}
sb.append(")");
}
}
}