TiDB 原始碼閱讀系列文章(十六)INSERT 語句詳解

PingCAP發表於2019-02-27

在之前的一篇文章 《TiDB 原始碼閱讀系列文章(四)INSERT 語句概覽》 中,我們已經介紹了 INSERT 語句的大體流程。為什麼需要為 INSERT 單獨再寫一篇?因為在 TiDB 中,單純插入一條資料是最簡單的情況,也是最常用的情況;更為複雜的是在 INSERT 語句中設定各種行為,比如,對於 Unique Key 衝突的情況應如何處理:是報錯?是忽略當前插入的資料?還是覆蓋已有資料?所以,這篇會為大家繼續深入介紹 INSERT 語句。

本文將首先介紹在 TiDB 中的 INSERT 語句的分類,以及各語句的語法和語義,然後分別介紹五種 INSERT 語句的原始碼實現。

INSERT 語句的種類

從廣義上講,TiDB 有以下六種 INSERT 語句:

  • Basic INSERT

  • INSERT IGNORE

  • INSERT ON DUPLICATE KEY UPDATE

  • INSERT IGNORE ON DUPLICATE KEY UPDATE

  • REPLACE

  • LOAD DATA

這六種語句理論上都屬於 INSERT 語句。

第一種,Basic INSERT,即是最普通的 INSERT 語句,語法 INSERT INTO VALUES (),語義為插入一條語句,若發生唯一約束衝突(主鍵衝突、唯一索引衝突),則返回執行失敗。

第二種,語法 INSERT IGNORE INTO VALUES (),是當 INSERT 的時候遇到唯一約束衝突後,忽略當前 INSERT 的行,並記一個 warning。當語句執行結束後,可以通過 SHOW WARNINGS 看到哪些行沒有被插入。

第三種,語法 INSERT INTO VALUES () ON DUPLICATE KEY UPDATE,是當衝突後,更新衝突行後插入資料。如果更新後的行跟表中另一行衝突,則返回錯誤。

第四種,是在上一種情況,更新後的行又跟另一行衝突後,不插入該行並顯示為一個 warning。

第五種,語法 REPLACE INTO VALUES (),是當衝突後,刪除表上的衝突行,並繼續嘗試插入資料,如再次衝突,則繼續刪除標上衝突資料,直到表上沒有與改行衝突的資料後,插入資料。

最後一種,語法 LOAD DATA INFILE INTO 的語義與 INSERT IGNORE 相同,都是衝突即忽略,不同的是 LOAD DATA 的作用是將資料檔案匯入到表中,也就是其資料來源於 csv 資料檔案。

由於 INSERT IGNORE ON DUPLICATE KEY UPDATE 是在 INSERT ON DUPLICATE KEY UPDATE 上做了些特殊處理,將不再單獨詳細介紹,而是放在同一小節中介紹;LOAD DATA 由於其自身的特殊性,將留到其他篇章介紹。

Basic INSERT 語句

幾種 INSERT 語句的最大不同在於執行層面,這裡接著 《TiDB 原始碼閱讀系列文章(四)INSERT 語句概覽》 來講語句執行過程。不記得前面內容的同學可以返回去看原文章。

INSERT 的執行邏輯在 executor/insert.go 中。其實前面講的前四種 INSERT 的執行邏輯都在這個檔案裡。這裡先講最普通的 Basic INSERT

InsertExec 是 INSERT 的執行器實現,其實現了 Executor 介面。最重要的是下面三個介面:

  • Open:進行一些初始化

  • Next:執行寫入操作

  • Close:做一些清理工作

其中最重要也是最複雜的是 Next 方法,根據是否通過一個 SELECT 語句來獲取資料(INSERT SELECT FROM),將 Next 流程分為,insertRowsinsertRowsFromSelect 兩個流程。兩個流程最終都會進入 exec 函式,執行 INSERT。

exec 函式裡處理了前四種 INSERT 語句,其中本節要講的普通 INSERT 直接進入了 insertOneRow

在講 insertOneRow 之前,我們先看一段 SQL。

CREATE TABLE t (i INT UNIQUE);
INSERT INTO t VALUES (1);
BEGIN;
INSERT INTO t VALUES (1);
COMMIT;
複製程式碼

把這段 SQL 分別一行行地粘在 MySQL 和 TiDB 中看下結果。

MySQL:

mysql> CREATE TABLE t (i INT UNIQUE);
Query OK, 0 rows affected (0.15 sec)

mysql> INSERT INTO t VALUES (1);
Query OK, 1 row affected (0.01 sec)

mysql> BEGIN;
Query OK, 0 rows affected (0.00 sec)

mysql> INSERT INTO t VALUES (1);
ERROR 1062 (23000): Duplicate entry `1` for key `i`
mysql> COMMIT;
Query OK, 0 rows affected (0.11 sec)
複製程式碼

