前言
原文链接:TiDB 源码阅读系列文章(四)Insert 语句概览
前几篇我做的笔记:【笔记】《TiDB 源码阅读系列》1-3 SQL 框架
原文里有些地方和现在的 TiDB 略有差异,我会在笔记中指出。
(四) Insert 语句概览
本文重点介绍语句在执行框架下的具体执行逻辑。
语句
连接 TiDB
mysql -h 127.0.0.1 -P 4000 -u root
建立表
CREATE TABLE t (
id VARCHAR(31),
name VARCHAR(50),
age int,
key id_idx (id)
);
插入语句
INSERT INTO t VALUES ("pingcap001", "pingcap", 3);
处理流程简述:
- 通过协议层、Parser、Plan、Executor 后变成可执行的结构。
- 通过 Next() 来驱动语句真正执行。
语法解析
Insert 语句会被解析成一个 InsertStmt 结构体,它定义在:
parser/ast/dml.go
1345: // InsertStmt 是一个向已存在的表中插入新行的语句
type InsertStmt struct {
dmlNode
IsReplace bool
IgnoreErr bool
Table *TableRefsClause
Columns []*ColumnName
Lists [][]ExprNode
Setlist []*Assignment
Priority mysql.PriorityEnum
OnDuplicate []*Assignment
Select ResultSetNode
}
这里只用到了 Table 和 Lists 字段,分别表示向哪个表插入以及插入哪些数据。
生成 AST 之后,需要对其进行一系列处理,我们先跳过一些公共的处理逻辑,如预处理、合法性验证、权限检查。
查询计划
接下来是将 AST 转成 Plan 结构,进入流程是:
executor/compile.go
func (c *Compiler) Compile(ctx context.Context, stmtNode ast.StmtNode) (*ExecStmt, error) {
61: finalPlan, names, err := planner.Optimize(ctx, c.Ctx, stmtNode, infoSchema)
planner/optimize.go
func Optimize(ctx context.Context, sctx sessionctx.Context, node ast.Node, is infoschema.InfoSchema) (plannercore.Plan, types.NameSlice, error) {
60: bestPlan, names, _, err := optimize(ctx, sctx, node, is)
func optimize(ctx context.Context, sctx sessionctx.Context, node ast.Node, is infoschema.InfoSchema) (plannercore.Plan, types.NameSlice, float64, error) {
133: p, err := builder.Build(ctx, node)
planner/core/planbuilder.go
/ Build 将抽象语法树构建为 Plan
func (b *PlanBuilder) Build(ctx context.Context, node ast.Node) (Plan, error) {
377: switch x := node.(type) {
393: return b.buildInsert(ctx, x)
主要涉及两个部分:
- 补全 Schema 信息,包括数据库/表/列信息。(?)
- 根据 Insert 的类型(values, set, select)分发到不同函数中。对于 insert values,会处理 Lists 中所有的Value,将 ast.ExprNode 转换为 expression.Expression,也就是纳入表达式框架,后面会在这个框架下求值。
planner/core/planbuilder.go
func (b *PlanBuilder) buildInsert(ctx context.Context, insert *ast.InsertStmt) (Plan, error) {
1878: err := b.buildSetValuesOfInsert(ctx, insert, insertPlan, mockTablePlan, checkRefColumn)
1884: err := b.buildValuesListOfInsert(ctx, insert, insertPlan, mockTablePlan, checkRefColumn)
1890: err := b.buildSelectPlanOfInsert(ctx, insert, insertPlan)
func (b *PlanBuilder) buildValuesListOfInsert(..., insertPlan *Insert, ...) error {
2070: for i, valuesItem := range insert.Lists {
2079: exprList := make([]expression.Expression, 0, len(valuesItem))
2080: for j, valueItem := range valuesItem {
2081: var expr expression.Expression
2123: exprList = append(exprList, expr)
2125: insertPlan.Lists = append(insertPlan.Lists, exprList)
已经生成的 insertPlan 是 plannercore.Insert 类型,并且实现了 Plan 接口。
注意,Insert 并不被认为是逻辑计划(没有需要优化的地方),所以会在164行直接返回,不进行后面的操作。
而对于 Select 这种语句(下几篇中提到),会经过 DoOptimize 函数将逻辑计划优化并转换为物理计划:
planner/optimize.go
func optimize(ctx context.Context, sctx sessionctx.Context, node ast.Node, is infoschema.InfoSchema) (plannercore.Plan, types.NameSlice, float64, error) {
163: if !isLogicalPlan {
164: return p, names, 0, nil
172: finalPlan, cost, err := plannercore.DoOptimize(ctx, builder.GetOptFlag(), logic)
173: return finalPlan, names, cost, err
planner/core/optimizer.go
118: func DoOptimize(ctx context.Context, flag uint64, logic LogicalPlan) (PhysicalPlan, float64, error) {
执行
经过一系列复杂调用, plannercore.Insert 在这里被转换成了 executor.InsertExec 结构:
executer/builder.go
func (b *executorBuilder) buildInsert(v *plannercore.Insert) Executor {
728: insert := &InsertExec{
729: InsertValues: ivs,
730: OnDuplicate: append(v.OnDuplicate, v.GenCols.OnDuplicates...),
731: }
732: return insert
后续的执行都由这个结构进行,主要流程在 insert.go 中
executer/insert.go, insert_common.go
func (e *InsertExec) Next(ctx context.Context, req *chunk.Chunk) error {
256: return insertRows(ctx, e)
// insert_common.go 对插入数据的每行进行表达式求值。
func insertRows(ctx context.Context, base insertCommon) (err error) {
236: if err = base.exec(ctx, rows); err != nil {
func (e *InsertExec) exec(ctx context.Context, rows [][]types.Datum) error {
84: for _, row := range rows {
85: if _, err := e.addRecord(ctx, row); err != nil {
// insert_common.go 将一行数据写入存储引擎中
func (e *InsertValues) addRecord(ctx context.Context, row []types.Datum) (int64, error) {
974: h, err := e.Table.AddRecord(e.ctx, row, table.WithCtx(ctx))
table/tables/tables.go
454: func (t *TableCommon) AddRecord(ctx sessionctx.Context, r []types.Datum, opts ...table.AddRecordOption) (recordID int64, err error) {
最后在这个函数中构造索引、行,写入事务缓存,就不单独列出了。
小结
Insert 语句的执行流程如下:
- server/conn.go 监听端口,得到语句后进行处理,传给 session 包。
- session/session.go 的 execute 函数包含了语句解析执行的三个核心流程。
- 调用 parse 包中的内容,将语句解析成抽象语法树,即 ast.InsertStmt 类型。
- 调用 execute/complie.go,转发给 planner 包处理,传回一个 Plan 接口类型,实际是 plannercore.Insert 类型。
- 调用 executor 包,在 executor/builder.go 中 Plan 被转换为了 executor.InsertExec 类型,即执行器。
- 使用 Next 函数来执行语句,INSERT 语句在返回前就会被执行,通过 table 包执行结果并写入到事务缓存中。
- 将 resultSet 返回给 server/conn.go(SELECT语句会在此时调用 Next 执行)。