TiDB 源码阅读系列文章(十六)INSERT 语句详解
在之前的一篇文章 《TiDB 源碼閱讀系列文章(四)INSERT 語句概覽》 中,我們已經(jīng)介紹了 INSERT 語句的大體流程。為什么需要為 INSERT 單獨(dú)再寫一篇?因?yàn)樵?TiDB 中,單純插入一條數(shù)據(jù)是最簡(jiǎn)單的情況,也是最常用的情況;更為復(fù)雜的是在 INSERT 語句中設(shè)定各種行為,比如,對(duì)于 Unique Key 沖突的情況應(yīng)如何處理:是報(bào)錯(cuò)?是忽略當(dāng)前插入的數(shù)據(jù)?還是覆蓋已有數(shù)據(jù)?所以,這篇會(huì)為大家繼續(xù)深入介紹 INSERT 語句。
本文將首先介紹在 TiDB 中的 INSERT 語句的分類,以及各語句的語法和語義,然后分別介紹五種 INSERT 語句的源碼實(shí)現(xiàn)。
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 (),語義為插入一條語句,若發(fā)生唯一約束沖突(主鍵沖突、唯一索引沖突),則返回執(zhí)行失敗。
第二種,語法 INSERT IGNORE INTO VALUES (),是當(dāng) INSERT 的時(shí)候遇到唯一約束沖突后,忽略當(dāng)前 INSERT 的行,并記一個(gè) warning。當(dāng)語句執(zhí)行結(jié)束后,可以通過 SHOW WARNINGS 看到哪些行沒有被插入。
第三種,語法 INSERT INTO VALUES () ON DUPLICATE KEY UPDATE,是當(dāng)沖突后,更新沖突行后插入數(shù)據(jù)。如果更新后的行跟表中另一行沖突,則返回錯(cuò)誤。
第四種,是在上一種情況,更新后的行又跟另一行沖突后,不插入該行并顯示為一個(gè) warning。
第五種,語法 REPLACE INTO VALUES (),是當(dāng)沖突后,刪除表上的沖突行,并繼續(xù)嘗試插入數(shù)據(jù),如再次沖突,則繼續(xù)刪除標(biāo)上沖突數(shù)據(jù),直到表上沒有與改行沖突的數(shù)據(jù)后,插入數(shù)據(jù)。
最后一種,語法 LOAD DATA INFILE INTO 的語義與 INSERT IGNORE 相同,都是沖突即忽略,不同的是 LOAD DATA 的作用是將數(shù)據(jù)文件導(dǎo)入到表中,也就是其數(shù)據(jù)來源于 csv 數(shù)據(jù)文件。
由于 INSERT IGNORE ON DUPLICATE KEY UPDATE 是在 INSERT ON DUPLICATE KEY UPDATE 上做了些特殊處理,將不再單獨(dú)詳細(xì)介紹,而是放在同一小節(jié)中介紹;LOAD DATA 由于其自身的特殊性,將留到其他篇章介紹。
Basic INSERT 語句
幾種 INSERT 語句的最大不同在于執(zhí)行層面,這里接著 《TiDB 源碼閱讀系列文章(四)INSERT 語句概覽》 來講語句執(zhí)行過程。不記得前面內(nèi)容的同學(xué)可以返回去看原文章。
INSERT 的執(zhí)行邏輯在 executor/insert.go 中。其實(shí)前面講的前四種 INSERT 的執(zhí)行邏輯都在這個(gè)文件里。這里先講最普通的 Basic INSERT。
InsertExec 是 INSERT 的執(zhí)行器實(shí)現(xiàn),其實(shí)現(xiàn)了 Executor 接口。最重要的是下面三個(gè)接口:
- Open:進(jìn)行一些初始化
- Next:執(zhí)行寫入操作
- Close:做一些清理工作
其中最重要也是最復(fù)雜的是 Next 方法,根據(jù)是否通過一個(gè) SELECT 語句來獲取數(shù)據(jù)(INSERT SELECT FROM),將 Next 流程分為,insertRows 和 insertRowsFromSelect 兩個(gè)流程。兩個(gè)流程最終都會(huì)進(jìn)入 exec 函數(shù),執(zhí)行 INSERT。
exec 函數(shù)里處理了前四種 INSERT 語句,其中本節(jié)要講的普通 INSERT 直接進(jìn)入了 insertOneRow。
在講 insertOneRow 之前,我們先看一段 SQL。
CREATE TABLE t (i INT UNIQUE); INSERT INTO t VALUES (1); BEGIN; INSERT INTO t VALUES (1); COMMIT;把這段 SQL 分別一行行地粘在 MySQL 和 TiDB 中看下結(jié)果。
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'可以看出來,對(duì)于 INSERT 語句 TiDB 是在事務(wù)提交的時(shí)候才做沖突檢測(cè)而 MySQL 是在語句執(zhí)行的時(shí)候做的檢測(cè)。這樣處理的原因是,TiDB 在設(shè)計(jì)上,與 TiKV 是分層的結(jié)構(gòu),為了保證高效率的執(zhí)行,在事務(wù)內(nèi)只有讀操作是必須從存儲(chǔ)引擎獲取數(shù)據(jù),而所有的寫操作都事先放在單 TiDB 實(shí)例內(nèi)事務(wù)自有的 memDbBuffer 中,在事務(wù)提交時(shí)才一次性將事務(wù)寫入 TiKV。在實(shí)現(xiàn)中是在 insertOneRow 中設(shè)置了 PresumeKeyNotExists 選項(xiàng),所有的 INSERT 操作如果在本地檢測(cè)沒發(fā)現(xiàn)沖突,就先假設(shè)插入不會(huì)發(fā)生沖突,不需要去 TiKV 中檢查沖突數(shù)據(jù)是否存在,只將這些數(shù)據(jù)標(biāo)記為待檢測(cè)狀態(tài)。最后到提交過程中,統(tǒng)一將整個(gè)事務(wù)里待檢測(cè)數(shù)據(jù)使用 BatchGet 接口做一次批量檢測(cè)。
當(dāng)所有的數(shù)據(jù)都通過 insertOneRow 執(zhí)行完插入后,INSERT 語句基本結(jié)束,剩余的工作為設(shè)置一下 lastInsertID 等返回信息,并最終將其結(jié)果返回給客戶端。
INSERT IGNORE 語句
INSERT IGNORE 的語義在前面已經(jīng)介紹了。之前介紹了普通 INSERT 在提交的時(shí)候才檢查,那 INSERT IGNORE 是否可以呢?答案是不行的。因?yàn)?#xff1a;
這就需要在執(zhí)行 INSERT IGNORE 的時(shí)候,及時(shí)檢查數(shù)據(jù)的沖突情況。一個(gè)顯而易見的做法是,把需要插入的數(shù)據(jù)試著讀出來,當(dāng)發(fā)現(xiàn)沖突后,記一個(gè) warning,再繼續(xù)下一行。但是對(duì)于一個(gè)語句插入多行的情況,就需要反復(fù)從 TiKV 讀取數(shù)據(jù)來進(jìn)行檢測(cè),顯然,這樣的效率并不高。于是,TiDB 實(shí)現(xiàn)了 batchChecker,代碼在 executor/batch_checker.go。
在 batchChecker 中,首先,拿待插入的數(shù)據(jù),將其中可能沖突的唯一約束在 getKeysNeedCheck 中構(gòu)造成 Key(TiDB 是通過構(gòu)造唯一的 Key 來實(shí)現(xiàn)唯一約束的,詳見 《三篇文章了解 TiDB 技術(shù)內(nèi)幕——說計(jì)算》)。
然后,將構(gòu)造出來的 Key 通過 BatchGetValues 一次性讀上來,得到一個(gè) Key-Value map,能被讀到的都是沖突的數(shù)據(jù)。
最后,拿即將插入的數(shù)據(jù)的 Key 到 BatchGetValues 的結(jié)果中進(jìn)行查詢。如果查到了沖突的行,構(gòu)造好 warning 信息,然后開始下一行,如果查不到?jīng)_突的行,就可以進(jìn)行安全的 INSERT 了。這部分的實(shí)現(xiàn)在 batchCheckAndInsert 中。
同樣,在所有數(shù)據(jù)執(zhí)行完插入后,設(shè)置返回信息,并將執(zhí)行結(jié)果返回客戶端。
INSERT ON DUPLICATE KEY UPDATE 語句
INSERT ON DUPLICATE KEY UPDATE 是幾種 INSERT 語句中最為復(fù)雜的。其語義的本質(zhì)是包含了一個(gè) INSERT 和 一個(gè) UPDATE。較之與其他 INSERT 復(fù)雜的地方就在于,UPDATE 語義是可以將一行更新成任何合法的樣子。
在上一節(jié)中,介紹了 TiDB 中對(duì)于特殊的 INSERT 語句采用了 batch 的方式來實(shí)現(xiàn)其沖突檢查。在處理 INSERT ON DUPLICATE KEY UPDATE 的時(shí)候我們采用了同樣的方式,但由于語義的復(fù)雜性,實(shí)現(xiàn)步驟也復(fù)雜了不少。
首先,與 INSERT IGNORE 相同,首先將待插入數(shù)據(jù)構(gòu)造出來的 Key,通過 BatchGetValues 一次性地讀出來,得到一個(gè) Key-Value map。再把所有讀出來的 Key 對(duì)應(yīng)的表上的記錄也通過一次 BatchGetValues 讀出來,這部分?jǐn)?shù)據(jù)是為了將來做 UPDATE 準(zhǔn)備的,具體實(shí)現(xiàn)在 initDupOldRowValue。
然后,在做沖突檢查的時(shí)候,如果遇到?jīng)_突,則首先進(jìn)行一次 UPDATE。我們?cè)谇懊?Basic INSERT 小節(jié)中已經(jīng)介紹了,TiDB 的 INSERT 是提交的時(shí)候才去 TiKV 真正執(zhí)行。同樣的,UPDATE 語句也是在事務(wù)提交的時(shí)候才真正去 TiKV 執(zhí)行的。在這次 UPDATE 中,可能還是會(huì)遇到唯一約束沖突的問題,如果遇到了,此時(shí)即報(bào)錯(cuò)返回,如果該語句是 INSERT IGNORE ON DUPLICATE KEY UPDATE 則會(huì)忽略這個(gè)錯(cuò)誤,繼續(xù)下一行。
在上一步的 UPDATE 中,還需要處理以下場(chǎng)景,如下面這個(gè) SQL:
CREATE TABLE t (i INT UNIQUE); INSERT INTO t VALUES (1), (1) ON DUPLICATE KEY UPDATE i = i;可以看到,這個(gè) SQL 中,表中原來并沒有數(shù)據(jù),第二句的 INSERT 也就不可能讀到可能沖突的數(shù)據(jù),但是,這句 INSERT 本身要插入的兩行數(shù)據(jù)之間沖突了。這里的正確執(zhí)行應(yīng)該是,第一個(gè) 1 正常插入,第二個(gè) 1 插入的時(shí)候發(fā)現(xiàn)有沖突,更新第一個(gè) 1。此時(shí),就需要做如下處理。將上一步被 UPDATE 的數(shù)據(jù)對(duì)應(yīng)的 Key-Value 從第一步的 Key-Value map 中刪掉,將 UPDATE 出來的數(shù)據(jù)再根據(jù)其表信息構(gòu)造出唯一約束的 Key 和 Value,把這個(gè) Key-Value 對(duì)放回第一步讀出來 Key-Value map 中,用于后續(xù)數(shù)據(jù)進(jìn)行沖突檢查。這個(gè)細(xì)節(jié)的實(shí)現(xiàn)在 fillBackKeys。這種場(chǎng)景同樣出現(xiàn)在,其他 INSERT 語句中,如 INSERT IGNORE、REPLACE、LOAD DATA。之所以在這里介紹是因?yàn)?#xff0c;INSERT ON DUPLICATE KEY UPDATE 是最能完整展現(xiàn) batchChecker 的各方面的語句。
最后,同樣在所有數(shù)據(jù)執(zhí)行完插入/更新后,設(shè)置返回信息,并將執(zhí)行結(jié)果返回客戶端。
REPLACE 語句
REPLACE 語句雖然它看起來像是獨(dú)立的一類 DML,實(shí)際上觀察語法的話,它與 Basic INSERT 只是把 INSERT 換成了 REPLACE。與之前介紹的所有 INSERT 語句不同的是,REPLACE 語句是一個(gè)一對(duì)多的語句。簡(jiǎn)要說明一下就是,一般的 INSERT 語句如果需要 INSERT 某一行,那將會(huì)當(dāng)遭遇了唯一約束沖突的時(shí)候,出現(xiàn)以下幾種處理方式:
- 放棄插入,報(bào)錯(cuò)返回:Basic INSERT
- 放棄插入,不報(bào)錯(cuò):INSERT IGNORE
- 放棄插入,改成更新沖突的行,如果更新的值再次沖突
- 報(bào)錯(cuò):INSERT ON DUPLICATE KEY UPDATE
- 不報(bào)錯(cuò):INSERT IGNORE ON DUPLICATE KEY UPDATE
他們都是處理一行數(shù)據(jù)跟表中的某一行沖突時(shí)的不同處理。但是 REPLACE 語句不同,它將會(huì)刪除遇到的所有沖突行,直到?jīng)]有沖突后再插入數(shù)據(jù)。如果表中有 5 個(gè)唯一索引,那有可能有 5 條與等待插入的行沖突的行。那么 REPLACE 語句將會(huì)一次性刪除這 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在執(zhí)行完之后,實(shí)際影響了 5 行數(shù)據(jù)。
理解了 REPLACE 語句的特殊性以后,我們就可以更容易理解其具體實(shí)現(xiàn)。
與 INSERT 語句類似,REPLACE 語句的主要執(zhí)行部分也在其 Next 方法中,與 INSERT 不同的是,其中的 insertRowsFromSelect 和 insertRows 傳遞了 ReplaceExec 自己的 exec 方法。在 exec 中調(diào)用了 replaceRow,其中同樣使用了 batchChecker 中的批量沖突檢測(cè),與 INSERT 有所不同的是,這里會(huì)刪除一切檢測(cè)出的沖突,最后將待插入行寫入。
寫在最后
INSERT 語句是所有 DML 語句中最復(fù)雜,功能最強(qiáng)大多變的一個(gè)。其既有像 INSERT ON DUPLICATE UPDATE 這種能執(zhí)行 INSERT 也能執(zhí)行 UPDATE 的語句,也有像 REPLACE 這種一行數(shù)據(jù)能影響許多行數(shù)據(jù)的語句。INSERT 語句自身都可以連接一個(gè) SELECT 語句作為待插入數(shù)據(jù)的輸入,因此,其又受到了來自 planner 的影響(關(guān)于 planner 的部分詳見相關(guān)的源碼閱讀文章: (七)基于規(guī)則的優(yōu)化 和 (八)基于代價(jià)的優(yōu)化)。熟悉 TiDB 的 INSERT 各個(gè)語句實(shí)現(xiàn),可以幫助各位讀者在將來使用這些語句時(shí),更好地根據(jù)其特色使用最為合理、高效語句。另外,如果有興趣向 TiDB 貢獻(xiàn)代碼的讀者,也可以通過本文更快的理解這部分的實(shí)現(xiàn)。
作者:于帥鵬總結(jié)
以上是生活随笔為你收集整理的TiDB 源码阅读系列文章(十六)INSERT 语句详解的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Confluence 6 使用 Apac
- 下一篇: webpack4.0各个击破(2)——