这是我参与「第四届青训营 」笔记创作活动的第21天
这次继续记录一下关于项目中SQL解析与验证部分的学习笔记,这是第二部分
- Visitor
Visitor是遍历AST的手段,是处理AST最方便的模式,Visitor是一个接口,有缺省什么都没做的实现VistorAdapter。
Druid内置提供了如下Visitor:
OutputVisitor用来把AST输出为字符串 WallVisitor 来分析SQL语意来防御SQL注入攻击 ParameterizedOutputVisitor用来合并未参数化的SQL进行统计 EvalVisitor 用来对SQL表达式求值 ExportParameterVisitor用来提取SQL中的变量参数 SchemaStatVisitor 用来统计SQL中使用的表、字段、过滤条件、排序表达式、分组表达式 SQL格式化 Druid内置了基于语义的SQL格式化功能
- SQL Compile
SQL Compiler 处理层完成 SQL 语意检查(Preprocess)、编译执行计划(Logical Optimize、Physical Optimize) 工作,将 SQL 编译成可执行的物理执行计划。
1、 首先,从 MySQL Protocol Layer 串联起 “解析”、“执行” 操作,并在 func (s *session) ExecuteStmt(…) 中调用 func (c *Compiler) Compile(…) 进行真正的编译处理。
2、其次,在 Compile 内部调用 func Preprocess(…),进行 Preprocess 完成前置检查,如:语义检查。具体实现流程为,通过 AST 的 Accept 方法 方法, 构造一个 Vistor 实现对 AST 的遍历。 每个 Visitor 接口包含 Enter、Leave 方法,并在 Enter 或 Leave 时,依据 SQL 类型进行判断。本例 point get 会跳到 func (n *SelectStmt) Accept(v Visitor) 中,不断分支处理完成遍历。 3、最后,进入 Optimizer 处理,本例中因为是点查会越过大量优化器处理过程,直接进入 func TryFastPlan(…) 进行简单的 “权限检查” 及 “数据库名检查”。
// TryFastPlan tries to use the PointGetPlan for the query.
func TryFastPlan(ctx sessionctx.Context, node ast.Node) (p Plan) {
......
case *ast.SelectStmt:
if fp := tryPointGetPlan(ctx, x, isForUpdateReadSelectLock(x.LockInfo)); fp != nil {
if checkFastPlanPrivilege(ctx, fp.dbName, fp.TblInfo.Name.L, mysql.SelectPriv) != nil {
return nil
}
if tidbutil.IsMemDB(fp.dbName) {
return nil
}
if fp.IsTableDual {
return
}
p = fp
return
}
}
return nil
}
优化部分
此模块负责进行合法性检查及名字绑定、由AST生成逻辑执行计划,并基于一系列优化规则进行优化,生成物理执行计划并返回。
模块的入口为:
// executor/compiler.go
// 将AST结点转化为物理执行计划
func (c *Compiler) Compile(ctx context.Context, stmtNode ast.StmtNode) (*ExecStmt, error) {
// 进行合法性检查及名字绑定
infoSchema := infoschema.GetInfoSchema(c.Ctx)
if err := plannercore.Preprocess(c.Ctx, stmtNode, infoSchema); err != nil {
return nil, err
}
// 优化,生成物理执行计划
finalPlan, names, err := planner.Optimize(ctx, c.Ctx, stmtNode, infoSchema)
if err != nil {
return nil, err
}
return &ExecStmt{
InfoSchema: infoSchema,
Plan: finalPlan,
Text: stmtNode.Text(),
StmtNode: stmtNode,
Ctx: c.Ctx,
OutputNames: names,
}, nil
}
在Optimize函数中,首先由AST构造逻辑执行计划:
// planner/optimize.go
builder := plannercore.NewPlaBuilder(sctx, is)
p, err := builder.Build(ctx, node)
if err != nil {
return nil, nil, err
}
logic, isLogicalPlan := p.(plannercore.LogicalPlan)
再进行优化,生成最终执行计划:
// planner/optimize.go
finalPlan, err := plannercore.DoOptimize(ctx, builder.GetOptFlag(), logic)
在DoOptimize函数中,会进行逻辑优化:
// planner/core/optimizer.go
logic, err := logicalOptimize(ctx, flag, logic)
if err != nil {
return nil, err
}
// planner/core/optimizer.go
// flag为掩码,代表需要应用哪些优化规则
func logicalOptimize(ctx context.Context, flag uint64, logic LogicalPlan) (LogicalPlan, error) {
var err error
for i, rule := range optRuleList {
if flag&(1<<uint(i)) == 0 {
continue
}
// 遍历优化规则,调用rule.optimize进行优化
logic, err = rule.optimize(ctx, logic)
if err != nil {
return nil, err
}
}
return logic, err
}
其中,optRuleList为优化规则列表:
// planner/core/optimizer.go
var optRuleList = []logicalOptRule{
&columnPruner{},
&buildKeySolver{},
&aggregationEliminator{},
&projectionEliminator{},
&maxMinEliminator{},
&ppdSolver{},
&outerJoinEliminator{},
&aggregationPushDownSolver{},
&pushDownTopNOptimizer{},
&joinReOrderSolver{},
}
类型logicalOptRule为优化规则:
// planner/core/optimizer.go
type logicalOptRule interface {
optimize(context.Context, LogicalPlan) (LogicalPlan, error)
name() string
}
列表中的每种规则均有相应的optimize方法实现,位于planner/core/rule*中。
列裁剪
列裁剪算法位于planner/core/rule_column_prunning.go中。逻辑执行计划LogicalPlan接口包含列裁剪PruneColumns方法,而每种逻辑执行计划结点均实现了LogicalPlan接口。planner/core/rule_column_prunning.go则包含了每种逻辑执行计划结点对应的列裁剪PruneColumns方法实现。
列裁剪目的为裁剪掉不需要读取的列,以节约IO资源;算法实现为自顶向下遍历逻辑执行计划树,并调用每个结点所实现的PruneColumns方法:某个结点需要用到的列,等于它自己需要用到的列,加上父节点需要用到的列:
// lp为逻辑执行计划树的根结点
func (s *columnPruner) optimize(ctx context.Context, lp LogicalPlan) (LogicalPlan, error) {
err := lp.PruneColumns(lp.Schema().Columns)
return lp, err
}
例如对于Select算子,PruneColumns方法实现为:
func (p *LogicalSelection) PruneColumns(parentUsedCols []*expression.Column) error {
child := p.children[0]
// 父节点用到的列 <= 父节点用到的列 + 当前结点用到的列
parentUsedCols = expression.ExtractColumnsFromExpressions(parentUsedCols, p.Conditions, nil )
// 调用子节点的PruneColumns方法,传入父节点用到的列
return child.PruneColumns(parentUsedCols)
}
Predicate及Limit下推
planner/core/rule_predicate_push_down中实现了将Predicate下推到Project与Join算子下面。
谓词下推目的为将能下推的条件尽量下推,使得提前过滤更多的记录,减小参与Join等算子的数据量。
谓词下推接口函数为:
func (p *baseLogicalPlan) PredicatePushDown(predicates []expression.Expression) ([]expression.Expression, LogicalPlan)
其处理当前的执行计划p,参数predicates表示要添加的过滤条件;函数返回值为无法下推的条件以及新生成的执行计划。
例如,对于Join算子的谓词下推,首先会尽可能将左外连接和右外连接简化为内连接;再收集所有过滤条件,区分哪些是 Join 的等值条件,哪些是 Join 需要用到的条件,哪些全部来自于左子节点,哪些全部来自于右子节点;区分之后,对于内连接,可以把左条件和右条件分别向左右子节点下推。等值条件和其它条件保留在当前的 Join 算子中,剩下的返回。
case 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)
dual := Conds2TableDual(p, tempCond)
if dual != nil {
return ret, dual
}
equalCond, leftPushCond, rightPushCond, otherCond = p.extractOnCondition(tempCond, true, true)
// 把左条件和右条件分别向左右子节点下推
p.LeftConditions = nil
p.RightConditions = nil
// 等值条件和其它条件保留在当前的 Join 算子中
p.EqualConditions = equalCond
p.OtherConditions = otherCond
leftCond = leftPushCond
rightCond = rightPushCond
}
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)
addSelection(p, lCh, leftRet, 0)
addSelection(p, rCh, rightRet, 1)
p.updateEQCond()
for _, eqCond := range p.EqualConditions {
p.LeftJoinKeys = append(p.LeftJoinKeys, eqCond.GetArgs()[0].(*expression.Column))
p.RightJoinKeys = append(p.RightJoinKeys, eqCond.GetArgs()[1].(*expression.Column))
}
p.mergeSchema()
buildKeyInfo(p)
return ret, p.self
谓词下推算法的执行流程与列裁剪类似:自顶向下遍历执行计划树,在当前结点的PredicatePushDown方法中处理谓词下推并调用子节点的PredicatePushDown方法。
在planner/core/rule_topn_push_down中,还实现了将Limit下推到Project与Join算子下面。例如:
func (p *LogicalProjection) pushDownTopN(topN *LogicalTopN) LogicalPlan {
for _, expr := range p.Exprs {
if expression.HasAssignSetVarFunc(expr) {
return p.baseLogicalPlan.pushDownTopN(topN)
}
}
if topN != nil {
for _, by := range topN.ByItems {
by.Expr = expression.ColumnSubstitute(by.Expr, p.schema, p.Exprs)
}
// 删除无意义的常量排序项
for i := len(topN.ByItems) - 1; i >= 0; i-- {
switch topN.ByItems[i].Expr.(type) {
case *expression.Constant:
topN.ByItems = append(topN.ByItems[:i], topN.ByItems[i+1:]...)
}
}
}
p.children[0] = p.children[0].pushDownTopN(topN)
return p
}
支持的算子
- UnionScan
type PhysicalUnionScan struct {
basePhysicalPlan
Conditions []expression.Expression
HandleCol *expression.Column
}
通过下推 API ,把一部分简单的 SQL 层的执行逻辑下推到 KV 层执行,减少 RPC 的次数和数据传输量,从而提升性能
为了解决对脏数据的读取,SQL 层实现了 UnionStore 的结构,UnionStore 对 SQL 层的 Buffer 和 KV 层接口做了一个封装,事务对 KV 层的读写都经过 UnionStore 。
UnionStore 收到请求时会先在 Buffer 里寻找,找不到时才会调用 KV 层的接口
当需要遍历数据的时候 UnionStore 会创建 Buffer 和 KV 的迭代器,并合并成一个
type UnionStore interface {
MemBuffer
// GetKeyExistErrInfo gets the key exist error info for the lazy check.
GetKeyExistErrInfo(k Key) *existErrInfo
// DeleteKeyExistErrInfo deletes the key exist error info for the lazy check.
DeleteKeyExistErrInfo(k Key)
// WalkBuffer iterates all buffered kv pairs.
WalkBuffer(f func(k Key, v []byte) error) error
// SetOption sets an option with a value, when val is nil, uses the default value of this option.
SetOption(opt Option, val interface{})
// DelOption deletes an option.
DelOption(opt Option)
// GetOption gets an option.
GetOption(opt Option) interface{}
// GetMemBuffer return the MemBuffer binding to this UnionStore.
GetMemBuffer() MemBuffer
}
为了解决 SQL 层脏数据的可见性问题,定义了Union Scan 算法以 Row 为单位,创建一个 DirtyTable 保存事务的修改
addedRows 保存新写入的 row, deleteRows 保存删除的 row
- 对于
INSERT,我们需要把 row 添加到 addedRows 里。 - 对于
DELETE,我们需要把 row 从 addedRows 里删掉,然后把 row 添加到 deleteRows 里。 - 对于
UPDATE,相当于先执行DELETE, 再执行INSERT。
对于每一条下推 API 得到的结果集里的 Row,在 deleteRows 里查找,如果有,那么代表这一条结果已经被删掉,那么把它从结果集里删掉,得到过滤后的结果集。
把 addedRows 里的所有 Row,放到一个 slice 里,并对这个 slice 用快照结果集相同的顺序排序,生成脏数据结果集。
返回结果的时候,将过滤后的快照结果集与脏数据结果集进行 Merge。
type DirtyTable struct {bl
tid int64
addedRows map[int64]struct{}
deletedRows map[int64]struct{}
}