TiDB 源码阅读系列文章(四)Insert 语句概览

本文为 TiDB 源码阅读系列文章的第四篇。上一篇文章简单介绍了整体流程,无论什么语句,大体上是在这个框架下运行,DDL 语句也不例外。

本篇文章会以 Insert 语句为例进行讲解,帮助读者理解前一篇文章,下一篇文章会介绍 Select 语句的执行流程。这两条是最常用的读、写语句,其他的语句相信读者能触类旁通,可以自行研究或者是等待后续的文章。对于这两类语句,目前也只会针对核心流程进行说明,更复杂的 Join、Insert-Into-OnDuplicate-Update 等会等到后面的文章进行讲解。另外本文会重点介绍每个语句在执行框架下面的具体执行逻辑,请读者阅读前先了解 Insert 语句的行为。

表结构

这里先给一个表结构,下面介绍的 SQL 语句都是在这个表上的操作。

CREATE TABLE t {
id      VARCHAR(31),
name VARCHAR(50),
age    int,
key id_idx (id)
};

Insert 语句

INSERT INTO t VALUES ("pingcap001", "pingcap", 3); 以这条语句为例,解释 Insert 是如何运行的。

语句处理流程

首先大家回忆一下上一篇文章介绍的框架,一条 SQL 语句经过协议层、Parser、Plan、Executor 这样几个模块处理后,变成可执行的结构,再通过 Next() 来驱动语句的真正执行。对于框架,每类语句都差不多;对于每个核心步骤,每个语句会有自己的处理逻辑。

语法解析

先看 Parser,对于 Insert 语句的解析逻辑在这里,可以看到这条语句会被解析成下面这个结构

// InsertStmt is a statement to insert new rows into an existing table.
// See https://dev.mysql.com/doc/refman/5.7/en/insert.html
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 这两个字段,也就是向哪个表插入哪些数据。其中 Lists 是一个二维数组,数组中的每一行对应于一行数据,这个语句只包含一行数据。有了 AST 之后,需要对其进行一系列处理,预处理、合法性验证、权限检查这些暂时跳过(每个语句的处理逻辑都差不多),我们看一下针对 Insert 语句的处理逻辑。

查询计划

接下来是将 AST 转成 Plan 结构,这个操作是在 planBuilder.buildInsert() 中完成。对于这个简单的语句,主要涉及两个部分:

  • 补全 Schema 信息
包括 Database/Table/Column 信息,这个语句没有指定向那些列插入数据,所以会使用所有的列。
  • 处理 Lists 中的数据
