09 | 普通索引和惟一索引,應該怎麼選擇?

今天的正文開始前,我要特地感謝一下評論區幾位留下高質量留言的同窗。mysql

用戶名是 @某、人 的同窗,對文章的知識點作了梳理,而後提了關於事務可見性的問題,就是先啓動可是後提交的事務,對數據可見性的影響。@夏日雨同窗也提到了這個問題,我在置頂評論中回覆了,今天的文章末尾也會再展開說明。@Justin和@倪大人兩位同窗提了兩個好問題。sql

對於可以引起更深一步思考的問題,我會在回覆的內容中寫上「好問題」三個字,方便你搜索,你也能夠去看看他們的留言。數據庫

很是感謝你們很細緻地看文章,而且留下了那麼多和很高質量的留言。知道文章有給你們帶來一些新理解,對我來講是一個很好的鼓勵。同時,也讓其餘認真看評論區的同窗,有機會發現一些本身尚未意識到的、但可能還不清晰的知識點,這也在整體上提升了整個專欄的質量。再次謝謝大家。數組

好了,如今就回到咱們今天的正文內容。緩存

在前面的基礎篇文章中,我給你介紹過索引的基本概念,相信你已經瞭解了惟一索引和普通索引的區別。今天咱們就繼續來談談,在不一樣的業務場景下,應該選擇普通索引,仍是惟一索引?session

假設你在維護一個市民系統,每一個人都有一個惟一的身份證號,並且業務代碼已經保證了不會寫入兩個重複的身份證號。若是市民系統須要按照身份證號查姓名,就會執行相似這樣的SQL語句:性能

select name from CUser where id_card = 'xxxxxxxyyyyyyzzzzz';

因此,你必定會考慮在id_card字段上建索引。學習

因爲身份證號字段比較大,我不建議你把身份證號當作主鍵,那麼如今你有兩個選擇,要麼給id_card字段建立惟一索引,要麼建立一個普通索引。若是業務代碼已經保證了不會寫入重複的身份證號,那麼這兩個選擇邏輯上都是正確的。優化

如今我要問你的是,從性能的角度考慮,你選擇惟一索引仍是普通索引呢?選擇的依據是什麼呢?spa

簡單起見,咱們仍是用第4篇文章《深刻淺出索引(上)》中的例子來講明,假設字段 k 上的值都不重複。

圖1 InnoDB的索引組織結構

接下來,咱們就從這兩種索引對查詢語句和更新語句的性能影響來進行分析。

查詢過程

假設,執行查詢的語句是 select id from T where k=5。這個查詢語句在索引樹上查找的過程,先是經過B+樹從樹根開始,按層搜索到葉子節點,也就是圖中右下角的這個數據頁,而後能夠認爲數據頁內部經過二分法來定位記錄。

  • 對於普通索引來講,查找到知足條件的第一個記錄(5,500)後,須要查找下一個記錄,直到碰到第一個不知足k=5條件的記錄。
  • 對於惟一索引來講,因爲索引定義了惟一性,查找到第一個知足條件的記錄後,就會中止繼續檢索。

那麼,這個不一樣帶來的性能差距會有多少呢?答案是,微乎其微。

你知道的,InnoDB的數據是按數據頁爲單位來讀寫的。也就是說,當須要讀一條記錄的時候,並非將這個記錄自己從磁盤讀出來,而是以頁爲單位,將其總體讀入內存。在InnoDB中,每一個數據頁的大小默認是16KB。

由於引擎是按頁讀寫的,因此說,當找到k=5的記錄的時候,它所在的數據頁就都在內存裏了。那麼,對於普通索引來講,要多作的那一次「查找和判斷下一條記錄」的操做,就只須要一次指針尋找和一次計算。

固然,若是k=5這個記錄恰好是這個數據頁的最後一個記錄,那麼要取下一個記錄,必須讀取下一個數據頁,這個操做會稍微複雜一些。

可是,咱們以前計算過,對於整型字段,一個數據頁能夠放近千個key,所以出現這種狀況的機率會很低。因此,咱們計算平均性能差別時,仍能夠認爲這個操做成本對於如今的CPU來講能夠忽略不計。

更新過程

爲了說明普通索引和惟一索引對更新語句性能的影響這個問題,我須要先跟你介紹一下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由於減小了隨機磁盤訪問,因此對更新性能的提高是會很明顯的。

以前我就碰到過一件事兒,有個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的配合使用,對於數據量大的表的更新優化仍是很明顯的。

特別地,在使用機械硬盤時,change buffer這個機制的收效是很是顯著的。因此,當你有一個相似「歷史數據」的庫,而且出於成本考慮用的是機械硬盤時,那你應該特別關注這些表裏的索引,儘可能使用普通索引,而後把change buffer 儘可能開大,以確保這個「歷史數據」表的數據寫入速度。

