1142 lines
34 KiB
Go
1142 lines
34 KiB
Go
// Copyright 2016 PingCAP, Inc.
|
|
//
|
|
// Licensed 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,
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package plan
|
|
|
|
import (
|
|
"math"
|
|
|
|
"github.com/juju/errors"
|
|
"github.com/ngaut/log"
|
|
"github.com/pingcap/tidb/context"
|
|
"github.com/pingcap/tidb/expression"
|
|
"github.com/pingcap/tidb/infoschema"
|
|
"github.com/pingcap/tidb/kv"
|
|
"github.com/pingcap/tidb/model"
|
|
"github.com/pingcap/tidb/mysql"
|
|
"github.com/pingcap/tidb/terror"
|
|
"github.com/pingcap/tidb/util/types"
|
|
)
|
|
|
|
const (
|
|
netWorkFactor = 1.5
|
|
memoryFactor = 5.0
|
|
selectionFactor = 0.8
|
|
cpuFactor = 0.9
|
|
aggFactor = 0.1
|
|
joinFactor = 0.3
|
|
)
|
|
|
|
// JoinConcurrency means the number of goroutines that participate in joining.
|
|
var JoinConcurrency = 5
|
|
|
|
func (p *DataSource) convert2TableScan(prop *requiredProperty) (*physicalPlanInfo, error) {
|
|
client := p.ctx.GetClient()
|
|
ts := &PhysicalTableScan{
|
|
Table: p.tableInfo,
|
|
Columns: p.Columns,
|
|
TableAsName: p.TableAsName,
|
|
DBName: p.DBName,
|
|
physicalTableSource: physicalTableSource{client: client},
|
|
}
|
|
ts.tp = Tbl
|
|
ts.allocator = p.allocator
|
|
ts.SetSchema(p.Schema())
|
|
ts.initIDAndContext(p.ctx)
|
|
if p.ctx.Txn() != nil {
|
|
ts.readOnly = p.ctx.Txn().IsReadOnly()
|
|
} else {
|
|
ts.readOnly = true
|
|
}
|
|
|
|
var resultPlan PhysicalPlan
|
|
resultPlan = ts
|
|
table := p.tableInfo
|
|
sc := p.ctx.GetSessionVars().StmtCtx
|
|
if sel, ok := p.parents[0].(*Selection); ok {
|
|
newSel := *sel
|
|
conds := make([]expression.Expression, 0, len(sel.Conditions))
|
|
for _, cond := range sel.Conditions {
|
|
conds = append(conds, cond.Clone())
|
|
}
|
|
ts.AccessCondition, newSel.Conditions = detachTableScanConditions(conds, table)
|
|
ts.TableConditionPBExpr, ts.tableFilterConditions, newSel.Conditions =
|
|
expressionsToPB(sc, newSel.Conditions, client)
|
|
err := buildTableRange(ts)
|
|
if err != nil {
|
|
return nil, errors.Trace(err)
|
|
}
|
|
if len(newSel.Conditions) > 0 {
|
|
newSel.SetChildren(ts)
|
|
newSel.onTable = true
|
|
resultPlan = &newSel
|
|
}
|
|
} else {
|
|
ts.Ranges = []TableRange{{math.MinInt64, math.MaxInt64}}
|
|
}
|
|
statsTbl := p.statisticTable
|
|
rowCount := uint64(statsTbl.Count)
|
|
if table.PKIsHandle {
|
|
for i, colInfo := range ts.Columns {
|
|
if mysql.HasPriKeyFlag(colInfo.Flag) {
|
|
ts.pkCol = p.Schema().Columns[i]
|
|
break
|
|
}
|
|
}
|
|
var offset int
|
|
for _, colInfo := range table.Columns {
|
|
if mysql.HasPriKeyFlag(colInfo.Flag) {
|
|
offset = colInfo.Offset
|
|
break
|
|
}
|
|
}
|
|
var err error
|
|
rowCount, err = getRowCountByTableRange(sc, statsTbl, ts.Ranges, offset)
|
|
if err != nil {
|
|
return nil, errors.Trace(err)
|
|
}
|
|
}
|
|
if ts.TableConditionPBExpr != nil {
|
|
rowCount = uint64(float64(rowCount) * selectionFactor)
|
|
}
|
|
return resultPlan.matchProperty(prop, &physicalPlanInfo{count: rowCount}), nil
|
|
}
|
|
|
|
func (p *DataSource) convert2IndexScan(prop *requiredProperty, index *model.IndexInfo) (*physicalPlanInfo, error) {
|
|
client := p.ctx.GetClient()
|
|
is := &PhysicalIndexScan{
|
|
Index: index,
|
|
Table: p.tableInfo,
|
|
Columns: p.Columns,
|
|
TableAsName: p.TableAsName,
|
|
OutOfOrder: true,
|
|
DBName: p.DBName,
|
|
physicalTableSource: physicalTableSource{client: client},
|
|
}
|
|
is.tp = Idx
|
|
is.allocator = p.allocator
|
|
is.initIDAndContext(p.ctx)
|
|
is.SetSchema(p.schema)
|
|
if p.ctx.Txn() != nil {
|
|
is.readOnly = p.ctx.Txn().IsReadOnly()
|
|
} else {
|
|
is.readOnly = true
|
|
}
|
|
|
|
var resultPlan PhysicalPlan
|
|
resultPlan = is
|
|
statsTbl := p.statisticTable
|
|
rowCount := uint64(statsTbl.Count)
|
|
sc := p.ctx.GetSessionVars().StmtCtx
|
|
if sel, ok := p.parents[0].(*Selection); ok {
|
|
newSel := *sel
|
|
conds := make([]expression.Expression, 0, len(sel.Conditions))
|
|
for _, cond := range sel.Conditions {
|
|
conds = append(conds, cond.Clone())
|
|
}
|
|
is.AccessCondition, newSel.Conditions = detachIndexScanConditions(conds, is)
|
|
memDB := infoschema.IsMemoryDB(p.DBName.L)
|
|
isDistReq := !memDB && client != nil && client.SupportRequestType(kv.ReqTypeIndex, 0)
|
|
if isDistReq {
|
|
idxConds, tblConds := detachIndexFilterConditions(newSel.Conditions, is.Index.Columns, is.Table)
|
|
is.IndexConditionPBExpr, is.indexFilterConditions, idxConds = expressionsToPB(sc, idxConds, client)
|
|
is.TableConditionPBExpr, is.tableFilterConditions, tblConds = expressionsToPB(sc, tblConds, client)
|
|
newSel.Conditions = append(idxConds, tblConds...)
|
|
}
|
|
err := buildIndexRange(p.ctx.GetSessionVars().StmtCtx, is)
|
|
if err != nil {
|
|
if !terror.ErrorEqual(err, types.ErrTruncated) {
|
|
return nil, errors.Trace(err)
|
|
}
|
|
log.Warn("truncate error in buildIndexRange")
|
|
}
|
|
rowCount, err = is.getRowCountByIndexRanges(sc, statsTbl)
|
|
if err != nil {
|
|
return nil, errors.Trace(err)
|
|
}
|
|
if len(newSel.Conditions) > 0 {
|
|
newSel.SetChildren(is)
|
|
newSel.onTable = true
|
|
resultPlan = &newSel
|
|
}
|
|
} else {
|
|
rb := rangeBuilder{sc: p.ctx.GetSessionVars().StmtCtx}
|
|
is.Ranges = rb.buildIndexRanges(fullRange, types.NewFieldType(mysql.TypeNull))
|
|
}
|
|
is.DoubleRead = !isCoveringIndex(is.Columns, is.Index.Columns, is.Table.PKIsHandle)
|
|
return resultPlan.matchProperty(prop, &physicalPlanInfo{count: rowCount}), nil
|
|
}
|
|
|
|
func isCoveringIndex(columns []*model.ColumnInfo, indexColumns []*model.IndexColumn, pkIsHandle bool) bool {
|
|
for _, colInfo := range columns {
|
|
if pkIsHandle && mysql.HasPriKeyFlag(colInfo.Flag) {
|
|
continue
|
|
}
|
|
isIndexColumn := false
|
|
for _, indexCol := range indexColumns {
|
|
if colInfo.Name.L == indexCol.Name.L && indexCol.Length == types.UnspecifiedLength {
|
|
isIndexColumn = true
|
|
break
|
|
}
|
|
}
|
|
if !isIndexColumn {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (p *DataSource) need2ConsiderIndex(prop *requiredProperty) bool {
|
|
if _, ok := p.parents[0].(*Selection); ok || len(prop.props) > 0 {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// convert2PhysicalPlan implements the LogicalPlan convert2PhysicalPlan interface.
|
|
// If there is no index that matches the required property, the returned physicalPlanInfo
|
|
// will be table scan and has the cost of MaxInt64. But this can be ignored because the parent will call
|
|
// convert2PhysicalPlan again with an empty *requiredProperty, so the plan with the lowest
|
|
// cost will be chosen.
|
|
func (p *DataSource) convert2PhysicalPlan(prop *requiredProperty) (*physicalPlanInfo, error) {
|
|
info, err := p.getPlanInfo(prop)
|
|
if err != nil {
|
|
return nil, errors.Trace(err)
|
|
}
|
|
if info != nil {
|
|
return info, nil
|
|
}
|
|
info, err = p.tryToConvert2DummyScan(prop)
|
|
if info != nil || err != nil {
|
|
return info, errors.Trace(err)
|
|
}
|
|
client := p.ctx.GetClient()
|
|
memDB := infoschema.IsMemoryDB(p.DBName.L)
|
|
isDistReq := !memDB && client != nil && client.SupportRequestType(kv.ReqTypeSelect, 0)
|
|
if !isDistReq {
|
|
memTable := &PhysicalMemTable{
|
|
DBName: p.DBName,
|
|
Table: p.tableInfo,
|
|
Columns: p.Columns,
|
|
TableAsName: p.TableAsName,
|
|
}
|
|
memTable.SetSchema(p.schema)
|
|
rb := &rangeBuilder{sc: p.ctx.GetSessionVars().StmtCtx}
|
|
memTable.Ranges = rb.buildTableRanges(fullRange)
|
|
info = &physicalPlanInfo{p: memTable}
|
|
info = enforceProperty(prop, info)
|
|
p.storePlanInfo(prop, info)
|
|
return info, nil
|
|
}
|
|
indices, includeTableScan := availableIndices(p.indexHints, p.tableInfo)
|
|
if includeTableScan {
|
|
info, err = p.convert2TableScan(prop)
|
|
if err != nil {
|
|
return nil, errors.Trace(err)
|
|
}
|
|
}
|
|
if !includeTableScan || p.need2ConsiderIndex(prop) {
|
|
for _, index := range indices {
|
|
indexInfo, err := p.convert2IndexScan(prop, index)
|
|
if err != nil {
|
|
return nil, errors.Trace(err)
|
|
}
|
|
if info == nil || indexInfo.cost < info.cost {
|
|
info = indexInfo
|
|
}
|
|
}
|
|
}
|
|
return info, errors.Trace(p.storePlanInfo(prop, info))
|
|
}
|
|
|
|
// tryToConvert2DummyScan is an optimization which checks if its parent is a selection with a constant condition
|
|
// that evaluates to false. If it is, there is no need for a real physical scan, a dummy scan will do.
|
|
func (p *DataSource) tryToConvert2DummyScan(prop *requiredProperty) (*physicalPlanInfo, error) {
|
|
sel, isSel := p.parents[0].(*Selection)
|
|
if !isSel {
|
|
return nil, nil
|
|
}
|
|
|
|
for _, cond := range sel.Conditions {
|
|
if con, ok := cond.(*expression.Constant); ok {
|
|
result, err := expression.EvalBool(con, nil, p.ctx)
|
|
if err != nil {
|
|
return nil, errors.Trace(err)
|
|
}
|
|
if !result {
|
|
dummy := &PhysicalDummyScan{}
|
|
dummy.tp = "Dummy"
|
|
dummy.allocator = p.allocator
|
|
dummy.initIDAndContext(p.ctx)
|
|
dummy.SetSchema(p.schema)
|
|
info := &physicalPlanInfo{p: dummy}
|
|
p.storePlanInfo(prop, info)
|
|
return info, nil
|
|
}
|
|
}
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
// addPlanToResponse creates a *physicalPlanInfo that adds p as the parent of info.
|
|
func addPlanToResponse(parent PhysicalPlan, info *physicalPlanInfo) *physicalPlanInfo {
|
|
np := parent.Copy()
|
|
np.SetChildren(info.p)
|
|
return &physicalPlanInfo{p: np, cost: info.cost, count: info.count}
|
|
}
|
|
|
|
// enforceProperty creates a *physicalPlanInfo that satisfies the required property by adding
|
|
// sort or limit as the parent of the given physical plan.
|
|
func enforceProperty(prop *requiredProperty, info *physicalPlanInfo) *physicalPlanInfo {
|
|
if info.p == nil {
|
|
return info
|
|
}
|
|
if len(prop.props) != 0 {
|
|
items := make([]*ByItems, 0, len(prop.props))
|
|
for _, col := range prop.props {
|
|
items = append(items, &ByItems{Expr: col.col, Desc: col.desc})
|
|
}
|
|
sort := &Sort{
|
|
ByItems: items,
|
|
ExecLimit: prop.limit,
|
|
}
|
|
sort.SetSchema(info.p.Schema())
|
|
info = addPlanToResponse(sort, info)
|
|
count := info.count
|
|
if prop.limit != nil {
|
|
count = prop.limit.Offset + prop.limit.Count
|
|
}
|
|
info.cost += sortCost(count)
|
|
} else if prop.limit != nil {
|
|
limit := prop.limit.Copy().(*Limit)
|
|
limit.SetSchema(info.p.Schema())
|
|
info = addPlanToResponse(limit, info)
|
|
}
|
|
if prop.limit != nil && prop.limit.Count < info.count {
|
|
info.count = prop.limit.Count
|
|
}
|
|
return info
|
|
}
|
|
|
|
func sortCost(cnt uint64) float64 {
|
|
if cnt == 0 {
|
|
// If cnt is 0, the log(cnt) will be NAN.
|
|
return 0.0
|
|
}
|
|
return float64(cnt)*math.Log2(float64(cnt))*cpuFactor + memoryFactor*float64(cnt)
|
|
}
|
|
|
|
// removeLimit removes the limit from prop.
|
|
func removeLimit(prop *requiredProperty) *requiredProperty {
|
|
ret := &requiredProperty{
|
|
props: prop.props,
|
|
sortKeyLen: prop.sortKeyLen,
|
|
}
|
|
return ret
|
|
}
|
|
|
|
// convertLimitOffsetToCount changes the limit(offset, count) in prop to limit(0, offset + count).
|
|
func convertLimitOffsetToCount(prop *requiredProperty) *requiredProperty {
|
|
ret := &requiredProperty{
|
|
props: prop.props,
|
|
sortKeyLen: prop.sortKeyLen,
|
|
}
|
|
if prop.limit != nil {
|
|
ret.limit = &Limit{
|
|
Count: prop.limit.Offset + prop.limit.Count,
|
|
}
|
|
}
|
|
return ret
|
|
}
|
|
|
|
func limitProperty(limit *Limit) *requiredProperty {
|
|
return &requiredProperty{limit: limit}
|
|
}
|
|
|
|
// convert2PhysicalPlan implements the LogicalPlan convert2PhysicalPlan interface.
|
|
func (p *Limit) convert2PhysicalPlan(prop *requiredProperty) (*physicalPlanInfo, error) {
|
|
info, err := p.getPlanInfo(prop)
|
|
if err != nil {
|
|
return nil, errors.Trace(err)
|
|
}
|
|
if info != nil {
|
|
return info, nil
|
|
}
|
|
info, err = p.children[0].(LogicalPlan).convert2PhysicalPlan(limitProperty(&Limit{Offset: p.Offset, Count: p.Count}))
|
|
if err != nil {
|
|
return nil, errors.Trace(err)
|
|
}
|
|
info = enforceProperty(prop, info)
|
|
p.storePlanInfo(prop, info)
|
|
return info, nil
|
|
}
|
|
|
|
// convert2PhysicalPlanSemi converts the semi join to *physicalPlanInfo.
|
|
func (p *Join) convert2PhysicalPlanSemi(prop *requiredProperty) (*physicalPlanInfo, error) {
|
|
lChild := p.children[0].(LogicalPlan)
|
|
rChild := p.children[1].(LogicalPlan)
|
|
allLeft := true
|
|
for _, col := range prop.props {
|
|
if !lChild.Schema().Contains(col.col) {
|
|
allLeft = false
|
|
}
|
|
}
|
|
join := &PhysicalHashSemiJoin{
|
|
WithAux: LeftOuterSemiJoin == p.JoinType,
|
|
EqualConditions: p.EqualConditions,
|
|
LeftConditions: p.LeftConditions,
|
|
RightConditions: p.RightConditions,
|
|
OtherConditions: p.OtherConditions,
|
|
Anti: p.anti,
|
|
}
|
|
join.ctx = p.ctx
|
|
join.tp = "HashSemiJoin"
|
|
join.allocator = p.allocator
|
|
join.initIDAndContext(p.ctx)
|
|
join.SetSchema(p.schema)
|
|
lProp := prop
|
|
if !allLeft {
|
|
lProp = &requiredProperty{}
|
|
}
|
|
if p.JoinType == SemiJoin {
|
|
lProp = removeLimit(lProp)
|
|
}
|
|
lInfo, err := lChild.convert2PhysicalPlan(lProp)
|
|
if err != nil {
|
|
return nil, errors.Trace(err)
|
|
}
|
|
rInfo, err := rChild.convert2PhysicalPlan(&requiredProperty{})
|
|
if err != nil {
|
|
return nil, errors.Trace(err)
|
|
}
|
|
resultInfo := join.matchProperty(prop, lInfo, rInfo)
|
|
if p.JoinType == SemiJoin {
|
|
resultInfo.count = uint64(float64(lInfo.count) * selectionFactor)
|
|
} else {
|
|
resultInfo.count = lInfo.count
|
|
}
|
|
if !allLeft {
|
|
resultInfo = enforceProperty(prop, resultInfo)
|
|
} else if p.JoinType == SemiJoin {
|
|
resultInfo = enforceProperty(limitProperty(prop.limit), resultInfo)
|
|
}
|
|
return resultInfo, nil
|
|
}
|
|
|
|
// convert2PhysicalPlanLeft converts the left join to *physicalPlanInfo.
|
|
func (p *Join) convert2PhysicalPlanLeft(prop *requiredProperty, innerJoin bool) (*physicalPlanInfo, error) {
|
|
lChild := p.children[0].(LogicalPlan)
|
|
rChild := p.children[1].(LogicalPlan)
|
|
allLeft := true
|
|
for _, col := range prop.props {
|
|
if !lChild.Schema().Contains(col.col) {
|
|
allLeft = false
|
|
}
|
|
}
|
|
join := &PhysicalHashJoin{
|
|
EqualConditions: p.EqualConditions,
|
|
LeftConditions: p.LeftConditions,
|
|
RightConditions: p.RightConditions,
|
|
OtherConditions: p.OtherConditions,
|
|
SmallTable: 1,
|
|
// TODO: decide concurrency by data size.
|
|
Concurrency: JoinConcurrency,
|
|
DefaultValues: p.DefaultValues,
|
|
}
|
|
join.tp = "HashLeftJoin"
|
|
join.allocator = p.allocator
|
|
join.initIDAndContext(lChild.context())
|
|
join.SetSchema(p.schema)
|
|
if innerJoin {
|
|
join.JoinType = InnerJoin
|
|
} else {
|
|
join.JoinType = LeftOuterJoin
|
|
}
|
|
lProp := prop
|
|
if !allLeft {
|
|
lProp = &requiredProperty{}
|
|
}
|
|
var lInfo *physicalPlanInfo
|
|
var err error
|
|
if innerJoin {
|
|
lInfo, err = lChild.convert2PhysicalPlan(removeLimit(lProp))
|
|
} else {
|
|
lInfo, err = lChild.convert2PhysicalPlan(convertLimitOffsetToCount(lProp))
|
|
}
|
|
if err != nil {
|
|
return nil, errors.Trace(err)
|
|
}
|
|
rInfo, err := rChild.convert2PhysicalPlan(&requiredProperty{})
|
|
if err != nil {
|
|
return nil, errors.Trace(err)
|
|
}
|
|
resultInfo := join.matchProperty(prop, lInfo, rInfo)
|
|
if !allLeft {
|
|
resultInfo = enforceProperty(prop, resultInfo)
|
|
} else {
|
|
resultInfo = enforceProperty(limitProperty(prop.limit), resultInfo)
|
|
}
|
|
return resultInfo, nil
|
|
}
|
|
|
|
// replaceColsInPropBySchema replaces the columns in original prop with the columns in schema.
|
|
func replaceColsInPropBySchema(prop *requiredProperty, schema *expression.Schema) *requiredProperty {
|
|
newProps := make([]*columnProp, 0, len(prop.props))
|
|
for _, p := range prop.props {
|
|
idx := schema.ColumnIndex(p.col)
|
|
if idx == -1 {
|
|
log.Errorf("Can't find column %s in schema", p.col)
|
|
}
|
|
newProps = append(newProps, &columnProp{col: schema.Columns[idx], desc: p.desc})
|
|
}
|
|
return &requiredProperty{
|
|
props: newProps,
|
|
sortKeyLen: prop.sortKeyLen,
|
|
limit: prop.limit,
|
|
}
|
|
}
|
|
|
|
// convert2PhysicalPlanRight converts the right join to *physicalPlanInfo.
|
|
func (p *Join) convert2PhysicalPlanRight(prop *requiredProperty, innerJoin bool) (*physicalPlanInfo, error) {
|
|
lChild := p.children[0].(LogicalPlan)
|
|
rChild := p.children[1].(LogicalPlan)
|
|
allRight := true
|
|
for _, col := range prop.props {
|
|
if !rChild.Schema().Contains(col.col) {
|
|
allRight = false
|
|
}
|
|
}
|
|
join := &PhysicalHashJoin{
|
|
EqualConditions: p.EqualConditions,
|
|
LeftConditions: p.LeftConditions,
|
|
RightConditions: p.RightConditions,
|
|
OtherConditions: p.OtherConditions,
|
|
// TODO: decide concurrency by data size.
|
|
Concurrency: JoinConcurrency,
|
|
DefaultValues: p.DefaultValues,
|
|
}
|
|
join.tp = "HashRightJoin"
|
|
join.allocator = p.allocator
|
|
join.initIDAndContext(p.ctx)
|
|
join.SetSchema(p.schema)
|
|
if innerJoin {
|
|
join.JoinType = InnerJoin
|
|
} else {
|
|
join.JoinType = RightOuterJoin
|
|
}
|
|
lInfo, err := lChild.convert2PhysicalPlan(&requiredProperty{})
|
|
if err != nil {
|
|
return nil, errors.Trace(err)
|
|
}
|
|
rProp := prop
|
|
if !allRight {
|
|
rProp = &requiredProperty{}
|
|
} else {
|
|
rProp = replaceColsInPropBySchema(rProp, rChild.Schema())
|
|
}
|
|
var rInfo *physicalPlanInfo
|
|
if innerJoin {
|
|
rInfo, err = rChild.convert2PhysicalPlan(removeLimit(rProp))
|
|
} else {
|
|
rInfo, err = rChild.convert2PhysicalPlan(convertLimitOffsetToCount(rProp))
|
|
}
|
|
if err != nil {
|
|
return nil, errors.Trace(err)
|
|
}
|
|
resultInfo := join.matchProperty(prop, lInfo, rInfo)
|
|
if !allRight {
|
|
resultInfo = enforceProperty(prop, resultInfo)
|
|
} else {
|
|
resultInfo = enforceProperty(limitProperty(prop.limit), resultInfo)
|
|
}
|
|
return resultInfo, nil
|
|
}
|
|
|
|
// convert2PhysicalPlan implements the LogicalPlan convert2PhysicalPlan interface.
|
|
func (p *Join) convert2PhysicalPlan(prop *requiredProperty) (*physicalPlanInfo, error) {
|
|
info, err := p.getPlanInfo(prop)
|
|
if err != nil {
|
|
return nil, errors.Trace(err)
|
|
}
|
|
if info != nil {
|
|
return info, nil
|
|
}
|
|
switch p.JoinType {
|
|
case SemiJoin, LeftOuterSemiJoin:
|
|
info, err = p.convert2PhysicalPlanSemi(prop)
|
|
if err != nil {
|
|
return nil, errors.Trace(err)
|
|
}
|
|
case LeftOuterJoin:
|
|
info, err = p.convert2PhysicalPlanLeft(prop, false)
|
|
if err != nil {
|
|
return nil, errors.Trace(err)
|
|
}
|
|
case RightOuterJoin:
|
|
info, err = p.convert2PhysicalPlanRight(prop, false)
|
|
if err != nil {
|
|
return nil, errors.Trace(err)
|
|
}
|
|
default:
|
|
lInfo, err := p.convert2PhysicalPlanLeft(prop, true)
|
|
if err != nil {
|
|
return nil, errors.Trace(err)
|
|
}
|
|
rInfo, err := p.convert2PhysicalPlanRight(prop, true)
|
|
if err != nil {
|
|
return nil, errors.Trace(err)
|
|
}
|
|
if rInfo.cost < lInfo.cost {
|
|
info = rInfo
|
|
} else {
|
|
info = lInfo
|
|
}
|
|
}
|
|
p.storePlanInfo(prop, info)
|
|
return info, nil
|
|
}
|
|
|
|
// convert2PhysicalPlanStream converts the logical aggregation to the stream aggregation *physicalPlanInfo.
|
|
func (p *Aggregation) convert2PhysicalPlanStream(prop *requiredProperty) (*physicalPlanInfo, error) {
|
|
for _, aggFunc := range p.AggFuncs {
|
|
if aggFunc.GetMode() == expression.FinalMode {
|
|
return &physicalPlanInfo{cost: math.MaxFloat64}, nil
|
|
}
|
|
}
|
|
agg := &PhysicalAggregation{
|
|
AggType: StreamedAgg,
|
|
AggFuncs: p.AggFuncs,
|
|
GroupByItems: p.GroupByItems,
|
|
}
|
|
agg.tp = "StreamAgg"
|
|
agg.allocator = p.allocator
|
|
agg.initIDAndContext(p.ctx)
|
|
agg.HasGby = len(p.GroupByItems) > 0
|
|
agg.SetSchema(p.schema)
|
|
// TODO: Consider distinct key.
|
|
info := &physicalPlanInfo{cost: math.MaxFloat64}
|
|
gbyCols := p.groupByCols
|
|
if len(gbyCols) != len(p.GroupByItems) {
|
|
// group by a + b is not interested in any order.
|
|
return info, nil
|
|
}
|
|
isSortKey := make([]bool, len(gbyCols))
|
|
newProp := &requiredProperty{
|
|
props: make([]*columnProp, 0, len(gbyCols)),
|
|
}
|
|
for _, pro := range prop.props {
|
|
idx := p.getGbyColIndex(pro.col)
|
|
if idx == -1 {
|
|
return info, nil
|
|
}
|
|
isSortKey[idx] = true
|
|
// We should add columns in aggregation in order to keep index right.
|
|
newProp.props = append(newProp.props, &columnProp{col: gbyCols[idx], desc: pro.desc})
|
|
}
|
|
newProp.sortKeyLen = len(newProp.props)
|
|
for i, col := range gbyCols {
|
|
if !isSortKey[i] {
|
|
newProp.props = append(newProp.props, &columnProp{col: col})
|
|
}
|
|
}
|
|
childInfo, err := p.children[0].(LogicalPlan).convert2PhysicalPlan(newProp)
|
|
if err != nil {
|
|
return nil, errors.Trace(err)
|
|
}
|
|
info = addPlanToResponse(agg, childInfo)
|
|
info.cost += float64(info.count) * cpuFactor
|
|
info.count = uint64(float64(info.count) * aggFactor)
|
|
return info, nil
|
|
}
|
|
|
|
// convert2PhysicalPlanFinalHash converts the logical aggregation to the final hash aggregation *physicalPlanInfo.
|
|
func (p *Aggregation) convert2PhysicalPlanFinalHash(x physicalDistSQLPlan, childInfo *physicalPlanInfo) *physicalPlanInfo {
|
|
agg := &PhysicalAggregation{
|
|
AggType: FinalAgg,
|
|
AggFuncs: p.AggFuncs,
|
|
GroupByItems: p.GroupByItems,
|
|
}
|
|
agg.tp = "HashAgg"
|
|
agg.allocator = p.allocator
|
|
agg.initIDAndContext(p.ctx)
|
|
agg.SetSchema(p.schema)
|
|
agg.HasGby = len(p.GroupByItems) > 0
|
|
schema := x.addAggregation(p.ctx, agg)
|
|
if schema.Len() == 0 {
|
|
return nil
|
|
}
|
|
x.(PhysicalPlan).SetSchema(schema)
|
|
info := addPlanToResponse(agg, childInfo)
|
|
info.count = uint64(float64(info.count) * aggFactor)
|
|
// if we build the final aggregation, it must be the best plan.
|
|
info.cost = 0
|
|
return info
|
|
}
|
|
|
|
// convert2PhysicalPlanCompleteHash converts the logical aggregation to the complete hash aggregation *physicalPlanInfo.
|
|
func (p *Aggregation) convert2PhysicalPlanCompleteHash(childInfo *physicalPlanInfo) *physicalPlanInfo {
|
|
agg := &PhysicalAggregation{
|
|
AggType: CompleteAgg,
|
|
AggFuncs: p.AggFuncs,
|
|
GroupByItems: p.GroupByItems,
|
|
}
|
|
agg.tp = "HashAgg"
|
|
agg.allocator = p.allocator
|
|
agg.initIDAndContext(p.ctx)
|
|
agg.HasGby = len(p.GroupByItems) > 0
|
|
agg.SetSchema(p.schema)
|
|
info := addPlanToResponse(agg, childInfo)
|
|
info.cost += float64(info.count) * memoryFactor
|
|
info.count = uint64(float64(info.count) * aggFactor)
|
|
return info
|
|
}
|
|
|
|
// convert2PhysicalPlanHash converts the logical aggregation to the physical hash aggregation.
|
|
func (p *Aggregation) convert2PhysicalPlanHash() (*physicalPlanInfo, error) {
|
|
childInfo, err := p.children[0].(LogicalPlan).convert2PhysicalPlan(&requiredProperty{})
|
|
if err != nil {
|
|
return nil, errors.Trace(err)
|
|
}
|
|
distinct := false
|
|
for _, fun := range p.AggFuncs {
|
|
if fun.IsDistinct() {
|
|
distinct = true
|
|
break
|
|
}
|
|
}
|
|
if !distinct {
|
|
if x, ok := childInfo.p.(physicalDistSQLPlan); ok {
|
|
info := p.convert2PhysicalPlanFinalHash(x, childInfo)
|
|
if info != nil {
|
|
return info, nil
|
|
}
|
|
}
|
|
}
|
|
return p.convert2PhysicalPlanCompleteHash(childInfo), nil
|
|
}
|
|
|
|
// convert2PhysicalPlan implements the LogicalPlan convert2PhysicalPlan interface.
|
|
func (p *Aggregation) convert2PhysicalPlan(prop *requiredProperty) (*physicalPlanInfo, error) {
|
|
planInfo, err := p.getPlanInfo(prop)
|
|
if err != nil {
|
|
return nil, errors.Trace(err)
|
|
}
|
|
if planInfo != nil {
|
|
return planInfo, nil
|
|
}
|
|
limit := prop.limit
|
|
if len(prop.props) == 0 {
|
|
planInfo, err = p.convert2PhysicalPlanHash()
|
|
if err != nil {
|
|
return nil, errors.Trace(err)
|
|
}
|
|
}
|
|
streamInfo, err := p.convert2PhysicalPlanStream(removeLimit(prop))
|
|
if planInfo == nil || streamInfo.cost < planInfo.cost {
|
|
planInfo = streamInfo
|
|
}
|
|
planInfo = enforceProperty(limitProperty(limit), planInfo)
|
|
err = p.storePlanInfo(prop, planInfo)
|
|
return planInfo, errors.Trace(err)
|
|
}
|
|
|
|
// convert2PhysicalPlan implements the LogicalPlan convert2PhysicalPlan interface.
|
|
func (p *Union) convert2PhysicalPlan(prop *requiredProperty) (*physicalPlanInfo, error) {
|
|
info, err := p.getPlanInfo(prop)
|
|
if err != nil {
|
|
return nil, errors.Trace(err)
|
|
}
|
|
if info != nil {
|
|
return info, nil
|
|
}
|
|
limit := prop.limit
|
|
childInfos := make([]*physicalPlanInfo, 0, len(p.children))
|
|
for _, child := range p.Children() {
|
|
newProp := &requiredProperty{}
|
|
if limit != nil {
|
|
newProp = convertLimitOffsetToCount(prop)
|
|
newProp.props = make([]*columnProp, 0, len(prop.props))
|
|
for _, c := range prop.props {
|
|
idx := p.Schema().ColumnIndex(c.col)
|
|
newProp.props = append(newProp.props, &columnProp{col: child.Schema().Columns[idx], desc: c.desc})
|
|
}
|
|
}
|
|
childInfo, err := child.(LogicalPlan).convert2PhysicalPlan(newProp)
|
|
if err != nil {
|
|
return nil, errors.Trace(err)
|
|
}
|
|
childInfos = append(childInfos, childInfo)
|
|
}
|
|
info = p.matchProperty(prop, childInfos...)
|
|
info = enforceProperty(prop, info)
|
|
p.storePlanInfo(prop, info)
|
|
return info, nil
|
|
}
|
|
|
|
// convert2PhysicalPlan implements the LogicalPlan convert2PhysicalPlan interface.
|
|
func (p *Selection) convert2PhysicalPlan(prop *requiredProperty) (*physicalPlanInfo, error) {
|
|
info, err := p.getPlanInfo(prop)
|
|
if err != nil {
|
|
return nil, errors.Trace(err)
|
|
}
|
|
if info != nil {
|
|
return info, nil
|
|
}
|
|
// Firstly, we try to push order.
|
|
info, err = p.convert2PhysicalPlanPushOrder(prop)
|
|
if err != nil {
|
|
return nil, errors.Trace(err)
|
|
}
|
|
if len(prop.props) > 0 {
|
|
// Secondly, we push nothing and enforce this property.
|
|
infoEnforce, err := p.convert2PhysicalPlanEnforce(prop)
|
|
if err != nil {
|
|
return nil, errors.Trace(err)
|
|
}
|
|
if infoEnforce.cost < info.cost {
|
|
info = infoEnforce
|
|
}
|
|
}
|
|
if ds, ok := p.children[0].(*DataSource); !ok {
|
|
info = p.matchProperty(prop, info)
|
|
} else {
|
|
client := p.ctx.GetClient()
|
|
memDB := infoschema.IsMemoryDB(ds.DBName.L)
|
|
isDistReq := !memDB && client != nil && client.SupportRequestType(kv.ReqTypeSelect, 0)
|
|
if !isDistReq {
|
|
info = p.matchProperty(prop, info)
|
|
}
|
|
}
|
|
p.storePlanInfo(prop, info)
|
|
return info, nil
|
|
}
|
|
|
|
func (p *Selection) convert2PhysicalPlanPushOrder(prop *requiredProperty) (*physicalPlanInfo, error) {
|
|
child := p.children[0].(LogicalPlan)
|
|
limit := prop.limit
|
|
info, err := child.convert2PhysicalPlan(removeLimit(prop))
|
|
if err != nil {
|
|
return nil, errors.Trace(err)
|
|
}
|
|
if limit != nil && info.p != nil {
|
|
if np, ok := info.p.(physicalDistSQLPlan); ok {
|
|
np.addLimit(limit)
|
|
scanCount := info.count
|
|
info.count = limit.Count
|
|
info.cost = np.calculateCost(info.count, scanCount)
|
|
if limit.Offset > 0 {
|
|
info = enforceProperty(&requiredProperty{limit: limit}, info)
|
|
}
|
|
} else {
|
|
info = enforceProperty(&requiredProperty{limit: limit}, info)
|
|
}
|
|
}
|
|
return info, nil
|
|
}
|
|
|
|
// convert2PhysicalPlanEnforce converts a selection to *physicalPlanInfo which does not push the
|
|
// required property to the children, but enforce the property instead.
|
|
func (p *Selection) convert2PhysicalPlanEnforce(prop *requiredProperty) (*physicalPlanInfo, error) {
|
|
child := p.children[0].(LogicalPlan)
|
|
info, err := child.convert2PhysicalPlan(&requiredProperty{})
|
|
if err != nil {
|
|
return nil, errors.Trace(err)
|
|
}
|
|
if prop.limit != nil && len(prop.props) > 0 {
|
|
if t, ok := info.p.(physicalDistSQLPlan); ok {
|
|
t.addTopN(p.ctx, prop)
|
|
}
|
|
info = enforceProperty(prop, info)
|
|
} else if len(prop.props) != 0 {
|
|
info.cost = math.MaxFloat64
|
|
}
|
|
return info, nil
|
|
}
|
|
|
|
// convert2PhysicalPlan implements the LogicalPlan convert2PhysicalPlan interface.
|
|
func (p *Projection) convert2PhysicalPlan(prop *requiredProperty) (*physicalPlanInfo, error) {
|
|
info, err := p.getPlanInfo(prop)
|
|
if err != nil {
|
|
return nil, errors.Trace(err)
|
|
}
|
|
if info != nil {
|
|
return info, nil
|
|
}
|
|
newProp := &requiredProperty{
|
|
props: make([]*columnProp, 0, len(prop.props)),
|
|
sortKeyLen: prop.sortKeyLen,
|
|
limit: prop.limit}
|
|
childSchema := p.children[0].Schema()
|
|
usedCols := make([]bool, childSchema.Len())
|
|
canPassSort := true
|
|
loop:
|
|
for _, c := range prop.props {
|
|
idx := p.schema.ColumnIndex(c.col)
|
|
switch v := p.Exprs[idx].(type) {
|
|
case *expression.Column:
|
|
childIdx := childSchema.ColumnIndex(v)
|
|
if !usedCols[childIdx] {
|
|
usedCols[childIdx] = true
|
|
newProp.props = append(newProp.props, &columnProp{col: v, desc: c.desc})
|
|
}
|
|
case *expression.ScalarFunction:
|
|
newProp = nil
|
|
canPassSort = false
|
|
break loop
|
|
default:
|
|
newProp.sortKeyLen--
|
|
}
|
|
}
|
|
if !canPassSort {
|
|
return &physicalPlanInfo{cost: math.MaxFloat64}, nil
|
|
}
|
|
info, err = p.children[0].(LogicalPlan).convert2PhysicalPlan(newProp)
|
|
if err != nil {
|
|
return nil, errors.Trace(err)
|
|
}
|
|
info = addPlanToResponse(p, info)
|
|
p.storePlanInfo(prop, info)
|
|
return info, nil
|
|
}
|
|
|
|
func matchProp(ctx context.Context, target, new *requiredProperty) bool {
|
|
if target.sortKeyLen > len(new.props) {
|
|
return false
|
|
}
|
|
for i := 0; i < target.sortKeyLen; i++ {
|
|
if !target.props[i].equal(new.props[i], ctx) {
|
|
return false
|
|
}
|
|
}
|
|
for i := target.sortKeyLen; i < len(target.props); i++ {
|
|
isMatch := false
|
|
for _, pro := range new.props {
|
|
if pro.col.Equal(target.props[i].col, ctx) {
|
|
isMatch = true
|
|
break
|
|
}
|
|
}
|
|
if !isMatch {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// convert2PhysicalPlan implements the LogicalPlan convert2PhysicalPlan interface.
|
|
func (p *Sort) convert2PhysicalPlan(prop *requiredProperty) (*physicalPlanInfo, error) {
|
|
info, err := p.getPlanInfo(prop)
|
|
if err != nil {
|
|
return nil, errors.Trace(err)
|
|
}
|
|
if info != nil {
|
|
return info, nil
|
|
}
|
|
if len(p.ByItems) == 0 {
|
|
return p.children[0].(LogicalPlan).convert2PhysicalPlan(prop)
|
|
}
|
|
selfProp := &requiredProperty{
|
|
props: make([]*columnProp, 0, len(p.ByItems)),
|
|
}
|
|
for _, by := range p.ByItems {
|
|
if col, ok := by.Expr.(*expression.Column); ok {
|
|
selfProp.props = append(selfProp.props, &columnProp{col: col, desc: by.Desc})
|
|
} else {
|
|
selfProp.props = nil
|
|
break
|
|
}
|
|
}
|
|
selfProp.sortKeyLen = len(selfProp.props)
|
|
if len(selfProp.props) != 0 && len(prop.props) == 0 && prop.limit != nil {
|
|
selfProp.limit = prop.limit
|
|
}
|
|
sortedPlanInfo, err := p.children[0].(LogicalPlan).convert2PhysicalPlan(selfProp)
|
|
if err != nil {
|
|
return nil, errors.Trace(err)
|
|
}
|
|
unSortedPlanInfo, err := p.children[0].(LogicalPlan).convert2PhysicalPlan(&requiredProperty{})
|
|
if err != nil {
|
|
return nil, errors.Trace(err)
|
|
}
|
|
sortCost := sortCost(unSortedPlanInfo.count)
|
|
if len(selfProp.props) == 0 {
|
|
np := p.Copy().(*Sort)
|
|
np.ExecLimit = prop.limit
|
|
sortedPlanInfo = addPlanToResponse(np, sortedPlanInfo)
|
|
} else if sortCost+unSortedPlanInfo.cost < sortedPlanInfo.cost {
|
|
sortedPlanInfo.cost = sortCost + unSortedPlanInfo.cost
|
|
np := *p
|
|
np.ExecLimit = selfProp.limit
|
|
sortedPlanInfo = addPlanToResponse(&np, unSortedPlanInfo)
|
|
}
|
|
if !matchProp(p.ctx, prop, selfProp) {
|
|
sortedPlanInfo.cost = math.MaxFloat64
|
|
}
|
|
p.storePlanInfo(prop, sortedPlanInfo)
|
|
return sortedPlanInfo, nil
|
|
}
|
|
|
|
// convert2PhysicalPlan implements the LogicalPlan convert2PhysicalPlan interface.
|
|
func (p *Apply) convert2PhysicalPlan(prop *requiredProperty) (*physicalPlanInfo, error) {
|
|
info, err := p.getPlanInfo(prop)
|
|
if err != nil {
|
|
return info, errors.Trace(err)
|
|
}
|
|
if info != nil {
|
|
return info, nil
|
|
}
|
|
if p.JoinType == InnerJoin || p.JoinType == LeftOuterJoin {
|
|
info, err = p.Join.convert2PhysicalPlanLeft(prop, p.JoinType == InnerJoin)
|
|
} else {
|
|
info, err = p.Join.convert2PhysicalPlanSemi(prop)
|
|
}
|
|
if err != nil {
|
|
return info, errors.Trace(err)
|
|
}
|
|
switch info.p.(type) {
|
|
case *PhysicalHashJoin, *PhysicalHashSemiJoin:
|
|
ap := &PhysicalApply{
|
|
PhysicalJoin: info.p,
|
|
OuterSchema: p.corCols,
|
|
}
|
|
ap.tp = App
|
|
ap.allocator = p.allocator
|
|
ap.initIDAndContext(p.ctx)
|
|
ap.SetChildren(info.p.Children()...)
|
|
ap.SetSchema(info.p.Schema())
|
|
info.p = ap
|
|
default:
|
|
info.cost = math.MaxFloat64
|
|
info.p = nil
|
|
}
|
|
p.storePlanInfo(prop, info)
|
|
return info, nil
|
|
}
|
|
|
|
func (p *Analyze) prepareSimpleTableScan(cols []*model.ColumnInfo) *PhysicalTableScan {
|
|
ts := &PhysicalTableScan{
|
|
Table: p.Table.TableInfo,
|
|
Columns: cols,
|
|
TableAsName: &p.Table.Name,
|
|
DBName: p.Table.DBInfo.Name,
|
|
physicalTableSource: physicalTableSource{client: p.ctx.GetClient()},
|
|
}
|
|
ts.tp = Tbl
|
|
ts.allocator = p.allocator
|
|
ts.SetSchema(p.Schema())
|
|
ts.initIDAndContext(p.ctx)
|
|
ts.readOnly = true
|
|
ts.Ranges = []TableRange{{math.MinInt64, math.MaxInt64}}
|
|
return ts
|
|
}
|
|
|
|
func (p *Analyze) prepareSimpleIndexScan(idxOffset int, cols []*model.ColumnInfo) *PhysicalIndexScan {
|
|
tblInfo := p.Table.TableInfo
|
|
is := &PhysicalIndexScan{
|
|
Index: tblInfo.Indices[idxOffset],
|
|
Table: tblInfo,
|
|
Columns: cols,
|
|
TableAsName: &p.Table.Name,
|
|
OutOfOrder: false,
|
|
DBName: p.Table.DBInfo.Name,
|
|
physicalTableSource: physicalTableSource{client: p.ctx.GetClient()},
|
|
DoubleRead: false,
|
|
}
|
|
is.tp = Aly
|
|
is.allocator = p.allocator
|
|
is.initIDAndContext(p.ctx)
|
|
is.SetSchema(p.Schema())
|
|
is.readOnly = true
|
|
rb := rangeBuilder{sc: p.ctx.GetSessionVars().StmtCtx}
|
|
is.Ranges = rb.buildIndexRanges(fullRange, types.NewFieldType(mysql.TypeNull))
|
|
return is
|
|
}
|
|
|
|
// convert2PhysicalPlan implements the LogicalPlan convert2PhysicalPlan interface.
|
|
func (p *Analyze) convert2PhysicalPlan(prop *requiredProperty) (*physicalPlanInfo, error) {
|
|
info, err := p.getPlanInfo(prop)
|
|
if err != nil {
|
|
return nil, errors.Trace(err)
|
|
}
|
|
if info != nil {
|
|
return info, nil
|
|
}
|
|
var childInfos []*physicalPlanInfo
|
|
for _, idx := range p.IdxOffsets {
|
|
var columns []*model.ColumnInfo
|
|
tblInfo := p.Table.TableInfo
|
|
for _, idxCol := range tblInfo.Indices[idx].Columns {
|
|
for _, col := range tblInfo.Columns {
|
|
if col.Name.L == idxCol.Name.L {
|
|
columns = append(columns, col)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
is := p.prepareSimpleIndexScan(idx, columns)
|
|
childInfos = append(childInfos, is.matchProperty(prop, &physicalPlanInfo{count: 0}))
|
|
}
|
|
// TODO: It's inefficient to do table scan two times for massive data.
|
|
if p.PkOffset != -1 {
|
|
col := p.Table.TableInfo.Columns[p.PkOffset]
|
|
ts := p.prepareSimpleTableScan([]*model.ColumnInfo{col})
|
|
childInfos = append(childInfos, ts.matchProperty(prop, &physicalPlanInfo{count: 0}))
|
|
}
|
|
if p.ColOffsets != nil {
|
|
cols := make([]*model.ColumnInfo, 0, len(p.ColOffsets))
|
|
for _, offset := range p.ColOffsets {
|
|
cols = append(cols, p.Table.TableInfo.Columns[offset])
|
|
}
|
|
ts := p.prepareSimpleTableScan(cols)
|
|
childInfos = append(childInfos, ts.matchProperty(prop, &physicalPlanInfo{count: 0}))
|
|
}
|
|
for _, child := range p.Children() {
|
|
childInfo, err := child.(LogicalPlan).convert2PhysicalPlan(&requiredProperty{})
|
|
if err != nil {
|
|
return nil, errors.Trace(err)
|
|
}
|
|
childInfos = append(childInfos, childInfo)
|
|
}
|
|
info = p.matchProperty(prop, childInfos...)
|
|
p.storePlanInfo(prop, info)
|
|
return info, nil
|
|
}
|
|
|
|
// addCachePlan will add a Cache plan above the plan whose father's IsCorrelated() is true but its own IsCorrelated() is false.
|
|
func addCachePlan(p PhysicalPlan, allocator *idAllocator) []*expression.CorrelatedColumn {
|
|
if len(p.Children()) == 0 {
|
|
return nil
|
|
}
|
|
selfCorCols := p.extractCorrelatedCols()
|
|
newChildren := make([]Plan, 0, len(p.Children()))
|
|
for _, child := range p.Children() {
|
|
childCorCols := addCachePlan(child.(PhysicalPlan), allocator)
|
|
if len(selfCorCols) > 0 && len(childCorCols) == 0 {
|
|
newChild := &Cache{}
|
|
newChild.tp = "Cache"
|
|
newChild.allocator = allocator
|
|
newChild.initIDAndContext(p.context())
|
|
newChild.SetSchema(child.Schema())
|
|
|
|
addChild(newChild, child)
|
|
newChild.SetParents(p)
|
|
|
|
newChildren = append(newChildren, newChild)
|
|
} else {
|
|
newChildren = append(newChildren, child)
|
|
}
|
|
}
|
|
p.SetChildren(newChildren...)
|
|
return selfCorCols
|
|
}
|