[这里](https://github.com/pingcap/tidb/blob/source-code/plan/planbuilder.go#L821)会处理一遍所有的 Value,将 ast.ExprNode 转换成 expression.Expression,也就是纳入了我们的表达式框架,后面会在这个框架下求值。大多数情况下,这里的 Value 都是常量,也就是 expression.Constant。

如果 Insert 语句比较复杂,比如要插入的数据来自于一个 Select,或者是 OnDuplicateUpdate 这种情况,还会做更多的处理,这里暂时不再深入描述,读者可以执行看 buildInsert() 中其他的代码。

现在 ast.InsertStmt 已经被转换成为 plan.Insert 结构,对于 Insert 语句并没有什么可以优化的地方,plan.Insert 这个结构只实现了 Plan 这个接口,所以在下面这个判断中,不会走进 Optimize 流程:

    if logic, ok := p.(LogicalPlan); ok {
        return doOptimize(builder.optFlag, logic)
    }

其他比较简单的语句也不会进入 doOptimize,比如 Show 这种语句,下一篇文章会讲解 Select 语句,会涉及到 doOptimize 函数。

执行

拿到 plan.Insert 这个结构后,查询计划就算制定完成。最后我们看一下 Insert 是如何执行的。

首先 plan.Insert 在这里被转成 executor.InsertExec 结构,后续的执行都由这个结构进行。执行入口是 Next 方法,第一步是要对待插入数据的每行进行表达式求值,具体的可以看 getRows 这个函数,拿到数据后就进入最重要的逻辑— InsertExec.exec() 这个函数,这个函数有点长,不过只考虑我们文章中讲述得这条 SQL 的话,可以把代码简化成下面这段逻辑:

    for _, row := range rows {
            h, err := e.Table.AddRecord(e.ctx, row, false)
	}

接下来我们看一下 AddRecord 这个函数是如何将一行数据写入存储引擎中。要理解这段代码,需要了解一下 TiDB 是如何将 SQL 的数据映射为 Key-Value,可以先读一下我们之前写的一些文章,比如这一篇。这里假设读者已经了解了这一点背景知识,那么一定会知道这里需要将 Row 和 Index 的 Key-Value 构造出来的,写入存储引擎。

构造 Index 数据的代码在 addIndices() 函数中,会调用 index.Create() 这个方法:

构造 Index Key:
func (c *index) GenIndexKey(sc *stmtctx.StatementContext, indexedValues [](#)types.Datum, h int64, buf [](#)byte) (key [](#)byte, distinct bool, err error) {
......
    key = c.getIndexKeyBuf(buf, len(c.prefix)+len(indexedValues)*9+9)
    key = append(key, [](#)byte(c.prefix)...)
    key, err = codec.EncodeKey(sc, key, indexedValues...)
    if !distinct && err == nil {
        key, err = codec.EncodeKey(sc, key, types.NewDatum(h))
    }
构造 Index Value:
func (c *index) Create(ctx context.Context, rm kv.RetrieverMutator, indexedValues [](#)types.Datum, h int64) (int64, error) {
    if !distinct {
        // non-unique index doesn't need store value, write a '0' to reduce space
        err = rm.Set(key, [](#)byte'0')
        return 0, errors.Trace(err)
    }
......
    if skipCheck {
        err = rm.Set(key, encodeHandle(h))
        return 0, errors.Trace(err)
    }

构造 Row 数据的代码比较简单,就在 tables.AddRecord 函数中:

构造 Row Key: 
key := t.RecordKey(recordID)
构造 Row Value:
writeBufs.RowValBuf, err = tablecodec.EncodeRow(ctx.GetSessionVars().StmtCtx, row, colIDs, writeBufs.RowValBuf, writeBufs.AddRowValues)

构造完成后,调用类似下面这端代码即可将 Key-Value 写到当前事务的缓存中:

    if err = txn.Set(key, value); err != nil {
        return 0, errors.Trace(err)
    }

在事务的提交过程中,即可将这些 Key-Value 提交到存储引擎中。

小结

Insert 语句在诸多 DML 语句中算是最简单的语句,本文也没有涉及 Insert 语句中更复杂的情况,所以相对比较好理解。上面讲了这么多代码,让我们用一幅图来再回顾一下整个流程。

Insert.png

最后给大家留一个思考题,本文描述了如何写入数据,那么 TiDB 是如何删除数据的呢?也就是 Delete 语句的执行流程是什么样子的,请大家追踪源码,调研一下这个流程,有兴趣的读者可以仿照本文写一篇源码解析文档,投稿给我们。

下一篇文章会介绍一下 Select 语句的执行流程,不但会涉及到 SQL 层,也会介绍 Coprocessor 模块是如何工作的,敬请期待。

作者:申砾

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Java后端生活

JDBC(三)PreparedStatement

SQL 注入是利用某些系统没有对用户输入的数据进行充分的检查,而在用户输入数据中注入非法的 SQL 语句段或命令,从而利用系统的 SQL 引擎完成恶意行为的做法

681
来自专栏零基础使用Django2.0.1打造在线教育网站

利用Flask搭建微电影视频网站(二):项目优化与模型设计

努力与运动兼备~~~有任何问题可以加我好友或者关注微信公众号,欢迎交流,我们一起进步!

2321
来自专栏前端侠2.0

oracle中,通过触发器,记录每个语句影响总行数

       业务系统中,有一步“抽数”流程,就是把一些数据从其它服务器同步到本库的目标表。这个过程有可能 多人同时抽数,互相影响。有测试人员反应,原来抽过的数...

952
来自专栏数据库

使用VBA创建Access数据表

导读: 本期介绍如何在Access数据库中创建一张空数据表。下期将介绍如何将工作表中的数据存入数据库对应的表中,随后还将介绍如何从数据库的表中取出数据输出到Ex...

2127
来自专栏PHP实战技术

Mysql存储过程从0开始(上)

1、首先你要明白,mysql也是一种语言,他也可以编写程序,也是支持逻辑判断,if,elseif,else,switch,while等等的判断 2、mysql赋...

2796
来自专栏农夫安全

注入学习之sqli-labs-4(第三关)

前言 说明一下问什么没有less2、less3、less4的讲解? 前两篇如果你弄懂了,第2、3、4关卡原理都是一样的,无非是sql语句的稍微不同 比如: 第一...

3376
来自专栏python成长之路

(解释文)My SQL中主键为0和主键自排约束的关系

1755
来自专栏散尽浮华

mysql表名忽略大小写问题记录

问题描述: 一开发同事在linux下调一个程序老是报错说找不到表,但是登陆mysql,show tables查看明明是已经创建了这张表的!!如下: mysql>...

2607
来自专栏Vamei实验室

来玩Play框架05 数据库

数据库是整个站点的数据储藏室。用户提交的数据可以存储在数据库中,以便未来使用。Play可以通过JDBC和数据库通信。我讲介绍Play和mysql数据库的连接。 ...

2039
来自专栏杨建荣的学习笔记

Oracle中的PUBLIC(r10笔记第14天)

Oracle中的PUBLIC是一种特殊的存在,总是感觉概念比较模糊,我们就简单通过几个测试来理解吧。 首先我们创建一个public的synonym,我们看看这...

2934

扫码关注云+社区