好像 2020.8 session.execute 的代码做了一些调整,最好先 update 一下再看。
Ps 别嫌文章乱,文末有彩蛋。
TiDB源码阅读笔记(一) TiDB的入口 的最后,提到了 execute SQL 的入口,因为绝大部分 SQL 在 TiDB 中走的都是 COM_QUERY ,所以基本走的都是 handleQuery ,
func (cc *clientConn) handleQuery(ctx context.Context, sql string) (err error) {
stmts, err := cc.ctx.Execute(ctx, sql)
}
当然 execute 就是几乎所有 SQL 的必经之路。
今天我们继续这段旅途,看看 TiDB 是如何执行一条 SQL 的。 TiDB源码阅读笔记(二) 简单理解 Lex & Yacc 中我们简单理解了一下 Lex 和 Yacc (感觉写的稀碎,没有好的顺序 )。这里完成了 SQL 语句的解析,将 Token 进行处理形成ast 语法树,之后做了什么呢,起始的位置就是在这里。
看返回值也知道,这是整个 SQL 执行的最核心部分,返回的是 RecordSet 结果集。我们一起看下整个方法,简化一下
func (s *session) Execute(ctx , sql) (recordSets){
if span := opentracing.SpanFromContext(ctx); span != nil && span.Tracer() != nil {
}
parseStartTime := time.Now()
stmtNodes, warns, err := s.ParseSQL(ctx, sql, charsetInfo, collation)
if err != nil {
s.rollbackOnError(ctx)
}
rs, err := s.ExecuteStmt(ctx, stmtNodes[0])
return []sqlexec.RecordSet{rs}, err
}
这里大概做了这么几件事情,做 SQL Parse ,如果失败了就 rollback ,成功了就执行 ExecuteStmt 制定逻辑、物理优化,执行的结果返回给 client。我们分别去看下主要的东西 ParseSQL 和 ExecuteStmt。
ParseSQL
主要将语句的 Token 进行解析,生成对应的 ast 语法树
parser.charset = charset
parser.collation = collation
parser.src = sql
parser.result = parser.result[:0] // 可以看到,将 parse 结果放到了这里
parser.lexer.reset(sql)
l = &parser.lexer
yyParse(l, parser) // parse
选择了一个 TiDB 自动发送的 SQL 例子来看,其实这些东西在 debug 的时候很讨厌了。可以看到,result 中现在是解析后的 result ,是条 DML(dmlNode)语句,需要返回 resultSet(resultNode),From 和 Where 是必须的,Field 中是查询的 4 个字段,最后还有 orderbyNode,这也是这个语句的所有 token 了,都解析清楚了。
for _, stmt := range parser.result { //遍历result
ast.SetFlag(stmt) //设置标示,标示是否含有引用、聚合函数..
}
return parser.result, warns, nil // 返回 result,解析完成
----------------------------------------------------------------------------------
if err != nil {// 如果解析失败,就 rollback
s.rollbackOnError(ctx)
}
之后将解析好的 stmtNodes 交给 ExecuteStmt 处理
stmtNodes, warns, err := s.ParseSQL(ctx, sql, charsetInfo, collation)
rs, err := s.ExecuteStmt(ctx, stmtNodes[0])
ExecuteStmt
func (s *session) ExecuteStmt{
----------------------------------------------------------------------------------
s.PrepareTxnCtx(ctx)//是否开启事务,进去看下
// 根据 autocommit 判断,还有乐观或悲观锁
func (s *session) PrepareTxnCtx(ctx context.Context) {
if !s.sessionVars.IsAutocommit() {
pessTxnConf := config.GetGlobalConfig().PessimisticTxn
if pessTxnConf.Enable {
s.sessionVars.TxnCtx.IsPessimistic = true
}
}
}
----------------------------------------------------------------------------------
err := s.loadCommonGlobalVariablesIfNeeded() // 读取全局变量
compiler := executor.Compiler{Ctx: s}
stmt, err := compiler.Compile(ctx, stmtNode) // 检查、优化
if err != nil {
s.rollbackOnError(ctx) // Compiler 报错 rollback
}
s.currentPlan = stmt.Plan
recordSet, err := runStmt(ctx, s, stmt)
return recordSet, nil
}
executeStmt 主要做了两件事,第一是 compiler ,做合法性校验,制定逻辑(基于规则)、物理(基于代价)优化,还是挨个看看
compile
func (c *Compiler) Compile{
if err := plannercore.Preprocess(c.Ctx, stmtNode, infoSchema);
finalPlan, names, err := planner.Optimize(ctx, c.Ctx, stmtNode, infoSchema)
return &ExecStmt{
GoCtx: ctx,
InfoSchema: infoSchema,
Plan: finalPlan,
LowerPriority: lowerPriority,
Text: stmtNode.Text(),
StmtNode: stmtNode,
Ctx: c.Ctx,
OutputNames: names,
}, nil
}
(1)Preprocess 主要完成了合法性等校验,具体可看下/tidb/planner/core/preprocess.go
(2)Optimize 逻辑、物理优化
func Optimize{
tableHints := hint.ExtractTableHintsFromStmtNode(node, sctx)
stmtHints, warns := handleStmtHints(tableHints)
}
进入 finalPlan, names, err := planner.Optimize(ctx, c.Ctx, stmtNode, infoSchema)
hintProcessor := &hint.BlockHintProcessor{Ctx: sctx}
node.Accept(hintProcessor)
node.Accept 是将之前的 result 处理成 ast.node,具体来说就是实现了 Accept 接口,比如看看 selectstmt 的 Accept 实现,首先判断有没有 hints,如果有,也要做处理,之后就是 selectstmt 的 token(略了大部分),可以看到,每一个都调用了 Accept ,最终形成 ast
n = newNode.(*SelectStmt)
if n.TableHints != nil && len(n.TableHints) != 0 {
newHints := make([]*TableOptimizerHint, len(n.TableHints))
for i, hint := range n.TableHints {
node, ok := hint.Accept(v)
n.TableHints = newHints
}
if n.From != nil {
node, ok := n.From.Accept(v)
n.From = node.(*TableRefsClause)
}
if n.Where != nil {
node, ok := n.Where.Accept(v)
n.Where = node.(ExprNode)
}
return v.Leave(n)
之后就要制作执行计划了,看看 Build
builder := plannercore.NewPlanBuilder(sctx, is, hintProcessor)
p, err := builder.Build(ctx, node)
----------------------------------------------------------------------------------
func (b *PlanBuilder) Build(ctx context.Context, node ast.Node) (Plan, error) {
b.optFlag |= flagPrunColumns
switch x := node.(type) {
case *ast.AdminStmt:
return b.buildAdmin(ctx, x)
case *ast.DeallocateStmt:
return &Deallocate{Name: x.Name}, nil
case *ast.DeleteStmt:
return b.buildDelete(ctx, x)
}
根据不同的 ast case 走不同的分支(略了大部分),renturn plan
finalPlan, cost, err := plannercore.DoOptimize(ctx, sctx, builder.GetOptFlag(), logic)
----------------------------------------------------------------------------------
func DoOptimize(ctx context.Context, sctx sessionctx.Context, flag uint64, logic LogicalPlan) (PhysicalPlan, float64, error) {
logic, err := logicalOptimize(ctx, flag, logic)// 逻辑优化
physical, cost, err := physicalOptimize(logic)// 物理优化
finalPlan := postOptimize(sctx, physical)
return finalPlan, cost, nil
}
----------------------------------------------------------------------------------
//看下逻辑优化,下面是目前 TiDB 的逻辑优化规则
var optRuleList = []logicalOptRule{
&gcSubstituter{},
&columnPruner{},
&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
}
这里进行了逻辑优化和物理优化,TiDB 目前的逻辑优化有如上的规则,可以阅读相关的文章或阅读代码来了解,今后我也会看一下,希望也能写一篇文章。简单来讲,当 一条 SQL 做逻辑优化的时候,会遍历这里的所有优化规则,如果合适就会采用,当然,优化出来的 plan 一定会比之前的要好才行。
在这之后就做好了一条 SQL 的解析、校验、优化等工作,制作好的 ExecStmt 会 return 给 recordSet, err := runStmt(ctx, s, stmt) 进行执行,我们看下
func runStmt(ctx context.Context, se *session, s sqlexec.Statement) (rs sqlexec.RecordSet, err error) {
err = se.checkTxnAborted(s)
rs, err = s.Exec(ctx)
if !s.IsReadOnly(sessVars) {
if err == nil && sessVars.TxnCtx.CouldRetry {
GetHistory(se).Add(s, sessVars.StmtCtx)
}
if se.txn.Valid() {
if err != nil {
se.StmtRollback()
} else {
err = se.StmtCommit(sessVars.StmtCtx.MemTracker)
}
}
}
if rs != nil {
return &execStmtResult{
RecordSet: rs,
sql: s,
se: se,
}, err
}
err = finishStmt(ctx, se, err, s)
s.(*executor.ExecStmt).FinishExecuteStmt(origTxnCtx.StartTS, err == nil, false)
return nil, err
}
这里主要做了这么几件事,首先是 se.checkTxnAborted ,面对一些未知不知道如何处理的 error ,TiDB目前也不是处理的很全面,所以比如面对 commit 失败的场景, TiDB 在这个函数统一处理,看下
func (s *session) checkTxnAborted(stmt sqlexec.Statement) error {
if _, ok := stmt.(*executor.ExecStmt).StmtNode.(*ast.CommitStmt); ok {
return nil
}
if _, ok := stmt.(*executor.ExecStmt).StmtNode.(*ast.RollbackStmt); ok {
return nil
}
return err
}
这里大概就是,如果一个事务“坏掉了”,就不再接受正常的 statement 了,只接受 commit 或 rollback 。
Exec 主要将 plan 转换为 executor
ctx := a.Ctx
stmtCtx := ctx.GetSessionVars().StmtCtx
if _, ok := a.Plan.(*plannercore.Execute); !ok {
useMaxTS, err := plannercore.IsPointGetWithPKOrUniqueKeyByAutoCommit(ctx, a.Plan)
if useMaxTS {
logutil.BgLogger().Debug("init txnStartTS with MaxUint64", zap.Uint64("conn", ctx.GetSessionVars().ConnectionID), zap.String("text", a.Text))
err = ctx.InitTxnWithStartTS(math.MaxUint64)
}
else if ctx.GetSessionVars().SnapshotTS != 0 {
if _, ok := a.Plan.(*plannercore.CheckTable); ok {
err = ctx.InitTxnWithStartTS(ctx.GetSessionVars().SnapshotTS)
}
}
b := newExecutorBuilder(ctx, a.InfoSchema) // 生成 ExecuteBuilder
----------------------------------------------------------------------------------
e := b.build(a.Plan) // 构造 ExecuteBuilder 看下
err := executorExec.Build(b)
switch v := p.(type) {
case *plannercore.Insert:
return b.buildInsert(v)
// 这里用 buildInsert 举例子看下
// 大致看下吧,感觉跟的太深写起来也很乱,这个有兴趣可以好好看下
func (b *executorBuilder) buildInsert(v *plannercore.Insert) Executor {
b.snapshotTS = b.ctx.GetSessionVars().TxnCtx.GetForUpdateTS()
selectExec := b.build(v.SelectPlan)
ivs := &InsertValues{
baseExecutor: baseExec,
Table: v.Table,
Columns: v.Columns,
Lists: v.Lists,
SetList: v.SetList,
GenExprs: v.GenCols.Exprs,
allAssignmentsAreConstant: v.AllAssignmentsAreConstant,
hasRefCols: v.NeedFillDefaultValue,
SelectExec: selectExec,
}
err := ivs.initInsertColumns()// insert 有三种类型,选择进行 init
if v.IsReplace { // 是否是 replace insert
return b.buildReplace(ivs)
}
insert := &InsertExec{
InsertValues: ivs,
OnDuplicate: append(v.OnDuplicate, v.GenCols.OnDuplicates...),
}
return insert
}
----------------------------------------------------------------------------------
a.Plan = executorExec.plan
if executorExec.lowerPriority {
ctx.GetSessionVars().StmtCtx.Priority = kv.PriorityLow
}
e = executorExec.stmtExec
}
a.isSelectForUpdate = b.hasLock && (!stmtCtx.InDeleteStmt && !stmtCtx.InUpdateStmt)
return e, nil
这样 exec 就完成了,到最后,若有 error 就 rollback ,否则 commit ,返回 resultSet 就结束了这段旅程。
if rs != nil {
return &execStmtResult{
RecordSet: rs,
sql: s,
se: se,
}, err
}
好了,基本上 TiDB 的 SQL 都要进行这样的处理,毕竟只要你 debug ,系统自动发的 SQL 就会带你“飞”了。这个流程很长,这里也只不过是泛泛而谈,希望今后能继续深入了解。如果看完你觉得条理不清晰,很乱,可以看看下面我整理的 xMind ,可能会清晰一点。