change buffer 和 redo log

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

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

備註:這裏,你能夠再回顧下第2篇文章《日誌系統:一條SQL更新語句是如何執行的?》中的相關內容。

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

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

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

圖2 帶change buffer的更新過程

分析這條更新語句,你會發現它涉及了四個部分:內存、redo log(ib_log_fileX)、 數據表空間(t.ibd)、系統表空間(ibdata1)。

這條更新語句作了以下的操做(按照圖中的數字順序):

  1. Page 1在內存中,直接更新內存;

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

  3. 將上述兩個動做記入redo log中(圖中3和4)。

作完上面這些,事務就能夠完成了。因此,你會看到,執行這條更新語句的成本很低,就是寫了兩處內存,而後寫了一處磁盤(兩次操做合在一塊兒寫了一次磁盤),並且仍是順序寫的。

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

那在這以後的讀請求,要怎麼處理呢?

好比,咱們如今要執行 select * from t where k in (k1, k2)。這裏,我畫了這兩個讀請求的流程圖。

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

圖3 帶change buffer的讀過程

從圖中能夠看到:

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

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

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

因此,若是要簡單地對比這兩個機制在提高更新性能上的收益的話,redo log 主要節省的是隨機寫磁盤的IO消耗(轉成順序寫),而change buffer主要節省的則是隨機讀磁盤的IO消耗。

小結

今天,我從普通索引和惟一索引的選擇開始,和你分享了數據的查詢和更新過程,而後說明了change buffer的機制以及應用場景,最後講到了索引選擇的實踐。

因爲惟一索引用不上change buffer的優化機制,所以若是業務能夠接受,從性能角度出發我建議你優先考慮非惟一索引。

最後,又到了思考題時間。

經過圖2你能夠看到,change buffer一開始是寫內存的,那麼若是這個時候機器掉電重啓,會不會致使change buffer丟失呢?change buffer丟失可不是小事兒,再從磁盤讀入數據可就沒有了merge過程,就等因而數據丟失了。會不會出現這種狀況呢?

你能夠把你的思考和觀點寫在留言區裏,我會在下一篇文章的末尾和你討論這個問題。感謝你的收聽,也歡迎你把這篇文章分享給更多的朋友一塊兒閱讀。

補充:
評論區你們對「是否使用惟一索引」有比較多的討論,主要是糾結在「業務可能沒法確保」的狀況。這裏,我再說明一下:

  • 首先,業務正確性優先。我們這篇文章的前提是「業務代碼已經保證不會寫入重複數據」的狀況下,討論性能問題。若是業務不能保證,或者業務就是要求數據庫來作約束,那麼沒得選,必須建立惟一索引。這種狀況下,本篇文章的意義在於,若是碰上了大量插入數據慢、內存命中率低的時候,能夠給你多提供一個排查思路。
  • 而後,在一些「歸檔庫」的場景,你是能夠考慮使用惟一索引的。好比,線上數據只須要保留半年,而後歷史數據保存在歸檔庫。這時候,歸檔數據已是確保沒有惟一鍵衝突了。要提升歸檔效率,能夠考慮把表裏面的惟一索引改爲普通索引。

上期問題時間

上期的問題是:如何構造一個「數據沒法修改」的場景。評論區裏已經有很多同窗給出了正確答案,這裏我再描述一下。


這樣,session A看到的就是我截圖的效果了。

其實,還有另一種場景,同窗們在留言區都尚未提到。

這個操做序列跑出來,session A看的內容也是可以復現我截圖的效果的。這個session B’啓動的事務比A要早,實際上是上期咱們描述事務版本的可見性規則時留的彩蛋,由於規則裏還有一個「活躍事務的判斷」,我是準備留到這裏再補充的。

當我試圖在這裏講述完整規則的時候,發現第8篇文章《事務究竟是隔離的仍是不隔離的?》中的解釋引入了太多的概念,以至於分析起來很是複雜。

所以,我重寫了第8篇,這樣咱們人工去判斷可見性的時候,纔會更方便。【看到這裏,我建議你可以再從新打開第8篇文章並認真學習一次。若是學習的過程當中,有任何問題,也歡迎你給我留言】

用新的方式來分析session B’的更新爲何對session A不可見就是:在session A視圖數組建立的瞬間,session B’是活躍的,屬於「版本未提交,不可見」這種狀況。

業務中若是要繞過這類問題,@約書亞提供了一個「樂觀鎖」的解法,你們能夠去上一篇的留言區看一下。

相關文章
相關標籤/搜索