TiDB:

mysql> CREATE TABLE t (i INT UNIQUE);
Query OK, 0 rows affected (1.04 sec)

mysql> INSERT INTO t VALUES (1);
Query OK, 1 row affected (0.12 sec)

mysql> BEGIN;
Query OK, 0 rows affected (0.01 sec)

mysql> INSERT INTO t VALUES (1);
Query OK, 1 row affected (0.00 sec)

mysql> COMMIT;
ERROR 1062 (23000): Duplicate entry `1` for key `i`
複製程式碼

可以看出來,對於 INSERT 語句 TiDB 是在事務提交的時候才做衝突檢測而 MySQL 是在語句執行的時候做的檢測。這樣處理的原因是,TiDB 在設計上,與 TiKV 是分層的結構,為了保證高效率的執行,在事務內只有讀操作是必須從儲存引擎獲取資料,而所有的寫操作都事先放在單 TiDB 例項內事務自有的 memDbBuffer 中,在事務提交時才一次性將事務寫入 TiKV。在實現中是在 insertOneRow 中設定了 PresumeKeyNotExists 選項,所有的 INSERT 操作如果在本地檢測沒發現衝突,就先假設插入不會發生衝突,不需要去 TiKV 中檢查衝突資料是否存在,只將這些資料標記為待檢測狀態。最後到提交過程中,統一將整個事務裡待檢測資料使用 BatchGet 介面做一次批量檢測。

當所有的資料都通過 insertOneRow 執行完插入後,INSERT 語句基本結束,剩餘的工作為設定一下 lastInsertID 等返回資訊,並最終將其結果返回給客戶端。

INSERT IGNORE 語句

INSERT IGNORE 的語義在前面已經介紹了。之前介紹了普通 INSERT 在提交的時候才檢查,那 INSERT IGNORE 是否可以呢?答案是不行的。因為:

  1. INSERT IGNORE 如果在提交時檢測,那事務模組就需要知道哪些行需要忽略,哪些直接報錯回滾,這無疑增加了模組間的耦合。

  2. 使用者希望立刻獲取 INSERT IGNORE 有哪些行沒有寫入進去。即,立刻通過 SHOW WARNINGS 看到哪些行實際沒有寫入。

這就需要在執行 INSERT IGNORE 的時候,及時檢查資料的衝突情況。一個顯而易見的做法是,把需要插入的資料試著讀出來,當發現衝突後,記一個 warning,再繼續下一行。但是對於一個語句插入多行的情況,就需要反覆從 TiKV 讀取資料來進行檢測,顯然,這樣的效率並不高。於是,TiDB 實現了 batchChecker,程式碼在 executor/batch_checker.go

batchChecker 中,首先,拿待插入的資料,將其中可能衝突的唯一約束在 getKeysNeedCheck 中構造成 Key(TiDB 是通過構造唯一的 Key 來實現唯一約束的,詳見 《三篇文章瞭解 TiDB 技術內幕——說計算》)。

然後,將構造出來的 Key 通過 BatchGetValues 一次性讀上來,得到一個 Key-Value map,能被讀到的都是衝突的資料。

最後,拿即將插入的資料的 Key 到 BatchGetValues 的結果中進行查詢。如果查到了衝突的行,構造好 warning 資訊,然後開始下一行,如果查不到衝突的行,就可以進行安全的 INSERT 了。這部分的實現在 batchCheckAndInsert 中。

同樣,在所有資料執行完插入後,設定返回資訊,並將執行結果返回客戶端。

INSERT ON DUPLICATE KEY UPDATE 語句

INSERT ON DUPLICATE KEY UPDATE 是幾種 INSERT 語句中最為複雜的。其語義的本質是包含了一個 INSERT 和 一個 UPDATE。較之與其他 INSERT 複雜的地方就在於,UPDATE 語義是可以將一行更新成任何合法的樣子。

在上一節中,介紹了 TiDB 中對於特殊的 INSERT 語句採用了 batch 的方式來實現其衝突檢查。在處理 INSERT ON DUPLICATE KEY UPDATE 的時候我們採用了同樣的方式,但由於語義的複雜性,實現步驟也複雜了不少。

首先,與 INSERT IGNORE 相同,首先將待插入資料構造出來的 Key,通過 BatchGetValues 一次性地讀出來,得到一個 Key-Value map。再把所有讀出來的 Key 對應的表上的記錄也通過一次 BatchGetValues 讀出來,這部分資料是為了將來做 UPDATE 準備的,具體實現在 initDupOldRowValue

