假設有一張市民表(本篇只需要用其中的name和id_card字段,有興趣的可以翻看“索引”篇,里面有建表語句)
每個人都有一個唯一的身份證號,且業務代碼已經保證不會重復.
由於業務需求,市民需要按身份證查找對應姓名,即執行如下sql
select name from CUser where id_card = 'xxxxxxxyyyyyyzzzzz';
我們自然會想在id_card上建索引。因為該字段較大,一般不建議直接作為主鍵。
於是我們面臨選擇:是建普通索引還是唯一索引——結合前面的背景說明,兩者都能滿足業務需求。
但從性能角度來考慮的話,選哪個?
我們從這兩種索引對查詢語句和更新語句的性能影響來進行分析。
查詢過程
為了簡化這個模型,我們將id_card設為k,k為int型。
執行查詢語句
select name from T where k=5
這個查詢語句在索引樹上查找的過程,先是通過 B+ 樹從樹根開始,按層搜索到葉子節點,
可以認為數據頁內部通過二分法來定位記錄。
- 對於普通索引來說,查找到滿足條件的第一個記錄 (5,'張三') 后,需要查找下一個記錄,直到碰到第一個不滿足 k=5 條件的記錄。
- 對於唯一索引來說,由於索引定義了唯一性,查找到第一個滿足條件的記錄后,就會停止繼續檢索
那么,這個不同帶來的性能差距會有多少呢?答案是,微乎其微。
InnoDB 的數據是按數據頁為單位來讀寫的。當需要讀一條記錄的時候,並不是將這個記錄本身從磁盤讀出來,而是以頁為單位,將其整體讀入內存。在 InnoDB 中,每個數據頁的大小默認是 16KB。
當找到 k=5 的記錄的時候,它所在的數據頁就都在內存里了。對於普通索引來說,要多做的那一次“查找和判斷下一條記錄”的操作,就只需要一次指針尋找和一次計算。
當然,如果 k=5 這個記錄剛好是這個數據頁的最后一個記錄,那么要取下一個記錄,必須讀取下一個數據頁,這個操作會稍微復雜一些。
但是,對於整型字段,一個數據頁可以放近千個 key,因此出現這種情況的概率會很低。
所以,我們計算平均性能差異時,仍可以認為這個操作成本對於現在的 CPU 來說可以忽略不計。
即:對於普通索引和唯一索引來說,查詢性能無差別。
更新過程
為了說明普通索引和唯一索引對更新語句性能的影響這個問題,這里先介紹一下 change buffer。
當需要更新一個數據頁時,如果數據頁在內存中就直接更新,而如果這個數據頁還沒有在內存中的話,在不影響數據一致性的前提下,InnoDB 會將這些更新操作緩存在 change buffer 中,這樣就不需要從磁盤中讀入這個數據頁了。在下次查詢需要訪問這個數據頁的時候,將數據頁讀入內存,然后執行 change buffer 中與這個頁有關的操作。通過這種方式就能保證這個數據邏輯的正確性。
雖然名字叫作 change buffer,實際上它是可以持久化的數據。也就是說,change buffer 在內存中有拷貝,也會被寫入到磁盤上。
將 change buffer 中的操作應用到原數據頁,得到最新結果的過程稱為 merge。除了訪問這個數據頁會觸發 merge 外,系統有后台線程會定期 merge。在數據庫正常關閉(shutdown)的過程中,也會執行 merge 操作。
顯然,如果能夠將更新操作先記錄在 change buffer,減少讀磁盤,語句的執行速度會得到明顯的提升。
而且,數據讀入內存是需要占用 buffer pool 的,所以這種方式還能夠避免占用內存,提高內存利用率。
那么,什么條件下可以使用 change buffer 呢?
對於唯一索引來說,所有的更新操作都要先判斷這個操作是否違反唯一性約束。而這必須要將數據頁讀入內存才能判斷。如果都已經讀入到內存了,那直接更新內存會更快,就沒必要使用 change buffer 了。
因此,唯一索引的更新就不能使用 change buffer,實際上也只有普通索引可以使用。
change buffer 用的是 buffer pool 里的內存,因此不能無限增大。change buffer 的大小,可以通過參數 innodb_change_buffer_max_size 來動態設置。這個參數設置為 50 的時候,表示 change buffer 的大小最多只能占用 buffer pool 的 50%。
了解完change buffer,結合我們的場景,假設現在正准備插入一條(4, '李四'),我們大致可以分為以下兩種情況:
1.這個記錄要更新的目標頁在內存中:
- 對於唯一索引,找到 3 和 5 之間的位置,判斷到沒有沖突,插入這個值,語句執行結束;
- 對於普通索引來說,找到 3 和 5 之間的位置,插入這個值,語句執行結束。
此時,普通索引和唯一索引對更新語句性能影響的差別,只是一個判斷,只會耗費微小的 CPU 時間。可以忽略
2.這個記錄要更新的目標頁不在內存中
- 對於唯一索引,需要將數據頁讀入內存,判斷到沒有沖突,插入這個值,語句執行結束;
- 對於普通索引,則是將更新記錄在 change buffer,語句執行就結束了。
將數據從磁盤讀入內存涉及隨機 IO 的訪問,是數據庫里面成本最高的操作之一。change buffer 因為減少了隨機磁盤訪問,所以對更新性能的提升是會很明顯的。
有個 DBA 的同學反饋說,他負責的某個業務的庫內存命中率突然從 99% 降低到了 75%,整個系統處於阻塞狀態,更新語句全部堵住。探究其原因后,發現這個業務有大量插入數據的操作,而他在前一天把其中的某個普通索引改成了唯一索引。
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 的配合使用,對於數據量大的表的更新優化還是很明顯的。
特別地,在使用機械硬盤時,change buffer 這個機制的收效是非常顯著的。所以,當你有一個類似“歷史數據”的庫,並且出於成本考慮用的是機械硬盤時,那你應該特別關注這些表里的索引,盡量使用普通索引,然后把 change buffer 盡量開大,以確保這個“歷史數據”表的數據寫入速度。
小結:
1.從查詢過程來說,唯一索引和普通索引幾乎無差別
2.從更新過程來說,由於change buffer的加成,當要更新的目標頁不在內存中時,普通索引因為減少隨機 IO 的訪問,對更新性能的提升很明顯
3.大部分場景普通索引都優於唯一索引(在業務代碼保證唯一性的前提下),但如果業務模式是更新完后馬上查詢,此時普通索引+change buffer會起副作用