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

在以前的一篇文章 《TiDB 源碼閱讀系列文章(四)INSERT 語句概覽》 中,咱們已經介紹了 INSERT 語句的大致流程。爲何須要爲 INSERT 單獨再寫一篇?由於在 TiDB 中,單純插入一條數據是最簡單的狀況,也是最經常使用的狀況;更爲複雜的是在 INSERT 語句中設定各類行爲,好比,對於 Unique Key 衝突的狀況應如何處理:是報錯?是忽略當前插入的數據?仍是覆蓋已有數據?因此,這篇會爲你們繼續深刻介紹 INSERT 語句。mysql

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

INSERT 語句的種類

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

  • Basic INSERTsql

  • INSERT IGNORE安全

  • INSERT ON DUPLICATE KEY UPDATE函數

  • INSERT IGNORE ON DUPLICATE KEY UPDATE優化

  • REPLACE設計

  • LOAD DATA3d

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

第一種,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 貢獻代碼的讀者,也能夠經過本文更快的理解這部分的實現。

做者:于帥鵬

相關文章
相關標籤/搜索