TiDB 源碼閱讀系列文章(四)Insert 語句概覽

本文爲 TiDB 源碼閱讀系列文章的第四篇。上一篇文章簡單介紹了總體流程,不管什麼語句,大致上是在這個框架下運行,DDL 語句也不例外。html

本篇文章會以 Insert 語句爲例進行講解,幫助讀者理解前一篇文章,下一篇文章會介紹 Select 語句的執行流程。這兩條是最經常使用的讀、寫語句,其餘的語句相信讀者能舉一反三,能夠自行研究或者是等待後續的文章。對於這兩類語句,目前也只會針對核心流程進行說明,更復雜的 Join、Insert-Into-OnDuplicate-Update 等會等到後面的文章進行講解。另外本文會重點介紹每一個語句在執行框架下面的具體執行邏輯,請讀者閱讀前先了解 Insert 語句的行爲。mysql

表結構

這裏先給一個表結構,下面介紹的 SQL 語句都是在這個表上的操做。git

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

Insert 語句

INSERT INTO t VALUES ("pingcap001", "pingcap", 3); 以這條語句爲例,解釋 Insert 是如何運行的。github

語句處理流程

首先你們回憶一下上一篇文章介紹的框架,一條 SQL 語句通過協議層、Parser、Plan、Executor 這樣幾個模塊處理後,變成可執行的結構,再經過 Next() 來驅動語句的真正執行。對於框架,每類語句都差很少;對於每一個核心步驟,每一個語句會有本身的處理邏輯。sql

語法解析

先看 Parser,對於 Insert 語句的解析邏輯在這裏,能夠看到這條語句會被解析成下面這個結構express

// 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 信息,這個語句沒有指定向那些列插入數據,因此會使用全部的列。app

  • 處理 Lists 中的數據

    這裏會處理一遍全部的 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 模塊是如何工做的,敬請期待。

做者:申礫
相關文章
相關標籤/搜索