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

在之前的一篇文章 《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 貢獻代碼的讀者,也可以通過本文更快的理解這部分的實現。

作者:于帥鵬
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章