MySQL系列:普通索引、唯一索引如何選擇


1目錄

  • 唯一索引和普通索引如何選擇
  • 這兩種索引對 查詢語句更新語句的性能影響
  • change buffer 的使用場景
  • change buffer 和 redo log
  • MySQL8.0 取消 Query Cache

2繼續

不同業務場景下唯一索引和普通索引如何選擇?

假設維護一個市民系統,唯一標識用身份證號,且業務代碼已經保證了不會寫入兩個重復的身份證號。如果市民系統需要按照身份證號查詢姓名:

select name from aaaqi where id_card = 'xxxxxxxyyyyyyzzzzz';

由於身份證號字段比較大,不建議把身份證號當做主鍵, 那么現在要么給 id_card 字段創建唯一索引,要么創建普通索引。如果業務代碼已經保證了不會寫入重復的身份證號,那么這兩個選擇邏輯上都是正確的。

如果從性能的角度去考慮,選擇依據是怎樣的。

用之前MySQL 系列:索引(上)中的例子來說明,假設字段 k 上的值都不重復。

InnoDB 的索引組織結構
InnoDB 的索引組織結構

下面用這兩種索引對查詢語句和更新語句的性能影響來進行分析。

3查詢過程

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

  • 對於 普通索引來說,查找到滿足條件的第一個記錄(5,500)后,需要查找下一個記錄,直到碰到第一個不滿足 k=5 條件的記錄。
  • 對於 唯一索引來說,由於索引定義了唯一性,查找到第一個滿足條件的記錄后,就會停止繼續檢索。

上面的不同帶來的性能差距微乎其微。

innodb 的數據是按數據頁為單位來讀寫的。也就是說,當需要讀一條記錄的時候,並不是將這個記錄本身從磁盤讀出來,而是以頁為單位,將其整體讀入內存。在 innodb 中,每個數據頁的大小默認是 16kb。

因為引擎是按頁讀寫的,所以說,當找到 k=5 的記錄的時候,它所在的數據頁就在內存里了。那么,對於普通索引來說,要多做的那一次“查找和判斷嚇一條記錄”的操作,就只需要一次指針尋找和一次計算。

當然,如果 k=5 這個記錄剛好是這個數據頁的最后一個記錄,那么要取下一個記錄,必須讀取下一個數據頁,這個操作會稍微復雜些。

但,對於整型字段,一個數據頁可以放近千個 key,因此出現這種情況的概率會很低。所以,計算平均性能差異時,仍可認為這個操作成本對於現在的 CPU 來說可以忽略不計。

4更新過程

普通索引和唯一索引對更新語句性能的影響問題,這里先介紹下change buffer。

當需要更新一個數據頁時,如果數據頁在內存中就直接更新,而如果這個數據頁還沒有在內存中的話,在不影響數據一致性的前提下,innodb 會將這些更新操作緩存在 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 因為減少了隨機磁盤訪問,索引對更新性能的提升是會更明顯的。

5change buffer 的使用場景

通過上面的分析,清楚了使用change buffer 對更新過程的提升性能作用,也清楚change buffer 只限於普通索引的場景下,而不適用唯一 索引。

但,並不是所有普通索引的場景下 change buffer 都能起到提升性能的作用。

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

所以,對於寫多讀少的業務來說,頁面在寫完以后馬上就被訪問到的概率比較小,此時 change buffer的使用效果最好。 這種常見的業務模型比如,賬單類、日志類的系統。

反過來,如果一個業務的更新模式是寫入后馬上就會做查詢,那么即使滿足了條件,將更新記錄在 change buffer,但之后由於馬上就要被訪問這個數據頁,會立刻觸發 merge 過程。這樣隨機訪問 IO 的次數不會減少,反而會增加 change buffer 的維護代價。 所以,這種業務模式change buffer 會起到副作用。

6索引選擇

回到文章開頭,普通索引和唯一索引應該怎么選擇?其實,這兩類索引在查詢性能上沒差別,主要考慮的是對更新性能的影響。所以,建議盡量選擇普通索引。

如果所有的更新后面,都馬上又對這個記錄的查詢,那么應該關閉 change buffer 而在其他情況下,change buffer 都能提升更新性能。

在實際使用中會發現普通索引和 change buffer 的配合使用,對於數量大的表更新優化還是很明顯的。

特別是在使用機械硬盤時,change buffer 這個機制的收益是非常明顯的。所以,當你有一個類似 歷史數據 的庫,並且處於成本考慮用的是機械硬盤的時候,那應該特別關注這些表里的所有,盡量使用普通索引,然后把 change buffer 盡量開大,以確保這個庫里面的表數據寫入速度。

7change buffer 和 redo log

change buffer 、redo log 、 WAL 是不是感覺都差不多,忘了的可以看之前 MySQL 系列:一條更新語句是如何執行的的內容。

舉個栗子
舉個栗子

現在,要在表上執行下面插入語句:

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

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

如下圖 A 所示是帶 change buffer 的更新狀態圖:

圖A:帶 change buffer 的更新過程
圖A:帶 change buffer 的更新過程

這條更新語句一共涉及了 4 個部分:內存、redo log(ib_log_fileX)、數據表空間(t.ibd)、系統表空間(ibdata1)。

這條更新語句做了如下操作(按圖中順序):

  1. page1 在內存中,直接更新內存;
  2. page2 沒有在內存中,就在內存的 change buffer 區域,記錄下“往 page2 插入一行”這個信息;
  3. 將上述兩個動作記入 redo log 中,圖中 3 和 4。

做完上面這些,事務就可以完成了。所以會看到執行這條更新語句的成本很低,就是寫了 2 處內存,然后寫了 1 處磁盤(2 次操作合在一起寫了 1 次磁盤),而且還是順序寫的。

圖中 2 個虛箭頭是后台操作,不影響響應時間。

在這之后的讀請求是這樣處理的。

假設,現在要執行select * from aaaqi where k in(k1,k2)。這里,下面有個讀請求的流程圖。

如果讀語句發生在更新語句后不久, 內存中的數據都還在,那么此時 2 個讀請操作就與系統表空間(ibdata1)和 redo log (ib_log_fileX)無關了。所以下圖沒有這 2 部分:

圖B: 帶 change buffer 的讀過程
圖B: 帶 change buffer 的讀過程

上圖可以看到如下:

  • 讀 page1 時,直接從內存返回。雖然磁盤上還是之前的數據,但是這里直接從內存返回結果,結果是正確的。
  • 讀 page2 時,需要把 page2 從磁盤讀入內存中,再應用 change buffer 里面的操作日志,生成一個正確的版本並返回結果。

可以看到,直到需要讀 page2 時,這個數據頁才會被讀入內存。

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

8MySQL為什么取消了Query Cache?

有興趣的可以復制下面鏈接看。

這貨沒留原文鏈接《MySQL為什么取消了Query Cache? 》:https://cloud.tencent.com/developer/article/1693427

MySQL8.0 新特性:https://www.cnblogs.com/songgj/p/10658916.html

MySQL8.0 官網地址:https://dev.mysql.com/doc/refman/8.0/en/added-deprecated-removed.html#optvars-deprecated

MySQL8.0官網 移除清單
MySQL8.0官網 移除清單
MySQL8.0 官網 移除清單
MySQL8.0 官網 移除清單


免責聲明!

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



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