TiDB 源码学习:关于 Projection Pruning 的细节问题

查询优化器发现节点之间是 Proj/Aggr --> Proj 模式的时候(也就是某个 Proj 节点的祖先是 Proj或 Aggr 节点的时候),会考虑对子节点做 Projection Pruning 优化。

是否可以消除 Proj 节点的判断依据是:当前的 Proj 节点输出的列是否和其子节点的输出列一样。如果一样,则可以消除。

让我产生疑问的地方是判断输出列是否和子节点一样的代码逻辑,代码如下:

func canProjectionBeEliminatedLoose(p *LogicalProjection) bool {

for _, expr := range p.Exprs {
    _, ok := expr.(*expression.Column)
    if !ok {
        return false
    }
}
return true

}
代码其实很简单,就是判断每个 Expr 是否是 *expression.Column 类型。根据 LogicalProjection (下面简称 LP)数据结构定义:

type LogicalProjection struct {

logicalSchemaProducer

Exprs []expression.Expression

// ...

}
LP 和其他一些逻辑查询节点类型一样,继承了 logicalSchemaProducer ,logicalSchemaProducer 保存列信息(参考 Schema 数据结构)。除此以外,LogicalProjection 还包含了 Exprs 字段。

我的疑问就是 要知道 Column 也是 Expression 类型,LogicalProjection 中 Exprs 字段和 schema.Columns 字段保存的信息不重复吗?二者有什么区别呢?

先说结论
对于很多简单查询来说 Exprs 和 schema.Columns 其实没啥区别,可以简单认为它们的信息就是一样的,比如下面这些查询:

select a from t;
select count(a) from t;
但是,对于 fields 中带有函数计算的查询语句(比如:select a + b as c from t),Exprs 字段中对应的数据类型就不再是 expression.Column 类型了, a+b 列也不是 LP 的子节点产生的,这类 LP 是不能被消除的。所以上面的代码逻辑简洁又正确。

具体代码分析
回顾代码中 LP 节点的创建过程,LP 是在 buildProjection 函数中创建出来的:

func (b PlanBuilder) buildProjection(p LogicalPlan, fields []ast.SelectField, mapper map[*ast.AggregateFuncExpr]int, considerWindow bool) (LogicalPlan, int, error) {

//...
proj := LogicalProjection{Exprs: make([]expression.Expression, , len(fields))}.Init(b.ctx)
schema := expression.NewSchema(make([]*expression.Column, , len(fields))...)
oldLen := 
for i, field := range fields {
    //...
    newExpr, np, err := b.rewrite(field.Expr, p, mapper, true)
    //...

    p = np
    proj.Exprs = append(proj.Exprs, newExpr)

    col, err := b.buildProjectionField(proj.id, schema.Len()+1, field, newExpr)
    //...
    schema.Append(col)
}
proj.SetSchema(schema)
proj.SetChildren(p)
return proj, oldLen, nil

}
代码中:

调用 rewrite 函数将 field.Expr 转换成 newExpr
将 newExpr 加入到 Exprs 字段中
调用 buildProjectionField 方法,使用 field/newExpr 创建新的列信息,加入到 schema 中
rewrite 函数是一个关键点,rewrite 过程涉及到了一些语法树和查询计划树中的数据结构,这里不做详细介绍。接下来,我们通过几个常见查询场景来说明一下 rewrite 函数起到的作用。

场景一:简单的查询语句

select a, b from t;
a 和 b 两个 field (*ast.SelectField)中的 Expr 类型都是 ast.ColumnNameExpr,rewrite 在处理这种类型的 field 时,其实就是找到它们的列名,从子节点的 schema 中找到对应的列(expression.Column 类型)并返回。

这类 field 在 rewrite 之后产生的 newExpr 是 *expression.Column 类型,这类语句中 LP 的 Exprs 和 schema.Columns 可以简单认为是一样的,没差别。

场景二:带有 Aggregation 的查询语句

select count(a) from t;
这种场景看似更复杂一些,其实 count(a) 这个 field 的 Expr 字段类型和QQ号码卖号场景一其实一样,也是 ast.ColumnNameExpr。rewrite 的处理过程基本没区别。

这个场景产生的查询计划是 DataSource -> LogicalAggregation -> LogicalProjection,count(a) 这一列其实是 LogicalAggregation 子节点提供的,LP 拿来用就行了。所以这类场景下,Exprs 和 schema.Columns 也没啥区别。

场景三:

select a + b as c from t;
在 fields 中有函数计算时,显然 LP 的子节点(在这里是 DataSource)的 schema 中是不可能包含这一列的。从这个场景下,Exprs 和 schema.Columns 就有所区别了。a+b field 中的 Expr 字段类型是ast.BinaryOperationExpr ,rewrite 的具体处理流程:

将 ast.BinaryOperationExpr 的 left 子节点和 right 子节点记录下来
根据 ast.BinaryOperationExpr 的函数类型找到对应的 ScalarFunction 类型
根据 step 1 和 step 2 中的信息,将该 field 转换成 expression.ScalarFunction 并返回
因此,a+b field 产生的 newExpr 并不是 Column 类型而是 ScalarFunction。

关于 rewrite

rewrite 函数在构建逻辑查询计划时作用很大,逻辑也比较复杂,不过在 Projection Pruning 优化中作用比较小,比较复杂的逻辑其实没有涉及到,特别是在上面提到的几个查询语句中。

上一篇:OVS 总体架构、源码结构及数据流程全面解析


下一篇:Android WebKit HTML主资源加载过程