【MySQL 讀書筆記】普通索引和惟一索引應該怎麼選擇

一般咱們在作這個選擇的時候,考慮得最多的應該是若是咱們須要讓 Database MySQL 來幫助咱們從數據庫層面過濾掉對應字段的重複數據咱們會選擇惟一索引,若是沒有前者的需求,通常都會使用普通索引。這篇文章將會站在性能的角度來分析一下二者的區別對性能的影響。mysql

這裏仍是用一張以前分析索引用到的圖。sql

 

查詢過程數據庫

在咱們查詢的時候咱們使用 select id from T where k=5。這個查詢語句經過查詢逐漸搜索到 B+Tree 的葉子節點,而後取到對應的數據頁,而後在數據頁內部找到對應記錄。我記得沒錯的話數據頁內部彷佛是鏈表形式存儲的。緩存

對於普通索引來講,查找到對應知足的條件 500 以後,還須要找下一個記錄,直到碰到第一個不知足 k=5 的條件記錄。併發

對於惟一索引來講,查找到第一條知足條件的記錄以後,就會當即中止繼續檢索。性能

這二者帶來的性能差距是微乎其微的。優化

首先咱們將數據頁從磁盤裏面讀出來是讀出整個16kb 的數據頁,那麼當咱們在找到第一個知足條件的記錄的時候其實絕大多數狀況咱們要讀取的下一個記錄也在內存中。即便不在,咱們也只須要再讀取一個數據頁,而且我記得數據頁之間是有指針直接能夠取到下一個數據頁位置的。這個優化進一步優化了讀取連續數據頁的性能,能夠認爲這樣的操做成本很低。spa

 

更新過程線程

爲了說明普通索引和惟一索引對更新語句性能的影響,咱們先來普及一下 change buffer 這個概念。指針

當咱們須要更新一個數據頁的時候,若是數據也在內存中久直接更新,而若是這個數據頁沒有在內存中,在不影響數據一致性的狀況下, InnoDB 會將這些更新操做緩存在 change buffer 中。這樣就不須要當即取磁盤中讀取這個數據頁進行engine了。在下次須要訪問這個數據頁的時候,再將數據頁讀入內存,而後執行 change buffer 中相關數據頁的操做。這種方式就能保證數據邏輯的正確性,而且節省隨機讀取 IO 消耗,而不是進行頻繁隨機讀取。這裏要特別注意,隨機讀寫多是數據庫裏面消耗最高的操做了。

須要說明的是,雖然名字叫做 change buffer 實際上它是能夠持久化的數據。也就是說, change buffer 在內存中有拷貝,也會被寫入到磁盤上。

將 change buffer 中的操做應用到原數據頁,獲得最新的結果的過程稱爲 merge。除了訪問這個數據頁會觸發 merge 外系統有後臺線程會按期 merge。在數據庫正常關閉的過程當中,也會觸發 merge 操做。

數據讀入內存是須要佔用 buffer pool 的,若是咱們須要更新的操做記錄在 change buffer ,能夠減小讀磁盤,並且這種方式能夠用避免短期內佔用內存,提升內存利用率。

 

那麼哪些狀況下可使用 change buffer 呢?

對於惟一索引的狀況全部的更新狀況都要判斷是否會違反惟一性約束,好比咱們在插入一條記錄的時候,咱們須要先判斷是否已經存在這條記錄,只要有咱們去掃表才能判斷,咱們就須要把對應的數據頁讀入內存,若是已經讀入內存,若是已經讀入內存就直接更新插入就行。沒有必要去使用 change buffer ,反正都必須先讀入內存。

所以,惟一索引的更新就不能使用 change buffer 這個東西。實際上就只有普通索引可使用 change buffer.

change buffer  用的是 InnoDB buffer pool 裏面的內存,change buffer 的大小能夠經過參數 innodb_change_buffer_max_size 來動態設置。這個參數默認是 25。表示最多佔用 buffer pool 百分之 25 應用於做 change buffer。

 

Change Buffer 的使用場景

經過上面的分析,你已經清楚了使用 change buffer 對更新過程的加速做用,也清楚了 change buffer 只限於用在普通索引的場景下,而不適用於惟一索引。那麼,如今有一個問題就是:普通索引的全部場景,使用 change buffer 均可以起到加速做用嗎?