然後,在做衝突檢查的時候,如果遇到衝突,則首先進行一次 UPDATE。我們在前面 Basic INSERT 小節中已經介紹了,TiDB 的 INSERT 是提交的時候才去 TiKV 真正執行。同樣的,UPDATE 語句也是在事務提交的時候才真正去 TiKV 執行的。在這次 UPDATE 中,可能還是會遇到唯一約束衝突的問題,如果遇到了,此時即報錯返回,如果該語句是 INSERT IGNORE ON DUPLICATE KEY UPDATE 則會忽略這個錯誤,繼續下一行。

在上一步的 UPDATE 中,還需要處理以下場景,如下面這個 SQL:

CREATE TABLE t (i INT UNIQUE);
INSERT INTO t VALUES (1), (1) ON DUPLICATE KEY UPDATE i = i;
複製程式碼

可以看到,這個 SQL 中,表中原來並沒有資料,第二句的 INSERT 也就不可能讀到可能衝突的資料,但是,這句 INSERT 本身要插入的兩行資料之間衝突了。這裡的正確執行應該是,第一個 1 正常插入,第二個 1 插入的時候發現有衝突,更新第一個 1。此時,就需要做如下處理。將上一步被 UPDATE 的資料對應的 Key-Value 從第一步的 Key-Value map 中刪掉,將 UPDATE 出來的資料再根據其表資訊構造出唯一約束的 Key 和 Value,把這個 Key-Value 對放回第一步讀出來 Key-Value map 中,用於後續資料進行衝突檢查。這個細節的實現在 fillBackKeys。這種場景同樣出現在,其他 INSERT 語句中,如 INSERT IGNOREREPLACELOAD DATA。之所以在這裡介紹是因為,INSERT ON DUPLICATE KEY UPDATE 是最能完整展現 batchChecker 的各方面的語句。

最後,同樣在所有資料執行完插入/更新後,設定返回資訊,並將執行結果返回客戶端。

REPLACE 語句

REPLACE 語句雖然它看起來像是獨立的一類 DML,實際上觀察語法的話,它與 Basic INSERT 只是把 INSERT 換成了 REPLACE。與之前介紹的所有 INSERT 語句不同的是,REPLACE 語句是一個一對多的語句。簡要說明一下就是,一般的 INSERT 語句如果需要 INSERT 某一行,那將會當遭遇了唯一約束衝突的時候,出現以下幾種處理方式:

  • 放棄插入,報錯返回:Basic INSERT

  • 放棄插入,不報錯:INSERT IGNORE

  • 放棄插入,改成更新衝突的行,如果更新的值再次衝突

  • 報錯:INSERT ON DUPLICATE KEY UPDATE

  • 不報錯:INSERT IGNORE ON DUPLICATE KEY UPDATE

他們都是處理一行資料跟表中的某一行衝突時的不同處理。但是 REPLACE 語句不同,它將會刪除遇到的所有衝突行,直到沒有衝突後再插入資料。如果表中有 5 個唯一索引,那有可能有 5 條與等待插入的行衝突的行。那麼 REPLACE 語句將會一次性刪除這 5 行,再將自己插入。看以下 SQL:

CREATE TABLE t (
i int unique, 
j int unique, 
k int unique, 
l int unique, 
m int unique);

INSERT INTO t VALUES 
(1, 1, 1, 1, 1), 
(2, 2, 2, 2, 2), 
(3, 3, 3, 3, 3), 
(4, 4, 4, 4, 4);

REPLACE INTO t VALUES (1, 2, 3, 4, 5);

SELECT * FROM t;
i j k l m
1 2 3 4 5
複製程式碼

在執行完之後,實際影響了 5 行資料。

理解了 REPLACE 語句的特殊性以後,我們就可以更容易理解其具體實現。

與 INSERT 語句類似,REPLACE 語句的主要執行部分也在其 Next 方法中,與 INSERT 不同的是,其中的 insertRowsFromSelectinsertRows 傳遞了 ReplaceExec 自己的 exec 方法。在 exec 中呼叫了 replaceRow,其中同樣使用了 batchChecker 中的批量衝突檢測,與 INSERT 有所不同的是,這裡會刪除一切檢測出的衝突,最後將待插入行寫入。

寫在最後

INSERT 語句是所有 DML 語句中最複雜,功能最強大多變的一個。其既有像 INSERT ON DUPLICATE UPDATE 這種能執行 INSERT 也能執行 UPDATE 的語句,也有像 REPLACE 這種一行資料能影響許多行資料的語句。INSERT 語句自身都可以連線一個 SELECT 語句作為待插入資料的輸入,因此,其又受到了來自 planner 的影響(關於 planner 的部分詳見相關的原始碼閱讀文章: (七)基於規則的優化(八)基於代價的優化)。熟悉 TiDB 的 INSERT 各個語句實現,可以幫助各位讀者在將來使用這些語句時,更好地根據其特色使用最為合理、高效語句。另外,如果有興趣向 TiDB 貢獻程式碼的讀者,也可以通過本文更快的理解這部分的實現。

作者:于帥鵬

相關文章