你分得清MySQL普通索引和唯一索引了嗎? 1 示例

走過路過不要錯過

點擊藍字關注我們

0 概念區分


普通索引和唯一索引


普通索引可以重複,唯一索引和主鍵一樣不能重複。


唯一索引可以作爲數據的一個合法驗證手段,例如學生表的身份證號碼字段,我們人爲規定該字段不得重複,那麼就使用唯一索引。

(一般設置學號字段爲主鍵)

主鍵和唯一索引


主鍵保證數據庫裏面的每一行都是唯一的,比如身份證,學號等,在表中要求唯一,不重複。

唯一索引的作用跟主鍵的作用一樣。


不同的是,在一張表裏面只能有一個主鍵,主鍵不能爲空,唯一索引可以有多個,唯一索引可以有一條記錄爲空,即保證跟別人不一樣就行。


比如學生表,在學校裏面一般用學號做主鍵,身份證則弄成唯一索引;

而到了教育局,他們就把身份證號弄成主鍵,學號換成了唯一索引。


選誰做表的主鍵,要看實際應用,主鍵不能爲空。

1 示例

一個市民系統,每個人都有個唯一身份證號;


業務代碼已保證不會寫入兩個重複的身份證號;


如果市民系統需要按照身份證號查姓名,就會執行類似SQL:

select name from CUser where id_card = 'xxxxxxxyyyyyyzzzzz';

相信你一定會在id_card字段上建索引。

由於身份證號字段比較大,不建推薦把身份證號做主鍵。
因此現在有兩個選擇

給id_card字段創建唯一索引

創建一個普通索引

如果業務代碼已保證不會寫入重複的身份證號,那這兩個選擇邏輯上都正確。

但從性能角度考慮,唯一索引還是普通索引呢?
假設字段 k 上的值都不重複。

InnoDB的索引組織結構

查詢語句

select id from T where k=5

該語句在索引樹查找的過程:
先通過B+樹從樹根開始,按層搜索到葉節點,即圖中右下角的數據頁,然後可認爲數據頁內部是通過二分法定位記錄。

對普通索引,查找到滿足條件的第一個記錄(5,500)後,需查找下個記錄,直到碰到第一個不滿足k=5條件的記錄

對唯一索引,由於索引定義了唯一性,查找到第一個滿足條件的記錄後,就會停止檢索。

該不同點帶來的性能差距會有多少呢?
微乎其微!

InnoDB數據是按數據頁爲單位讀寫。即當需讀一條記錄時,並非將該記錄本身從磁盤讀出,而是以頁爲單位,將其整體讀入內存。

InnoDB中,每個數據頁的大小默認是16KB。

因引擎按頁讀寫,所以,當找到k=5記錄時,它所在數據頁就都在內存了。
對普通索引,要多做的那一次“查找和判斷下一條記錄”的操作,就只需要一次指針尋找和一次計算。
如果k=5記錄剛好是該數據頁的最後一個記錄,那麼要取下個記錄,必須讀取下個數據頁,操作會稍微複雜。
對於整型字段,一個數據頁可存近千個key,因此這種情況概率很低。所以,計算平均性能差異時,仍可認爲該操作成本對現在的CPU可忽略不計。

需更新一個數據頁時

若數據頁在內存,直接更新

若該數據頁不在內存,在不影響數據一致性前提下,InooDB會將這些更新操作緩存在change buffer,無需從磁盤讀入該數據頁。


在下次查詢需要訪問該數據頁時,將數據頁讀入內存,然後執行change buffer中與這個頁有關的操作。

通過該方式就能保證這個數據邏輯的正確性。

雖然叫change buffer,實際上是可持久化的數據。
即change buffer在內存中有拷貝,也會被寫進磁盤。

將change buffer中的操作應用到原數據頁,得到最新結果的過程。

訪問該數據頁會觸發merge
系統有後臺線程會定期merge
在數據庫正常關閉(shutdown)的過程中,也會執行merge。

若能將更新操作先記錄在change buffer,減少讀盤,語句執行速度會明顯提升。
且數據讀入內存需要佔用buffer pool,所以該方式還能避免佔用內存,提高內存利用率。

對於唯一索引,所有更新操作要先判斷該操作是否違反唯一性約束。