由於 merge 的時候是真正進行數據更新的時刻,而 change buffer 的主要目的就是將記錄的變動動做緩存下來,因此在一個數據頁作 merge 以前,change buffer 記錄的變動越多(也就是這個頁面上要更新的次數越多),收益就越大。

所以,對於寫多讀少的業務來講,頁面在寫完之後立刻被訪問到的機率比較小,此時 change buffer 的使用效果最好。這種業務模型常見的就是帳單類、日誌類的系統。

反過來,假設一個業務的更新模式是寫入以後立刻會作查詢,那麼即便知足了條件,將更新先記錄在 change buffer,但以後因爲立刻要訪問這個數據頁,會當即觸發 merge 過程。這樣隨機訪問 IO 的次數不會減小,反而增長了 change buffer 的維護代價。因此,對於這種業務模式來講,change buffer 反而起到了反作用。

 

因此實際狀況中,若是是併發量不高的業務,仍是在底層對數據作保護不要猶豫,直接選用惟一索引是比較好的選擇。若是是對更新性能要求極高的場景,能夠考慮建普通索引,而後在代碼裏面對惟一性的狀況進行保護。

 

change buffer 和 redo log

理解了 change buffer 的原理,你可能會聯想到我在前面文章中和你介紹過的 redo log 和 WAL。

在前面文章的評論中,我發現有同窗混淆了 redo log 和 change buffer。WAL 提高性能的核心機制,也的確是儘可能減小隨機讀寫,這兩個概念確實容易混淆。因此,這裏我把它們放到了同一個流程裏來講明,便於你區分這兩個概念。

 

如今,咱們要在表上執行插入語句:

mysql> insert into t(id,k) values(id1,k1),(id2,k2);

這裏,咱們假設當前 k 索引樹的狀態,查找到位置後,k1 所在的數據頁在內存 (InnoDB buffer pool) 中,k2 所在的數據頁不在內存中。如圖 2 所示是帶 change buffer 的更新狀態圖。

這條插入操做作了以下操做:

1. page 1 在內存中,直接更新內存。

2. page 2 沒有在內存中,就在內存的 change buffer 區域,記錄下「我要往 page 2 插入行」這個信息。

3. 將這兩個動做記入 redo log 中。

作完這些事情事務就能夠完成了。成本是 更新兩處內存,一個 直接更新內存 一個 更新 change buffer。而後一次寫 redo log 寫磁盤操做。

同時圖中虛線部分不是後臺操做,不影響更新時間。

 

前面我說了觸發 merge 除了定時 merge 正常 shutdown MySQL 之外 若是當即查詢對應頁上的數據也會當即觸發和 change buffer 的 merge。

若是讀語句發生在更新語句後不久,內存中的數據都還在,那麼此時的這兩個讀操做就與系統表空間(ibdata1)和 redo log(ib_log_fileX)無關了。因此,我在圖中就沒畫出這兩部分。 

從圖中能夠看到:

1. 讀 Page 1 的時候,直接從內存返回。有幾位同窗在前面文章的評論中問到,WAL 以後若是讀數據,是否是必定要讀盤,是否是必定要從 redo log 裏面把數據更新之後才能夠返回?實際上是不用的。你能夠看一下圖 3 的這個狀態,雖然磁盤上仍是以前的數據,可是這裏直接從內存返回結果,結果是正確的。

2. 要讀 Page 2 的時候,須要把 Page 2 從磁盤讀入內存中,而後應用 change buffer 裏面的操做日誌,生成一個正確的版本並返回結果。

能夠看到直到要讀 page 2 的時候,這個數據頁纔會被讀入內存。

因此,若是要簡單對比這兩個機制在提高更新性能上的收益的話, redo log 主要節省的是隨機寫的磁盤的 io 消耗。

 

因爲惟一索引用不上 change buffer 的優化機制,所以若是業務能夠接受。從性能的角度出發應該先考慮非惟一索引。可是我前面也說了,仍是看業務來,若是對性能本就沒有什麼要求,而且代碼質量纔是頭等大問題,那應該堅決果斷的使用惟一索引。可讓你避免不少麻煩。

 

 

Reference:

本讀書筆記皆來自發布在極客時間的 林曉斌(丁奇)的 MySQL 實戰45講:

極客時間版權全部: https://time.geekbang.org/ 版權全部: 

https://time.geekbang.org/column/article/70848

相關文章
相關標籤/搜索