1、概念
不同的業務場景下,應該選擇普通索引,還是唯一索引?
假設你在維護一個市民系統,每個人都有一個唯一的身份證號,而且業務代碼已經保證了不會寫入兩個重復的身份證號。如果市民系統需要按照身份證號查姓名,就會執行類似這樣的SQL語句:
select name from CUser where id_card = 'xxxxxxxyyyyyyzzzzz';
所以,你一定會考慮在id_card字段上建索引。
由於身份證號字段比較大,我不建議你把身份證號當做主鍵,那么現在你有兩個選擇,要么給id_card字段創建唯一索引,要么創建一個普通索引。如果業務代碼已經保證了不會寫入重復的身份證號,那么這兩個選擇邏輯上都是正確的。
從兩種索引對查詢語句和更新語句的性能影響。
2、查詢過程
假設,執行查詢的語句是 select id from T where k=5。這個查詢語句在索引樹上查找的過程,先是通過B+樹從樹根開始,按層搜索到葉子節點,也就是圖中右下角的這個數據頁,然后可以認為數據頁內部通過二分法來定位記錄。
對於普通索引來說,查找到滿足條件的第一個記錄(5,500)后,需要查找下一個記錄,直到碰到第一個不滿足k=5條件的記錄。
對於唯一索引來說,由於索引定義了唯一性,查找到第一個滿足條件的記錄后,就會停止繼續檢索。
那么,這個不同帶來的性能差距會有多少呢?答案是,微乎其微。
你知道的,InnoDB的數據是按數據頁為單位來讀寫的。也就是說,當需要讀一條記錄的時候,並不是將這個記錄本身從磁盤讀出來,而是以頁為單位,將其整體讀入內存。在InnoDB中,每個數據頁的大小默認是16KB。
因為引擎是按頁讀寫的,所以說,當找到k=5的記錄的時候,它所在的數據頁就都在內存里了。那么,對於普通索引來說,要多做的那一次“查找和判斷下一條記錄”的操作,就只需要一次指針尋找和一次計算。
當然,如果k=5這個記錄剛好是這個數據頁的最后一個記錄,那么要取下一個記錄,必須讀取下一個數據頁,這個操作會稍微復雜一些。
但是,我們之前計算過,對於整型字段,一個數據頁可以放近千個key,因此出現這種情況的概率會很低。所以,我們計算平均性能差異時,仍可以認為這個操作成本對於現在的CPU來說可以忽略不計。
3、更新過程
(1)change buffer
當需要更新一個數據頁時,如果數據頁在內存中就直接更新,而如果這個數據頁還沒有在內存中的話,在不影響數據一致性的前提下,InooDB會將這些更新操作緩存在change buffer中,這樣就不需要從磁盤中讀入這個數據頁了。在下次查詢需要訪問這個數據頁的時候,將數據頁讀入內存,然后執行change buffer中與這個頁有關的操作。通過這種方式就能保證這個數據邏輯的正確性。
需要說明的是,雖然名字叫作change buffer,實際上它是可以持久化的數據。也就是說,change buffer在內存中有拷貝,也會被寫入到磁盤上。
將change buffer中的操作應用到原數據頁,得到最新結果的過程稱為merge。除了訪問這個數據頁會觸發merge外,系統有后台線程會定期merge。在數據庫正常關閉(shutdown)的過程中,也會執行merge操作。
顯然,如果能夠將更新操作先記錄在change buffer,減少讀磁盤,語句的執行速度會得到明顯的提升。而且,數據讀入內存是需要占用buffer pool的,所以這種方式還能夠避免占用內存,提高內存利用率。
那么,什么條件下可以使用change buffer呢?
對於唯一索引來說,所有的更新操作都要先判斷這個操作是否違反唯一性約束。比如,要插入(4,400)這個記錄,就要先判斷現在表中是否已經存在k=4的記錄,而這必須要將數據頁讀入內存才能判斷。如果都已經讀入到內存了,那直接更新內存會更快,就沒必要使用change buffer了。
因此,唯一索引的更新就不能使用change buffer,實際上也只有普通索引可以使用。
change buffer用的是buffer pool里的內存,因此不能無限增大。change buffer的大小,可以通過參數innodb_change_buffer_max_size來動態設置。這個參數設置為50的時候,表示change buffer的大小最多只能占用buffer pool的50%。
現在,你已經理解了change buffer的機制,那么我們再一起來看看如果要在這張表中插入一個新記錄(4,400)的話,InnoDB的處理流程是怎樣的。
第一種情況是,這個記錄要更新的目標頁在內存中。這時,InnoDB的處理流程如下:
對於唯一索引來說,找到3和5之間的位置,判斷到沒有沖突,插入這個值,語句執行結束;
對於普通索引來說,找到3和5之間的位置,插入這個值,語句執行結束。
這樣看來,普通索引和唯一索引對更新語句性能影響的差別,只是一個判斷,只會耗費微小的CPU時間。
但,這不是我們關注的重點。
第二種情況是,這個記錄要更新的目標頁不在內存中。這時,InnoDB的處理流程如下:
對於唯一索引來說,需要將數據頁讀入內存,判斷到沒有沖突,插入這個值,語句執行結束;
對於普通索引來說,則是將更新記錄在change buffer,語句執行就結束了。
將數據從磁盤讀入內存涉及隨機IO的訪問,是數據庫里面成本最高的操作之一。change buffer因為減少了隨機磁盤訪問,所以對更新性能的提升是會很明顯的。
4、change buffer的使用場景
使用change buffer對更新過程的加速作用,也清楚了change buffer只限於用在普通索引的場景下,而不適用於唯一索引。那么,現在有一個問題就是:普通索引的所有場景,使用change buffer都可以起到加速作用嗎?
因為merge的時候是真正進行數據更新的時刻,而change buffer的主要目的就是將記錄的變更動作緩存下來,所以在一個數據頁做merge之前,change buffer記錄的變更越多(也就是這個頁面上要更新的次數越多),收益就越大。
因此,對於寫多讀少的業務來說,頁面在寫完以后馬上被訪問到的概率比較小,此時change buffer的使用效果最好。這種業務模型常見的就是賬單類、日志類的系統。
反過來,假設一個業務的更新模式是寫入之后馬上會做查詢,那么即使滿足了條件,將更新先記錄在change buffer,但之后由於馬上要訪問這個數據頁,會立即觸發merge過程。這樣隨機訪問IO的次數不會減少,反而增加了change buffer的維護代價。所以,對於這種業務模式來說,change buffer反而起到了副作用。
5、索引選擇和實踐.
其實,這兩類索引在查詢能力上是沒差別的,主要考慮的是對更新性能的影響。所以,我建議你盡量選擇普通索引。
如果所有的更新后面,都馬上伴隨着對這個記錄的查詢,那么你應該關閉change buffer。而在其他情況下,change buffer都能提升更新性能。
在實際使用中,你會發現,普通索引和change buffer的配合使用,對於數據量大的表的更新優化還是很明顯的。
特別地,在使用機械硬盤時,change buffer這個機制的收效是非常顯著的。所以,當你有一個類似“歷史數據”的庫,並且出於成本考慮用的是機械硬盤時,那你應該特別關注這些表里的索引,盡量使用普通索引,然后把change buffer 盡量開大,以確保這個“歷史數據”表的數據寫入速度。
6、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的更新狀態圖。
帶有change buffer更新的過程
分析這條更新語句,你會發現它涉及了四個部分:內存、redo log(ib_log_fileX)、 數據表空間(t.ibd)、系統表空間(ibdata1)。
這條更新語句做了如下的操作(按照圖中的數字順序):
Page 1在內存中,直接更新內存;
Page 2沒有在內存中,就在內存的change buffer區域,記錄下“我要往Page 2插入一行”這個信息
將上述兩個動作記入redo log中(圖中3和4)。
做完上面這些,事務就可以完成了。所以,你會看到,執行這條更新語句的成本很低,就是寫了兩處內存,然后寫了一處磁盤(兩次操作合在一起寫了一次磁盤),而且還是順序寫的。
同時,圖中的兩個虛線箭頭,是后台操作,不影響更新的響應時間。
那在這之后的讀請求,要怎么處理呢?
比如,我們現在要執行 select * from t where k in (k1, k2)。這里,我畫了這兩個讀請求的流程圖。
如果讀語句發生在更新語句后不久,內存中的數據都還在,那么此時的這兩個讀操作就與系統表空間(ibdata1)和 redo log(ib_log_fileX)無關了。
帶有change buffer讀取的過程
從圖中可以看到:
讀Page 1的時候,直接從內存返回。有幾位同學在前面文章的評論中問到,WAL之后如果讀數據,是不是一定要讀盤,是不是一定要從redo log里面把數據更新以后才可以返回?其實是不用的。你可以看一下圖3的這個狀態,雖然磁盤上還是之前的數據,但是這里直接從內存返回結果,結果是正確的。
要讀Page 2的時候,需要把Page 2從磁盤讀入內存中,然后應用change buffer里面的操作日志,生成一個正確的版本並返回結果。
可以看到,直到需要讀Page 2的時候,這個數據頁才會被讀入內存。
如果要簡單地對比這兩個機制在提升更新性能上的收益的話,redo log 主要節省的是隨機寫磁盤的IO消耗(轉成順序寫),而change buffer主要節省的則是隨機讀磁盤的IO消耗
7、小結
從普通索引和唯一索引的選擇開始,數據的查詢和更新過程,然后說明了change buffer的機制以及應用場景,最后講到了索引選擇的實踐。
由於唯一索引用不上change buffer的優化機制,因此如果業務可以接受,從性能角度出發建議優先考慮非唯一索引。