本文爲 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 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() 中完成。對於這個簡單的語句,主要涉及兩個部分:緩存
包括 Database/Table/Column 信息,這個語句沒有指定向那些列插入數據,因此會使用全部的列。app
這裏會處理一遍全部的 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 語句中更復雜的狀況,因此相對比較好理解。上面講了這麼多代碼,讓咱們用一幅圖來再回顧一下整個流程。
最後給你們留一個思考題,本文描述瞭如何寫入數據,那麼 TiDB 是如何刪除數據的呢?也就是 Delete 語句的執行流程是什麼樣子的,請你們追蹤源碼,調研一下這個流程,有興趣的讀者能夠仿照本文寫一篇源碼解析文檔,投稿給咱們。
下一篇文章會介紹一下 Select 語句的執行流程,不但會涉及到 SQL 層,也會介紹 Coprocessor 模塊是如何工做的,敬請期待。
做者:申礫