比如,要插入(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處理流程。

分情況討論該記錄要更新的目標頁是否在內存中:

唯一索引


找到3和5之間位置,判斷到沒有衝突,插入值,語句執行結束。

普通索引


找到3和5之間位置,插入值,語句執行結束。


普通索引和唯一索引對更新語句性能影響的差別,只是一個判斷,只會耗費微小CPU時間。

唯一索引


需要將數據頁讀入內存,判斷到沒有衝突,插入值,語句執行結束

普通索引


將更新記錄在change buffer,語句執行結束

將數據從磁盤讀入內存涉及隨機IO訪問,是數據庫裏面成本最高操作之一。
change buffer因減少隨機磁盤訪問,所以對更新性能提升明顯。

問題案例:某業務的庫內存命中率突然從99%降低到了75%,整個系統處於阻塞狀態,更新語句全部堵住。
探究其原因,發現該業務有大量插入數據操作,而DBA在前天把其中的某個普通索引改成了唯一索引。

普通索引的所有場景,使用change buffer都可加速嗎?

因爲merge纔是真正進行數據更新時刻;
change buffer主要目的是將記錄的變更動作緩存下來;
所以在一個數據頁做merge前,change buffer記錄變更越多(即該數據頁上要更新的次數越多),收益越大。

對寫多讀少業務,頁面在寫完後馬上被訪問到的概率較小,change buffer使用效果最好。該類業務模型常見爲賬單、日誌類的系統。

反之,假設一業務的更新模式是寫後馬上查詢,那麼即使滿足條件,將更新先記錄在change buffer,但之後由於馬上要訪問該數據頁,立即觸發merge。
這樣隨機訪問IO的次數不會減少,反而增加change buffer維護代價。
所以,對於這種業務模式,change buffer起副作用。

普通索引和唯一索引如何抉擇。
這兩類索引在查詢性能上沒差別,主要考慮對更新性能影響。
所以,推薦儘量選擇普通索引。

如果所有更新後面,都緊跟對該記錄的查詢,那麼該關閉change buffer。
而在其他情況下,change buffer都能提升更新性能。
普通索引和change buffer的配合使用,對於數據量大的表的更新優化還是很明顯的。

在使用機械硬盤時,change buffer機制的收效非常顯著。
所以,當你有一個類似“歷史數據”的庫,並且出於成本考慮用機械硬盤時,應該關注這些表裏的索引,儘量使用普通索引,把change buffer 開大,確保“歷史數據”表的數據寫速度。

WAL 提升性能的核心機制,也是儘量減少隨機讀寫,這兩個概念易混淆。
所以,這裏我把它們放到了同一個流程裏來說明區分。

在表上

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

假設當前k索引樹的狀態,查找到位置後
k1所在數據頁在內存(InnoDB buffer pool),k2所在的數據頁不在內存中

帶change buffer的更新狀態圖。

該更新語句涉及四部分:

內存

redo log(ib_log_fileX)

數據表空間(t.ibd)

系統表空間(ibdata1)

該更新語句做了如下操作(按圖中數字順序):

Page1在內存,直接更新內存

Page2沒有在內存中,就在內存的change buffer區,記錄下“我要往Page2插一行”的信息

將前兩個動作記入redo log(圖中的3和4)

做完上面,事務完成。執行這條更新語句的成本很低,就寫兩處內存,然後寫一處磁盤(兩次操作合在一起寫了一次磁盤),還是順序寫。

圖中兩個虛箭,是後臺操作,不影響更新的響應時間。

這之後的讀請求,怎麼處理?
現在執行

select * from t where k in (k1, k2)

若讀語句緊隨在更新語句後,內存中的數據都還在,那麼此時這倆讀操作就與系統表空間(ibdata1)和 redo log(ib_log_fileX)無關。所以在圖中就沒畫這倆。

兩個讀請求的流程圖(帶change buffer的讀過程)

從圖中可見:
讀Page1時,直接從內存返回。
WAL之後如果讀數據,是不是一定要讀盤,是不是一定要從redo log裏面把數據更新以後纔可以返回?其實不用。
看上圖狀態,雖然磁盤上還是之前數據,但這裏直接從內存返回結果,結果正確。

要讀Page2時,需把Page2從磁盤讀入內存,然後應用change buffer裏面的操作日誌,生成一個正確版本並返回結果。
可見直到需讀Page2時,該數據頁才被讀入內存。

所以,要簡單對比這倆機制對更新性能影響

redo log 主要節省隨機寫磁盤的IO消耗(轉成順序寫)

change buffer主要節省隨機讀磁盤的IO消耗

由於唯一索引用不了change buffer的優化機制,因此如果業務可以接受,從性能角度,推薦優先考慮非唯一索引。

主要糾結在“業務可能無法確保”。本文前提是“業務代碼已經保證不會寫入重複數據”下,討論性能問題。

如果業務不能保證,或者業務就是要求數據庫來做約束,那麼沒得選,必須創建唯一索引。

這種情況下,本文意義在於,如果碰上大量插入數據慢、內存命中率低時,多提供一個排查思路。

然後,在一些“歸檔庫”的場景,可考慮使用唯一索引的。比如,線上數據只需保留半年,然後歷史數據保存在歸檔庫。此時,歸檔數據已是確保沒有唯一鍵衝突。要提高歸檔效率,可考慮把表的唯一索引改普通索引。

參考並整理自《MySQL 實戰 45 講》

往期推薦

大廠Java面試題解(45)-來設計個高併發系統?

一看就懂的MySQL行鎖

一看就懂的圖文講解事務隔離

使用私有構造器或枚舉來強化單例屬性吧

好文!點個好看!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章