查询优化器发现节点之间是 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 优化中作用比较小,比较复杂的逻辑其实没有涉及到,特别是在上面提到的几个查询语句中。