TiDB 逻辑优化
目录
logicalOptimize
func logicalOptimize(ctx context.Context, flag uint64, logic LogicalPlan) (LogicalPlan, error) {
var err error
for i, rule := range optRuleList {
// The order of flags is same as the order of optRule in the list.
// We use a bitmask to record which opt rules should be used. If the i-th bit is 1, it means we should
// apply i-th optimizing rule.
if flag&(1<<uint(i)) == 0 || isLogicalRuleDisabled(rule) {
continue
}
logic, err = rule.optimize(ctx, logic)
if err != nil {
return nil, err
}
}
return logic, err
}
var optRuleList = []logicalOptRule{
&gcSubstituter{},
&columnPruner{},
&resultReorder{},
&buildKeySolver{},
&decorrelateSolver{},
&aggregationEliminator{},
&projectionEliminator{},
&maxMinEliminator{},
&ppdSolver{},
&outerJoinEliminator{},
&partitionProcessor{},
&aggregationPushDownSolver{},
&pushDownTopNOptimizer{},
&joinReOrderSolver{},
&columnPruner{}, // column pruning again at last, note it will mess up the results of buildKeySolver
}
columnPruner
type columnPruner struct {
}
func (s *columnPruner) optimize(ctx context.Context, lp LogicalPlan) (LogicalPlan, error) {
err := lp.PruneColumns(lp.Schema().Columns)
return lp, err
}
LogicalPlan
需要实现 PruneColumns([]*expression.Column) error
接口
// LogicalPlan is a tree of logical operators.
// We can do a lot of logical optimizations to it, like predicate pushdown and column pruning.
type LogicalPlan interface {
...
// PruneColumns prunes the unused columns.
PruneColumns([]*expression.Column) error
...
}
例子
create table t(a int, b int, c int);
create table s(a int , b int, c int);
explain select t.a, s.* from t join (select a, count(*), sum(b) from s group by a) s on t.a = s.a where t.a < substring('123', 1, 1);
+------------------------------+----------+-----------+---------------+------------------------------------------------------------------------------------------------------------------------+
| id | estRows | task | access object | operator info |
+------------------------------+----------+-----------+---------------+------------------------------------------------------------------------------------------------------------------------+
| Projection_10 | 3323.33 | root | | myc_test.t.a, myc_test.s.a, Column#9, Column#10 |
| └─HashJoin_11 | 3323.33 | root | | inner join, equal:[eq(myc_test.t.a, myc_test.s.a)] |
| ├─HashAgg_18(Build) | 2658.67 | root | | group by:Column#16, funcs:count(1)->Column#9, funcs:sum(Column#14)->Column#10, funcs:firstrow(Column#15)->myc_test.s.a |
| │ └─Projection_26 | 3323.33 | root | | cast(myc_test.s.b, decimal(32,0) BINARY)->Column#14, myc_test.s.a, myc_test.s.a |
| │ └─TableReader_25 | 3323.33 | root | | data:Selection_24 |
| │ └─Selection_24 | 3323.33 | cop[tikv] | | lt(myc_test.s.a, 1), not(isnull(myc_test.s.a)) |
| │ └─TableFullScan_23 | 10000.00 | cop[tikv] | table:s | keep order:false, stats:pseudo |
| └─TableReader_15(Probe) | 3323.33 | root | | data:Selection_14 |
| └─Selection_14 | 3323.33 | cop[tikv] | | lt(myc_test.t.a, 1), not(isnull(myc_test.t.a)) |
| └─TableFullScan_13 | 10000.00 | cop[tikv] | table:t | keep order:false, stats:pseudo |
+------------------------------+----------+-----------+---------------+------------------------------------------------------------------------------------------------------------------------+
*LogicalProjection.PruneColumns
// PruneColumns implements LogicalPlan interface.
// If any expression has SetVar function or Sleep function, we do not prune it.
func (p *LogicalProjection) PruneColumns(parentUsedCols []*expression.Column) error {
child := p.children[0]
// 返回一个 make([]bool, p.schema.Len()),
// 对于 p.schema 中的每一个 col,如果出现在 parentUsedCols,那么标记为 true
// 标记为 true 的个数,应该等于 len(parentUsedCols)
used := expression.GetUsedList(parentUsedCols, p.schema)
for i := len(used) - 1; i >= 0; i-- {
if !used[i] && !exprHasSetVarOrSleep(p.Exprs[i]) {
// 更新 p.schema.Columns(LogicalProjection 的输出列)为在 parentUsedCols 中出现的列
// or has SetVar function or Sleep function
// 每个 p.schema.Column 对应一个 p.Expr 所以同时要更新
p.schema.Columns = append(p.schema.Columns[:i], p.schema.Columns[i+1:]...)
p.Exprs = append(p.Exprs[:i], p.Exprs[i+1:]...)
}
}
// 从 p.Exprs 中提取出 p 自己所需要的 coloum
// 如果 p.Expr 是 expression.*Column,那么就返回这个
// 如果 p.Expr 是 expression.*ScalarFunction,那么需要从 args 里面提取 coloumn
selfUsedCols := make([]*expression.Column, 0, len(p.Exprs))
selfUsedCols = expression.ExtractColumnsFromExpressions(selfUsedCols, p.Exprs, nil)
return child.PruneColumns(selfUsedCols)
}
*LogicalSelection.PruneColumns
// PruneColumns implements LogicalPlan interface.
func (p *LogicalSelection) PruneColumns(parentUsedCols []*expression.Column) error {
child := p.children[0]
// parentUsedCols + p.Conditions 中所需要的 cols 就是 p 自己所需要的 cols
parentUsedCols = expression.ExtractColumnsFromExpressions(parentUsedCols, p.Conditions, nil)
return child.PruneColumns(parentUsedCols)
}
可以看到,LogicalSelection
的 Schema()
就是他孩子的 Schema()
,LogicalSelection
就只是一个过滤的作用
// LogicalSelection represents a where or having predicate.
type LogicalSelection struct {
baseLogicalPlan
// Originally the WHERE or ON condition is parsed into a single expression,
// but after we converted to CNF(Conjunctive normal form), it can be
// split into a list of AND conditions.
Conditions []expression.Expression
// having selection can't be pushed down, because it must above the aggregation.
buildByHaving bool
}
// Schema implements Plan Schema interface.
func (p *baseLogicalPlan) Schema() *expression.Schema {
return p.children[0].Schema()
}
*LogicalJoin.PruneColumns
// PruneColumns implements LogicalPlan interface.
func (p *LogicalJoin) PruneColumns(parentUsedCols []*expression.Column) error {
// parentUsedCols + p.EqualConditions + p.LeftConditions + p.RightConditions + p.OtherConditions
// 中的 cols 中属于 lChild.Schema() 的 col 是 lChild 所需要产出的 col 即 leftCols
// rightCols 同理
leftCols, rightCols := p.extractUsedCols(parentUsedCols)
err := p.children[0].PruneColumns(leftCols)
if err != nil {
return err
}
addConstOneForEmptyProjection(p.children[0])
err = p.children[1].PruneColumns(rightCols)
if err != nil {
return err
}
addConstOneForEmptyProjection(p.children[1])
p.mergeSchema()
// ???
if p.JoinType == LeftOuterSemiJoin || p.JoinType == AntiLeftOuterSemiJoin {
joinCol := p.schema.Columns[len(p.schema.Columns)-1]
parentUsedCols = append(parentUsedCols, joinCol)
}
p.inlineProjection(parentUsedCols)
return nil
}
// By add const one, we can avoid empty Projection is eliminated.
// Because in some cases, Projectoin cannot be eliminated even its output is empty.
func addConstOneForEmptyProjection(p LogicalPlan) {
proj, ok := p.(*LogicalProjection)
if !ok {
return
}
if proj.Schema().Len() != 0 {
return
}
// 这里可以看出 LogicalProjection 的真实作用,proj.Exprs 中的每个 expr 的结果映射为 proj.schema 中的一个 col
constOne := expression.NewOne()
proj.schema.Append(&expression.Column{
UniqueID: proj.ctx.GetSessionVars().AllocPlanColumnID(),
RetType: constOne.GetType(),
})
proj.Exprs = append(proj.Exprs, &expression.Constant{
Value: constOne.Value,
RetType: constOne.GetType(),
})
}
func (p *LogicalJoin) mergeSchema() {
p.schema = buildLogicalJoinSchema(p.JoinType, p)
}
// ???
func buildLogicalJoinSchema(joinType JoinType, join LogicalPlan) *expression.Schema {
leftSchema := join.Children()[0].Schema()
switch joinType {
case SemiJoin, AntiSemiJoin:
return leftSchema.Clone()
case LeftOuterSemiJoin, AntiLeftOuterSemiJoin:
newSchema := leftSchema.Clone()
newSchema.Append(join.Schema().Columns[join.Schema().Len()-1])
return newSchema
}
newSchema := expression.MergeSchema(leftSchema, join.Children()[1].Schema())
if joinType == LeftOuterJoin {
resetNotNullFlag(newSchema, leftSchema.Len(), newSchema.Len())
} else if joinType == RightOuterJoin {
resetNotNullFlag(newSchema, 0, leftSchema.Len())
}
return newSchema
}
// inlineProjection prunes unneeded columns inline a executor.
func (s *logicalSchemaProducer) inlineProjection(parentUsedCols []*expression.Column) {
// 只保留属于 parentUsedCols 的 col
used := expression.GetUsedList(parentUsedCols, s.Schema())
for i := len(used) - 1; i >= 0; i-- {
if !used[i] {
s.schema.Columns = append(s.Schema().Columns[:i], s.Schema().Columns[i+1:]...)
}
}
}
*DataSource.PruneColumns
// PruneColumns implements LogicalPlan interface.
func (ds *DataSource) PruneColumns(parentUsedCols []*expression.Column) error {
// ds.schema 在没有 PruneColumns 之前会是这个表的所有列
used := expression.GetUsedList(parentUsedCols, ds.schema)
// ds.allConds 是推下来的谓词
exprCols := expression.ExtractColumnsFromExpressions(nil, ds.allConds, nil)
exprUsed := expression.GetUsedList(exprCols, ds.schema)
originSchemaColumns := ds.schema.Columns
originColumns := ds.Columns
for i := len(used) - 1; i >= 0; i-- {
// 更新 ds.schema.Columns(DataSource 的输出列)为在 parentUsedCols 中出现的列
// or 在 ds.allConds 中使用到的列
// 每个 ds.schema.Column 对应一个 ds.Column 所以同时要更新
if !used[i] && !exprUsed[i] {
ds.schema.Columns = append(ds.schema.Columns[:i], ds.schema.Columns[i+1:]...)
ds.Columns = append(ds.Columns[:i], ds.Columns[i+1:]...)
}
}
// For SQL like `select 1 from t`, tikv's response will be empty if no column is in schema.
// So we'll force to push one if schema doesn't have any column.
/// ???
if ds.schema.Len() == 0 {
...
}
// ds.handleCols 是表的主键列(主键可能是一列,也可能是多列组成,如果表没有主键,会送你一个)
// 见 func (b *PlanBuilder) buildDataSource
if ds.handleCols != nil && ds.handleCols.IsInt() && ds.schema.ColumnIndex(ds.handleCols.GetCol(0)) == -1 {
ds.handleCols = nil
}
return nil
}
*LogicalAggregation.PruneColumns
func (la *LogicalAggregation) PruneColumns(parentUsedCols []*expression.Column) error {
child := la.children[0]
used := expression.GetUsedList(parentUsedCols, la.Schema())
// la.Schema() 中的每一个 col 和 la.AggFuncs 中的 AggFun 一一对应
// 这说明在未列剪枝前 la.GroupByItems 也在 la.Schema() 中
// 且 对应的 AggFunc.Name = ast.AggFuncFirstRow
allFirstRow := true
allRemainFirstRow := true
for i := len(used) - 1; i >= 0; i-- {
if la.AggFuncs[i].Name != ast.AggFuncFirstRow {
allFirstRow = false
}
// 去除 la.schema.Columns 中不在 parentUsedCols 中的 col
// 并去除对应的 AggFunc
// exprHasSetVarOrSleep checks if the expression has SetVar function or Sleep function.
if !used[i] && !ExprsHasSideEffects(la.AggFuncs[i].Args) {
la.schema.Columns = append(la.schema.Columns[:i], la.schema.Columns[i+1:]...)
la.AggFuncs = append(la.AggFuncs[:i], la.AggFuncs[i+1:]...)
} else if la.AggFuncs[i].Name != ast.AggFuncFirstRow {
allRemainFirstRow = false
}
}
var selfUsedCols []*expression.Column
// la.AggFuncs 中使用到的 col 是 selfUsedCols
for _, aggrFunc := range la.AggFuncs {
selfUsedCols = expression.ExtractColumnsFromExpressions(selfUsedCols, aggrFunc.Args, nil)
var cols []*expression.Column
aggrFunc.OrderByItems, cols = pruneByItems(aggrFunc.OrderByItems)
selfUsedCols = append(selfUsedCols, cols...)
}
// ???
if len(la.AggFuncs) == 0 || (!allFirstRow && allRemainFirstRow) {
// If all the aggregate functions are pruned, we should add an aggregate function to maintain the info of row numbers.
// For all the aggregate functions except `first_row`, if we have an empty table defined as t(a,b),
// `select agg(a) from t` would always return one row, while `select agg(a) from t group by b` would return empty.
// For `first_row` which is only used internally by tidb, `first_row(a)` would always return empty for empty input now.
var err error
var newAgg *aggregation.AggFuncDesc
if allFirstRow {
newAgg, err = aggregation.NewAggFuncDesc(la.ctx, ast.AggFuncFirstRow, []expression.Expression{expression.NewOne()}, false)
} else {
newAgg, err = aggregation.NewAggFuncDesc(la.ctx, ast.AggFuncCount, []expression.Expression{expression.NewOne()}, false)
}
if err != nil {
return err
}
la.AggFuncs = append(la.AggFuncs, newAgg)
col := &expression.Column{
UniqueID: la.ctx.GetSessionVars().AllocPlanColumnID(),
RetType: newAgg.RetTp,
}
la.schema.Columns = append(la.schema.Columns, col)
}
// la.GroupByItems 中使用到的 col 也属于 selfUsedCols
if len(la.GroupByItems) > 0 {
for i := len(la.GroupByItems) - 1; i >= 0; i-- {
cols := expression.ExtractColumns(la.GroupByItems[i])
if len(cols) == 0 && !exprHasSetVarOrSleep(la.GroupByItems[i]) {
la.GroupByItems = append(la.GroupByItems[:i], la.GroupByItems[i+1:]...)
} else {
selfUsedCols = append(selfUsedCols, cols...)
}
}
// If all the group by items are pruned, we should add a constant 1 to keep the correctness.
// Because `select count(*) from t` is different from `select count(*) from t group by 1`.
// ???
if len(la.GroupByItems) == 0 {
la.GroupByItems = []expression.Expression{expression.NewOne()}
}
}
return child.PruneColumns(selfUsedCols)
}
ppdSolver
type ppdSolver struct{}
func (s *ppdSolver) optimize(ctx context.Context, lp LogicalPlan) (LogicalPlan, error) {
_, p := lp.PredicatePushDown(nil)
return p, nil
}
LogicalPlan
需要实现 PredicatePushDown([]expression.Expression) ([]expression.Expression, LogicalPlan)
接口
// LogicalPlan is a tree of logical operators.
// We can do a lot of logical optimizations to it, like predicate pushdown and column pruning.
type LogicalPlan interface {
...
// PredicatePushDown pushes down the predicates in the where/on/having clauses as deeply as possible.
// It will accept a predicate that is an expression slice, and return the expressions that can't be pushed.
// Because it might change the root if the having clause exists, we need to return a plan that represents a new root.
PredicatePushDown([]expression.Expression) ([]expression.Expression, LogicalPlan)
...
}
例子
create table t(a int, b int, c int);
create table s(a int , b int, c int);
explain select t.a, s.* from t join (select a, count(*), sum(b) from s group by a) s on t.a = s.a where t.a < substring('123', 1, 1);
+------------------------------+----------+-----------+---------------+------------------------------------------------------------------------------------------------------------------------+
| id | estRows | task | access object | operator info |
+------------------------------+----------+-----------+---------------+------------------------------------------------------------------------------------------------------------------------+
| Projection_10 | 3323.33 | root | | myc_test.t.a, myc_test.s.a, Column#9, Column#10 |
| └─HashJoin_11 | 3323.33 | root | | inner join, equal:[eq(myc_test.t.a, myc_test.s.a)] |
| ├─HashAgg_18(Build) | 2658.67 | root | | group by:Column#16, funcs:count(1)->Column#9, funcs:sum(Column#14)->Column#10, funcs:firstrow(Column#15)->myc_test.s.a |
| │ └─Projection_26 | 3323.33 | root | | cast(myc_test.s.b, decimal(32,0) BINARY)->Column#14, myc_test.s.a, myc_test.s.a |
| │ └─TableReader_25 | 3323.33 | root | | data:Selection_24 |
| │ └─Selection_24 | 3323.33 | cop[tikv] | | lt(myc_test.s.a, 1), not(isnull(myc_test.s.a)) |
| │ └─TableFullScan_23 | 10000.00 | cop[tikv] | table:s | keep order:false, stats:pseudo |
| └─TableReader_15(Probe) | 3323.33 | root | | data:Selection_14 |
| └─Selection_14 | 3323.33 | cop[tikv] | | lt(myc_test.t.a, 1), not(isnull(myc_test.t.a)) |
| └─TableFullScan_13 | 10000.00 | cop[tikv] | table:t | keep order:false, stats:pseudo |
+------------------------------+----------+-----------+---------------+------------------------------------------------------------------------------------------------------------------------+
*LogicalProjection.PredicatePushDown
// PredicatePushDown implements LogicalPlan PredicatePushDown interface.
func (p *LogicalProjection) PredicatePushDown(predicates []expression.Expression) (ret []expression.Expression, retPlan LogicalPlan) {
canBePushed := make([]expression.Expression, 0, len(predicates))
canNotBePushed := make([]expression.Expression, 0, len(predicates))
for _, expr := range p.Exprs {
// 如果 an expression contains SetVar function and assign a value
// 直接不推 predicates 了
if expression.HasAssignSetVarFunc(expr) {
_, child := p.baseLogicalPlan.PredicatePushDown(nil)
return predicates, child
}
}
// ??? 感觉像是把映射后的 col 转换成 映射前的 col
for _, cond := range predicates {
newFilter := expression.ColumnSubstitute(cond, p.Schema(), p.Exprs)
if !expression.HasGetSetVarFunc(newFilter) {
canBePushed = append(canBePushed, newFilter)
} else {
canNotBePushed = append(canNotBePushed, cond)
}
}
remained, child := p.baseLogicalPlan.PredicatePushDown(canBePushed)
return append(remained, canNotBePushed...), child
}
*baseLogicalPlan.PredicatePushDown
// PredicatePushDown implements LogicalPlan interface.
// 只要有孩子那就全力往下推,推不下去就在孩子头上加 LogicalSelection
// 没孩子那就没办法了,送回上层
func (p *baseLogicalPlan) PredicatePushDown(predicates []expression.Expression) ([]expression.Expression, LogicalPlan) {
if len(p.children) == 0 {
// p 没有孩子了,不推 predicates 了,返回 p.self
return predicates, p.self
}
child := p.children[0]
// 将 predicates 推到孩子上
// rest 推不下去的生成一个 LogicalSelection 接到孩子的头上,作为 p 的新孩子
rest, newChild := child.PredicatePushDown(predicates)
addSelection(p.self, newChild, rest, 0)
return nil, p.self
}
func addSelection(p LogicalPlan, child LogicalPlan, conditions []expression.Expression, chIdx int) {
// 没有 conditions 直接啥都不用干
if len(conditions) == 0 {
p.Children()[chIdx] = child
return
}
// 将 conditions 进行一些改写,比如:
// a = d & b * 2 = c & b = 1 & a = 4 => d = 4 & 2 = c & b = 1 & a = 4
// a = b and b = c and c = d and c < 1 => a = b and b = c and c = d and c < 1 and a < 1 and b < 1 and d < 1
conditions = expression.PropagateConstant(p.SCtx(), conditions)
// 如果 conditions 中有常量 false ,那么用 TableDual 作为 p 的孩子
// Return table dual when filter is constant false or null.
dual := Conds2TableDual(child, conditions)
if dual != nil {
p.Children()[chIdx] = dual
return
}
// 删除掉常量为 true 的 cond
conditions = DeleteTrueExprs(p, conditions)
if len(conditions) == 0 {
p.Children()[chIdx] = child
return
}
// 新建 LogicalSelection 连接在 child 上面,把 LogicalSelection 作为 p 的 child
selection := LogicalSelection{Conditions: conditions}.Init(p.SCtx(), p.SelectBlockOffset())
selection.SetChildren(child)
p.Children()[chIdx] = selection
}
*LogicalSelection.PredicatePushDown
// PredicatePushDown implements LogicalPlan PredicatePushDown interface.
func (p *LogicalSelection) PredicatePushDown(predicates []expression.Expression) ([]expression.Expression, LogicalPlan) {
// 只保留 *expression.Constant,且为 true 的 predicate
// 要去除 ContainMutableConst,(prepare plan cache 那一套的)
predicates = DeleteTrueExprs(p, predicates)
p.Conditions = DeleteTrueExprs(p, p.Conditions)
var child LogicalPlan
var retConditions []expression.Expression
// having selection can't be pushed down, because it must above the aggregation.
if p.buildByHaving {
retConditions, child = p.children[0].PredicatePushDown(predicates)
retConditions = append(retConditions, p.Conditions...)
} else {
canBePushDown, canNotBePushDown := splitSetGetVarFunc(p.Conditions)
// 把 p.Conditions 和上面传下来的 predicates 中能推的都推到孩子上
retConditions, child = p.children[0].PredicatePushDown(append(canBePushDown, predicates...))
retConditions = append(retConditions, canNotBePushDown...)
}
if len(retConditions) > 0 {
// 更新 p.Conditions 为 PropagateConstant 后的 retConditions
p.Conditions = expression.PropagateConstant(p.ctx, retConditions)
// Return table dual when filter is constant false or null.
dual := Conds2TableDual(p, p.Conditions)
if dual != nil {
return nil, dual
}
// 推不下去的都留在 p 了
return nil, p
}
// 全都推下去了,p 没有用了,只需要 p.child
return nil, child
}
*LogicalJoin.PredicatePushDown
LogicalProjection -> LogicalSelection (where) -> LogicalSelection (on) -> LogicalJoin
// PredicatePushDown implements LogicalPlan PredicatePushDown interface.
func (p *LogicalJoin) PredicatePushDown(predicates []expression.Expression) (ret []expression.Expression, retPlan LogicalPlan) {
// 首先会做一个简化,将左外连接和右外连接转化为内连接。
// 什么情况下外连接可以转内连接?左向外连接的结果集包括左表的所有行,而不仅仅是连接列所匹配的行。
// 如果左表的某行在右表中没有匹配的行,则在结果集右边补 NULL。做谓词下推时,如果我们知道接下来的
// 的谓词条件一定会把包含 NULL 的行全部都过滤掉,那么做外连接就没意义了,可以直接改写成内连接。
simplifyOuterJoin(p, predicates)
var equalCond []*expression.ScalarFunction
var leftPushCond, rightPushCond, otherCond, leftCond, rightCond []expression.Expression
switch p.JoinType {
case LeftOuterJoin, LeftOuterSemiJoin, AntiLeftOuterSemiJoin:
...
case RightOuterJoin:
...
case SemiJoin, InnerJoin:
tempCond := make([]expression.Expression, 0, len(p.LeftConditions)+len(p.RightConditions)+len(p.EqualConditions)+len(p.OtherConditions)+len(predicates))
tempCond = append(tempCond, p.LeftConditions...)
tempCond = append(tempCond, p.RightConditions...)
tempCond = append(tempCond, expression.ScalarFuncs2Exprs(p.EqualConditions)...)
tempCond = append(tempCond, p.OtherConditions...)
tempCond = append(tempCond, predicates...)
// ???
tempCond = expression.ExtractFiltersFromDNFs(p.ctx, tempCond)
tempCond = expression.PropagateConstant(p.ctx, tempCond)
// Return table dual when filter is constant false or null.
dual := Conds2TableDual(p, tempCond)
if dual != nil {
return ret, dual
}
// ExtractOnCondition divide conditions in CNF of join node into 4 groups.
// These conditions can be where conditions, join conditions, or collection of both.
// If deriveLeft/deriveRight is set, we would try to derive more conditions for left/right plan.
// 例如本例,有一个 t.a = s.a equalCond,
// 那么可以为 leftPushCond, rightPushCond 分别额外增加一个 not(isnull(myc_test.s.a)) 和 not(isnull(myc_test.t.a))
equalCond, leftPushCond, rightPushCond, otherCond = p.extractOnCondition(tempCond, true, true)
// SemiJoin InnerJoin 不留 p.LeftConditions 和 p.RightConditions,全都推下去
p.LeftConditions = nil
p.RightConditions = nil
// equalCond 和 otherCond 留在 p
p.EqualConditions = equalCond
p.OtherConditions = otherCond
leftCond = leftPushCond
rightCond = rightPushCond
case AntiSemiJoin:
...
}
leftCond = expression.RemoveDupExprs(p.ctx, leftCond)
rightCond = expression.RemoveDupExprs(p.ctx, rightCond)
leftRet, lCh := p.children[0].PredicatePushDown(leftCond)
rightRet, rCh := p.children[1].PredicatePushDown(rightCond)
// push 不到孩子上的,生成一个 LogicalSelection 接在孩子头上
addSelection(p, lCh, leftRet, 0)
addSelection(p, rCh, rightRet, 1)
// ???
p.updateEQCond()
// ???
buildKeyInfo(p)
return ret, p.self
}
*DataSource.PredicatePushDown
// PredicatePushDown implements LogicalPlan PredicatePushDown interface.
func (ds *DataSource) PredicatePushDown(predicates []expression.Expression) ([]expression.Expression, LogicalPlan) {
predicates = expression.PropagateConstant(ds.ctx, predicates)
predicates = DeleteTrueExprs(ds, predicates)
ds.allConds = predicates
// pushedDownConds are the conditions that will be pushed down to coprocessor.
ds.pushedDownConds, predicates = expression.PushDownExprs(ds.ctx.GetSessionVars().StmtCtx, predicates, ds.ctx.GetClient(), kv.UnSpecified)
// push 不到 coprocessor 上的,返回给上层
return predicates, ds
}
*LogicalAggregation.PredicatePushDown
func (la *LogicalAggregation) PredicatePushDown(predicates []expression.Expression) (ret []expression.Expression, retPlan LogicalPlan) {
var condsToPush []expression.Expression
// 在输入到 AggFuncs 前的 exprs,AggFuncs 的输出就是 la.Schema()
exprsOriginal := make([]expression.Expression, 0, len(la.AggFuncs))
for _, fun := range la.AggFuncs {
exprsOriginal = append(exprsOriginal, fun.Args[0])
}
groupByColumns := expression.NewSchema(la.GetGroupByCols()...)
for _, cond := range predicates {
switch cond.(type) {
case *expression.Constant:
condsToPush = append(condsToPush, cond)
ret = append(ret, cond)
case *expression.ScalarFunction:
extractedCols := expression.ExtractColumns(cond)
ok := true
for _, col := range extractedCols {
// 只能是 push 在 groupByColumns 中的 col
if !groupByColumns.Contains(col) {
ok = false
break
}
}
if ok {
// ??? 感觉像是把映射后的 col 转换成 映射前的 col
newFunc := expression.ColumnSubstitute(cond, la.Schema(), exprsOriginal)
condsToPush = append(condsToPush, newFunc)
} else {
ret = append(ret, cond)
}
default:
ret = append(ret, cond)
}
}
la.baseLogicalPlan.PredicatePushDown(condsToPush)
return ret, la
}
joinReOrderSolver
type joinReOrderSolver struct {
}
func (s *joinReOrderSolver) optimize(ctx context.Context, p LogicalPlan) (LogicalPlan, error) {
return s.optimizeRecursive(p.SCtx(), p)
}
*joinReOrderSolver.optimizeRecursive
// optimizeRecursive recursively collects join groups and applies join reorder algorithm for each group.
func (s *joinReOrderSolver) optimizeRecursive(ctx sessionctx.Context, p LogicalPlan) (LogicalPlan, error) {
var err error
// 递归的 extracts all the join nodes connected with
// 连续的 InnerJoins to construct a join group.
curJoinGroup, eqEdges, otherConds := extractJoinGroup(p)
// 至少 p 是一个 InnerJoin 的 LogicalJoin
if len(curJoinGroup) > 1 {
// curJoinGroup 中的所有成员是这一次 optimizeRecursive 调用中 p 需要 join reorder 的 joinNode
for i := range curJoinGroup {
// curJoinGroup 中的每个成员都可以调用 optimizeRecursive 来进行各自的 join reorder 优化的
curJoinGroup[i], err = s.optimizeRecursive(ctx, curJoinGroup[i])
if err != nil {
return nil, err
}
}
// 对 curJoinGroup 中的每个成员都调用 optimizeRecursive 进行各自的 join reorder 优化后
// 再对 curJoinGroup 中的所有成员进行作为 p 的 joinNode 的 join reorder 优化
baseGroupSolver := &baseSingleGroupJoinOrderSolver{
ctx: ctx,
otherConds: otherConds,
}
originalSchema := p.Schema()
// 默认是 0
if len(curJoinGroup) > ctx.GetSessionVars().TiDBOptJoinReorderThreshold {
groupSolver := &joinReorderGreedySolver{
baseSingleGroupJoinOrderSolver: baseGroupSolver,
eqEdges: eqEdges,
}
p, err = groupSolver.solve(curJoinGroup)
} else {
dpSolver := &joinReorderDPSolver{
baseSingleGroupJoinOrderSolver: baseGroupSolver,
}
dpSolver.newJoin = dpSolver.newJoinWithEdges
p, err = dpSolver.solve(curJoinGroup, expression.ScalarFuncs2Exprs(eqEdges))
}
if err != nil {
return nil, err
}
// ??? join reorder 为啥还能把 p 的 Schema 改了
schemaChanged := false
if len(p.Schema().Columns) != len(originalSchema.Columns) {
schemaChanged = true
} else {
for i, col := range p.Schema().Columns {
if !col.Equal(nil, originalSchema.Columns[i]) {
schemaChanged = true
break
}
}
}
// 如果 schema 被改了,那么就在上面接一个 LogicalProjection
if schemaChanged {
proj := LogicalProjection{
Exprs: expression.Column2Exprs(originalSchema.Columns),
}.Init(p.SCtx(), p.SelectBlockOffset())
proj.SetSchema(originalSchema)
proj.SetChildren(p)
p = proj
}
return p, nil
}
// p 不是一个 InnerJoin 的 LogicalJoin
// 对 p 的所有孩子均递归调用 s.optimizeRecursive 生成新孩子作为 p 的孩子
newChildren := make([]LogicalPlan, 0, len(p.Children()))
for _, child := range p.Children() {
newChild, err := s.optimizeRecursive(ctx, child)
if err != nil {
return nil, err
}
newChildren = append(newChildren, newChild)
}
p.SetChildren(newChildren...)
return p, nil
}
// extractJoinGroup extracts all the join nodes connected with 连续的
// InnerJoins to construct a join group. This join group is further used to
// construct a new join order based on a reorder algorithm.
//
// For example: "InnerJoin(InnerJoin(a, b), LeftJoin(c, d))"
// results in a join group {a, b, LeftJoin(c, d)}.
func extractJoinGroup(p LogicalPlan) (group []LogicalPlan, eqEdges []*expression.ScalarFunction, otherConds []expression.Expression) {
join, isJoin := p.(*LogicalJoin)
// p 如果不是 LogicalJoin
// 或 不是 InnerJoin
// 或 用户使用 STRAIGHT_JOIN 语法来强制指定一种 Join 顺序
if !isJoin || join.preferJoinType > uint(0) || join.JoinType != InnerJoin || join.StraightJoin {
return []LogicalPlan{p}, nil, nil
}
// 递归的 extracts all the join nodes connected with 连续的
// InnerJoins to construct a join group.
lhsGroup, lhsEqualConds, lhsOtherConds := extractJoinGroup(join.children[0])
rhsGroup, rhsEqualConds, rhsOtherConds := extractJoinGroup(join.children[1])
// InnerJoin 的 join.LeftConditions 和 join.RightConditions 一定为 nil
// 见上面的 PredicatePushDown
group = append(group, lhsGroup...)
group = append(group, rhsGroup...)
eqEdges = append(eqEdges, join.EqualConditions...)
eqEdges = append(eqEdges, lhsEqualConds...)
eqEdges = append(eqEdges, rhsEqualConds...)
otherConds = append(otherConds, join.OtherConditions...)
otherConds = append(otherConds, lhsOtherConds...)
otherConds = append(otherConds, rhsOtherConds...)
return group, eqEdges, otherConds
}
joinReorderGreedySolver
type joinReorderGreedySolver struct {
*baseSingleGroupJoinOrderSolver
eqEdges []*expression.ScalarFunction
}
func (s *joinReorderGreedySolver) solve(joinNodePlans []LogicalPlan) (LogicalPlan, error) {
for _, node := range joinNodePlans {
_, err := node.recursiveDeriveStats(nil)
if err != nil {
return nil, err
}
// 每一个 LogicalPlan 作为一个 jrNode 存在 s.curJoinGroup 中
s.curJoinGroup = append(s.curJoinGroup, &jrNode{
p: node,
cumCost: s.baseNodeCumCost(node),
})
}
// 按照 jrNode.cumCost 从小到大对 s.curJoinGroup 排序
sort.SliceStable(s.curJoinGroup, func(i, j int) bool {
return s.curJoinGroup[i].cumCost < s.curJoinGroup[j].cumCost
})
// cartesianGroup 中的每一个 LogicalPlan 和其他 LogicalPlan 无关联,
// 即它们之间,不存在 joinReorderGreedySolver.eqEdges
var cartesianGroup []LogicalPlan
for len(s.curJoinGroup) > 0 {
newNode, err := s.constructConnectedJoinTree()
if err != nil {
return nil, err
}
cartesianGroup = append(cartesianGroup, newNode.p)
}
return s.makeBushyJoin(cartesianGroup), nil
}
// baseNodeCumCost calculate the cumulative cost of the node in the join group.
func (s *baseSingleGroupJoinOrderSolver) baseNodeCumCost(groupNode LogicalPlan) float64 {
cost := groupNode.statsInfo().RowCount
for _, child := range groupNode.Children() {
cost += s.baseNodeCumCost(child)
}
return cost
}
https://docs.pingcap.com/zh/tidb/dev/join-reorder
func (s *joinReorderGreedySolver) constructConnectedJoinTree() (*jrNode, error) {
// 第一个 jrNode.cumCost 是最小的,因为前面排过序了
// 把它作为 curJoinTree
curJoinTree := s.curJoinGroup[0]
s.curJoinGroup = s.curJoinGroup[1:]
for {
bestCost := math.MaxFloat64
bestIdx := -1
var finalRemainOthers []expression.Expression
var bestJoin LogicalPlan
for i, node := range s.curJoinGroup {
// 如果 node, curJoinTree 之间存在至少一个 s.eqEdges
// 那说明这俩可以组成一个新的 LogicalJoin 为 newJoin
newJoin, remainOthers := s.checkConnectionAndMakeJoin(curJoinTree.p, node.p)
if newJoin == nil {
continue
}
_, err := newJoin.recursiveDeriveStats(nil)
if err != nil {
return nil, err
}
curCost := s.calcJoinCumCost(newJoin, curJoinTree, node)
// 如果 newJoin 是迄今为止最优,那么 bestJoin = newJoin
if bestCost > curCost {
bestCost = curCost
bestJoin = newJoin
bestIdx = i
finalRemainOthers = remainOthers
}
}
// If we could find more join node, meaning that the sub connected graph have been totally explored.
if bestJoin == nil {
break
}
// curJoinTree 更新为,新产生的,合并过后的 bestJoin
curJoinTree = &jrNode{
p: bestJoin,
cumCost: bestCost,
}
// 从 s.curJoinGroup 中删除掉被合并的 node
s.curJoinGroup = append(s.curJoinGroup[:bestIdx], s.curJoinGroup[bestIdx+1:]...)
s.otherConds = finalRemainOthers
}
return curJoinTree, nil
}
func (s *joinReorderGreedySolver) checkConnectionAndMakeJoin(leftNode, rightNode LogicalPlan) (LogicalPlan, []expression.Expression) {
var usedEdges []*expression.ScalarFunction
remainOtherConds := make([]expression.Expression, len(s.otherConds))
copy(remainOtherConds, s.otherConds)
// 如果 leftNode, rightNode 之间存在至少一个 s.eqEdges
// 那说明这俩可以组成一个新的 LogicalJoin
for _, edge := range s.eqEdges {
lCol := edge.GetArgs()[0].(*expression.Column)
rCol := edge.GetArgs()[1].(*expression.Column)
if leftNode.Schema().Contains(lCol) && rightNode.Schema().Contains(rCol) {
usedEdges = append(usedEdges, edge)
} else if rightNode.Schema().Contains(lCol) && leftNode.Schema().Contains(rCol) {
newSf := expression.NewFunctionInternal(s.ctx, ast.EQ, edge.GetType(), rCol, lCol).(*expression.ScalarFunction)
usedEdges = append(usedEdges, newSf)
}
}
if len(usedEdges) == 0 {
return nil, nil
}
var otherConds []expression.Expression
mergedSchema := expression.MergeSchema(leftNode.Schema(), rightNode.Schema())
// 从 remainOtherConds 中筛选出属于 leftNode, rightNode 的 otherConds
remainOtherConds, otherConds = expression.FilterOutInPlace(remainOtherConds, func(expr expression.Expression) bool {
return expression.ExprFromSchema(expr, mergedSchema)
})
return s.newJoinWithEdges(leftNode, rightNode, usedEdges, otherConds), remainOtherConds
}
// 二和一,并设置 Logical.EqualConditions 和 Logical.OtherConditions
func (s *baseSingleGroupJoinOrderSolver) newJoinWithEdges(lChild, rChild LogicalPlan, eqEdges []*expression.ScalarFunction, otherConds []expression.Expression) LogicalPlan {
newJoin := s.newCartesianJoin(lChild, rChild)
newJoin.EqualConditions = eqEdges
newJoin.OtherConditions = otherConds
return newJoin
}
// makeBushyJoin build bushy tree for the nodes which have no equal condition to connect them.
// 不断将相互之间不存在 eqConditions 的两个 LogicalPlan 二合一成为一个 LogicalJoin
func (s *baseSingleGroupJoinOrderSolver) makeBushyJoin(cartesianJoinGroup []LogicalPlan) LogicalPlan {
resultJoinGroup := make([]LogicalPlan, 0, (len(cartesianJoinGroup)+1)/2)
for len(cartesianJoinGroup) > 1 {
resultJoinGroup = resultJoinGroup[:0]
for i := 0; i < len(cartesianJoinGroup); i += 2 {
if i+1 == len(cartesianJoinGroup) {
resultJoinGroup = append(resultJoinGroup, cartesianJoinGroup[i])
break
}
// 二和一成为 LogicalJoin
newJoin := s.newCartesianJoin(cartesianJoinGroup[i], cartesianJoinGroup[i+1])
// 设置 newJoin.OtherConditions,
// 无需设置 newJoin.EqualConditions,
// 因为 cartesianJoinGroup[i], cartesianJoinGroup[i+1] 之间不存在 eqConditions
for i := len(s.otherConds) - 1; i >= 0; i-- {
cols := expression.ExtractColumns(s.otherConds[i])
if newJoin.schema.ColumnsIndices(cols) != nil {
newJoin.OtherConditions = append(newJoin.OtherConditions, s.otherConds[i])
s.otherConds = append(s.otherConds[:i], s.otherConds[i+1:]...)
}
}
resultJoinGroup = append(resultJoinGroup, newJoin)
}
cartesianJoinGroup, resultJoinGroup = resultJoinGroup, cartesianJoinGroup
}
return cartesianJoinGroup[0]
}
func (s *baseSingleGroupJoinOrderSolver) newCartesianJoin(lChild, rChild LogicalPlan) *LogicalJoin {
// ??? BlockOffset 到底是干啥的
// 当查询语句是包含多层嵌套子查询的复杂语句时,
// SelectBlockOffset 代表这个 LogicalPlan 是属于哪个 Select语句的
offset := lChild.SelectBlockOffset()
if offset != rChild.SelectBlockOffset() {
offset = -1
}
join := LogicalJoin{
JoinType: InnerJoin,
reordered: true,
}.Init(s.ctx, offset)
join.SetSchema(expression.MergeSchema(lChild.Schema(), rChild.Schema()))
join.SetChildren(lChild, rChild)
return join
}
joinReorderDPSolver
type joinReorderDPSolver struct {
*baseSingleGroupJoinOrderSolver
newJoin func(lChild, rChild LogicalPlan, eqConds []*expression.ScalarFunction, otherConds []expression.Expression) LogicalPlan
}
func (s *joinReorderDPSolver) solve(joinGroup []LogicalPlan, eqConds []expression.Expression) (LogicalPlan, error) {
for _, node := range joinGroup {
_, err := node.recursiveDeriveStats(nil)
if err != nil {
return nil, err
}
s.curJoinGroup = append(s.curJoinGroup, &jrNode{
p: node,
cumCost: s.baseNodeCumCost(node),
})
}
// adjacents[i] 代表的是所有和 i 存在 eqCond 的 LogicalPlan
adjacents := make([][]int, len(s.curJoinGroup))
// totalEqEdges 代表的是所有 eqConds
totalEqEdges := make([]joinGroupEqEdge, 0, len(eqConds))
// Build Graph for join group
for _, cond := range eqConds {
sf := cond.(*expression.ScalarFunction)
lCol := sf.GetArgs()[0].(*expression.Column)
rCol := sf.GetArgs()[1].(*expression.Column)
lIdx, err := findNodeIndexInGroup(joinGroup, lCol)
if err != nil {
return nil, err
}
rIdx, err := findNodeIndexInGroup(joinGroup, rCol)
if err != nil {
return nil, err
}
// lIdx 和 rIdx 之间存在 eqcond
totalEqEdges = append(totalEqEdges, joinGroupEqEdge{
nodeIDs: []int{node1, node2},
edge: edgeContent,
})
adjacents[node1] = append(adjacents[node1], node2)
adjacents[node2] = append(adjacents[node2], node1)
}
// totalNonEqEdges 代表的是所有 otherConds
totalNonEqEdges := make([]joinGroupNonEqEdge, 0, len(s.otherConds))
for _, cond := range s.otherConds {
cols := expression.ExtractColumns(cond)
mask := uint(0)
ids := make([]int, 0, len(cols))
for _, col := range cols {
idx, err := findNodeIndexInGroup(joinGroup, col)
if err != nil {
return nil, err
}
ids = append(ids, idx)
// 用一个 mask 来表示这个 cond 所涉及到的所有 col 所属的 LogicalPlan 的 index
// 把 1 左移 uint(idx) 位
mask |= 1 << uint(idx)
}
totalNonEqEdges = append(totalNonEqEdges, joinGroupNonEqEdge{
nodeIDs: ids,
nodeIDMask: mask,
expr: cond,
})
}
visited := make([]bool, len(joinGroup))
nodeID2VisitID := make([]int, len(joinGroup))
var joins []LogicalPlan
// BFS the tree.
for i := 0; i < len(joinGroup); i++ {
if visited[i] {
continue
}
// 找到和 i 存在直接或间接 eqcond 的所有 node
visitID2NodeID := s.bfsGraph(i, visited, adjacents, nodeID2VisitID)
nodeIDMask := uint(0)
for _, nodeID := range visitID2NodeID {
nodeIDMask |= 1 << uint(nodeID)
}
var subNonEqEdges []joinGroupNonEqEdge
for i := len(totalNonEqEdges) - 1; i >= 0; i-- {
// If this edge is not the subset of the current sub graph.
// totalNonEqEdges[i] 中所涉及到的部分或所有 node,不存在于 visitID2NodeID 中
// 忽略这个不属于这个子图的 NonEqEdge
if totalNonEqEdges[i].nodeIDMask&nodeIDMask != totalNonEqEdges[i].nodeIDMask {
continue
}
// 重新设置一下 totalNonEqEdges[i].nodeIDMask
newMask := uint(0)
for _, nodeID := range totalNonEqEdges[i].nodeIDs {
newMask |= 1 << uint(nodeID2VisitID[nodeID])
}
totalNonEqEdges[i].nodeIDMask = newMask
// 把这一项加入到 subNonEqEdges 中去
subNonEqEdges = append(subNonEqEdges, totalNonEqEdges[i])
// 从 totalNonEqEdges 删除这一项
totalNonEqEdges = append(totalNonEqEdges[:i], totalNonEqEdges[i+1:]...)
}
// Do DP on each sub graph.
join, err := s.dpGraph(visitID2NodeID, nodeID2VisitID, joinGroup, totalEqEdges, subNonEqEdges)
if err != nil {
return nil, err
}
// joins 中的每一项和剩余其他项都没有关联,不存在 eqconds
joins = append(joins, join)
}
remainedOtherConds := make([]expression.Expression, 0, len(totalNonEqEdges))
for _, edge := range totalNonEqEdges {
remainedOtherConds = append(remainedOtherConds, edge.expr)
}
// Build bushy tree for cartesian joins.
return s.makeBushyJoin(joins, remainedOtherConds), nil
}
// bfsGraph bfs a sub graph starting at startPos. And relabel its label for future use.
func (s *joinReorderDPSolver) bfsGraph(startNode int, visited []bool, adjacents [][]int, nodeID2VistID []int) []int {
queue := []int{startNode}
visited[startNode] = true
var visitID2NodeID []int
for len(queue) > 0 {
curNodeID := queue[0]
queue = queue[1:]
// nodeID2VistID 记录了 NodeID 在 visitID2NodeID 中的 index
nodeID2VistID[curNodeID] = len(visitID2NodeID)
// visitID2NodeID 记录了所有和 startNode 直接相关或者间接相关的 Node
visitID2NodeID = append(visitID2NodeID, curNodeID)
for _, adjNodeID := range adjacents[curNodeID] {
if visited[adjNodeID] {
continue
}
queue = append(queue, adjNodeID)
visited[adjNodeID] = true
}
}
return visitID2NodeID
}
// dpGraph is the core part of this algorithm.
// It implements the traditional join reorder algorithm: DP by subset using the following formula:
// bestPlan[S:set of node] = the best one among Join(bestPlan[S1:subset of S], bestPlan[S2: S/S1])
func (s *joinReorderDPSolver) dpGraph(visitID2NodeID, nodeID2VisitID []int, joinGroup []LogicalPlan,
totalEqEdges []joinGroupEqEdge, totalNonEqEdges []joinGroupNonEqEdge) (LogicalPlan, error) {
nodeCnt := uint(len(visitID2NodeID))
// 1<<nodeCnt 是 1 左移 nodeCnt 位,
// 代表 visitID2NodeID 有 2 的 nodeCnt 次方种子集
// 把每种子集的 bestplan 求出来,就解出来了
bestPlan := make([]*jrNode, 1<<nodeCnt)
// bestPlan[s] is nil can be treated as bestCost[s] = +inf.
// 这里先把只有一个 node 的子集的 bestplan 设置一下
// 只有一个 node 的子集的 bestplan 就是这个 node 自己嘛
for i := uint(0); i < nodeCnt; i++ {
bestPlan[1<<i] = s.curJoinGroup[visitID2NodeID[i]]
}
// Enumerate the nodeBitmap from small to big, make sure that S1 must be enumerated before S2 if S1 belongs to S2.
// 每次循环,nodeBitmap 都代表一个集合,是 visitID2NodeID 的一个子集
for nodeBitmap := uint(1); nodeBitmap < (1 << nodeCnt); nodeBitmap++ {
// 只有一个 node 的子集的 bestplan 已经被设置过了
if bits.OnesCount(nodeBitmap) == 1 {
continue
}
// This loop can iterate all its subset.
// 遍历 nodeBitmap 所代表集合的所有子集 sub
// remain 为 sub 的补集,remain + sub = nodeBitmap
for sub := (nodeBitmap - 1) & nodeBitmap; sub > 0; sub = (sub - 1) & nodeBitmap {
remain := nodeBitmap ^ sub
if sub > remain {
continue
}
// If this subset is not connected skip it.
if bestPlan[sub] == nil || bestPlan[remain] == nil {
continue
}
// Get the edge connecting the two parts.
// 检查 sub 所代表的 nodeBitmap 的子集和,与 remain 所代表的 nodeBitmap 的子集和
// 之间是否存在 eqconds
usedEdges, otherConds := s.nodesAreConnected(sub, remain, nodeID2VisitID, totalEqEdges, totalNonEqEdges)
// Here we only check equal condition currently.
if len(usedEdges) == 0 {
continue
}
// 将 bestPlan[sub].p 和 bestPlan[remain].p 做一个 join
join, err := s.newJoinWithEdge(bestPlan[sub].p, bestPlan[remain].p, usedEdges, otherConds)
if err != nil {
return nil, err
}
// 计算出 cost
curCost := s.calcJoinCumCost(join, bestPlan[sub], bestPlan[remain])
// 更新 bestPlan[nodeBitmap]
if bestPlan[nodeBitmap] == nil {
bestPlan[nodeBitmap] = &jrNode{
p: join,
cumCost: curCost,
}
} else if bestPlan[nodeBitmap].cumCost > curCost {
bestPlan[nodeBitmap].p = join
bestPlan[nodeBitmap].cumCost = curCost
}
}
}
return bestPlan[(1<<nodeCnt)-1].p, nil
}