stmt如何获取完整sql_TiDB源码阅读笔记(四) SQL 的必经之路

0a1fc399db179c9d2f6c67933f3467a2.png

好像 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

49655362990341eb8cb6c734fe88e818.png
select 4*fdields from stats_meta where version order by version

选择了一个 TiDB 自动发送的 SQL 例子来看,其实这些东西在 debug 的时候很讨厌了。可以看到,result 中现在是解析后的 result ,是条 DML(dmlNode)语句,需要返回 resultSet(resultNode),From 和 Where 是必须的,Field 中是查询的 4 个字段,最后还有 orderbyNode,这也是这个语句的所有 token 了,都解析清楚了。

61b8f7b1a70cc63e5fb764bb14788418.png
四个字段
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)
}

cca2813e0a02635a2abd5e396f8d4bb6.png
获取特定的系统变量

进入 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 ,可能会清晰一点。

860a85c7c34d6d78e2088a21302bc605.png
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值