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


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

這里還是用一張之前分析索引用到的圖。

 

查詢過程

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

對於普通索引來說,查找到對應滿足的條件 500 之后,還需要找下一個記錄,直到碰到第一個不滿足 k=5 的條件記錄。

對於唯一索引來說,查找到第一條滿足條件的記錄之后,就會立即停止繼續檢索。

這兩者帶來的性能差距是微乎其微的。

首先我們將數據頁從磁盤里面讀出來是讀出整個16kb 的數據頁,那么當我們在找到第一個滿足條件的記錄的時候其實絕大多數情況我們要讀取的下一個記錄也在內存中。即使不在,我們也只需要再讀取一個數據頁,並且我記得數據頁之間是有指針直接可以取到下一個數據頁位置的。這個優化進一步優化了讀取連續數據頁的性能,可以認為這樣的操作成本很低。

 

更新過程

為了說明普通索引和唯一索引對更新語句性能的影響,我們先來普及一下 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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM