我的博客地址:https://www.luozhiyun.com/archives/605
这篇文章我们看一下 TiDB 是插入数据是如何封装的,索引是如何维护的,如果插入的数据发生了冲突会如何处理,类似INSERT IGNORE 与 INSERT ON DUPLICATE KEY UPDATE插入语句是如何处理。
下面我们先构造一个表结构:
CREATE TABLE test_insert (a int primary key, b int, c int,d int,index b_index(b),unique index c_index(c) );这个表结构中有一个主键、普通索引、唯一索引。
普通 Insert 构建执行计划普通插入 SQL 考虑的是类似下面这样的语句:
INSERT INTO test.test_insert (a, b, c) VALUES (1, 1, 1);首先会和 select 语法一样先进行语法解析构建 ast 语法树:
type InsertStmt struct { dmlNode // sql 中的表信息 Table *TableRefsClause // 字段信息 Columns []*ColumnName // 要插入的数据 Lists [][]ExprNode ... }我这里展示的是几个比较重要的字段,因为在插入数据的时候可以使用 :INSERT INTO t VALUES(),(),()... 这样的语法,所以要插入的数据是一个切片:Lists。
然后制定查询计划,在制定查询计划的时候同样会走到 PlanBuilder 的 Build 方法中,然后根据 ast 语法树的类型 进入到 buildInsert 分支中:
func (b *PlanBuilder) Build(ctx context.Context, node ast.Node) (Plan, error) { b.optFlag |= flagPrunColumns switch x := node.(type) { case *ast.InsertStmt: return b.buildInsert(ctx, x) ... } func (b *PlanBuilder) buildInsert(ctx context.Context, insert *ast.InsertStmt) (Plan, error) { // 获取ast树中表节点 ts, ok := insert.Table.TableRefs.Left.(*ast.TableSource) if !ok { return nil, infoschema.ErrTableNotExists.GenWithStackByArgs() } // 获取表的相关信息 // 包含了表信息,库信息,分区信息等 tn, ok := ts.Source.(*ast.TableName) if !ok { return nil, infoschema.ErrTableNotExists.GenWithStackByArgs() } // 获取其中表信息 tableInfo := tn.TableInfo ... // Build Schema with DBName otherwise ColumnRef with DBName cannot match any Column in Schema. // schema包含表的字段信息,主键字段等,names是表的字段信息切片 schema, names, err := expression.TableInfo2SchemaAndNames(b.ctx, tn.Schema, tableInfo) if err != nil { return nil, err } // 根据表的id从缓存中获取表的元数据 // 这里包含的信息比较多,有表名、字段信息、隐藏字段、所有索引、表的字符集编码等 tableInPlan, ok := b.is.TableByID(tableInfo.ID) if !ok { return nil, errors.Errorf("Can't get table %s.", tableInfo.Name.O) } // 构建插入执行计划 insertPlan := Insert{ Table: tableInPlan, Columns: insert.Columns, tableSchema: schema, tableColNames: names, IsReplace: insert.IsReplace, }.Init(b.ctx) ... // 根据不同的语法执行不同的分支 // Branch for `INSERT ... SET ...`. if len(insert.Setlist) > 0 { // Branch for `INSERT ... VALUES ...`. } else if len(insert.Lists) > 0 { // 根据ast语法树中的= ast.ExprNode 转换成执行计划的 expression.Expression err := b.buildValuesListOfInsert(ctx, insert, insertPlan, mockTablePlan, checkRefColumn) if err != nil { return nil, err } // Branch for `INSERT ... SELECT ...`. } else { } ... return insertPlan, err }buildInsert 这个方法主要涉及两个部分:
补全表相关的元数据信息,包括 Database/Table/Column/Index 信息;
处理 ast 语法树中要插入的 Lists 中的数据,将 ast.ExprNode 转换成 expression.Expression。
然后将构建好的 Insert 执行计划返回。
需要注意的是,由于 Insert 语句比较简单,没什么优化的空间,所以不会走 DoOptimize 进行物理优化:
finalPlan, cost, err := plannercore.DoOptimize(ctx, sctx, builder.GetOptFlag(), logic) 执行 Insert 计划 func (a *ExecStmt) Exec(ctx context.Context) (_ sqlexec.RecordSet, err error) { ... // 生成执行器 e, err := a.buildExecutor() if err != nil { return nil, err } // ExecuteExec will rewrite `a.Plan`, so set plan label should be executed after `a.buildExecutor`. ctx = a.setPlanLabelForTopSQL(ctx) // handleNoDelay负责执行像 Insert 这种不需要返回数据的语句,只需要把语句执行完成即可 if handled, result, err := a.handleNoDelay(ctx, e, isPessimistic); handled { return result, err } ... return &recordSet{ executor: e, stmt: a, txnStartTS: txnStartTS, }, nil }这里根据执行计划生成执行器的过程和 Select 是一致的,我们简单看一下。buildExecutor 方法最后会将执行计划转化成 InsertExec 结构体,后续的执行都由这个结构进行。
![Frame 2](https://img.luozhiyun.com/Frame 2.png)
在生成完执行计划之后会进入到 handleNoDelay 执行 SQL 语句。后面的执行流程比较长,我们省略一些中间环节:
![Frame 3](https://img.luozhiyun.com/Frame 3-3229191.png)
insertRows 会主要做的就是根据字段类型,获取数据之后做数据填充。
func insertRows(ctx context.Context, base insertCommon) (err error) { // 获取 InsertValues 实例 e := base.insertCommon() ... // 设置填充函数 evalRowFunc := e.fastEvalRow // 如果要插入的数据不是常量,那么会使用evalRow函数 if !e.allAssignmentsAreConstant { evalRowFunc = e.evalRow } rows := make([][]types.Datum, 0, len(e.Lists)) for i, list := range e.Lists { e.rowCount++ var row []types.Datum row, err = evalRowFunc(ctx, list, i) if err != nil { return err } ... } // 批量设置自增id rows, err = e.lazyAdjustAutoIncrementDatum(ctx, rows) if err != nil { return err } // 将数据写入存储引擎中 err = base.exec(ctx, rows) if err != nil { return err } return nil }