Mysql普通索引和惟一索引的選擇分析

假設一個用戶管理系統,每一個人註冊都有一個惟一的手機號,並且業務代碼已經保證了不會寫入兩個重複的手機號。若是用戶管理系統須要按照手機號查姓名,就會執行相似這樣的 SQL 語句:mysql

select name from users where mobile = '15202124529';sql

一般會考慮在 mobile 字段上建索引。因爲手機號字段相對較大,一般基本不會把手機號當作主鍵,那麼如今就有兩個選擇:數據庫

1.  給 id_card 字段建立惟一索引
2.  建立一個普通索引

若是業務代碼已經保證了不會寫入重複的身份證號,那麼這兩個選擇邏輯上都是正確的。數組

從性能的角度考慮,選擇惟一索引仍是普通索引?

如圖:假設字段 k 上的值都不重複
image緩存

接下來,就從這兩種(ID,k)索引對查詢語句和更新語句的性能影響來進行分析異步

查詢過程

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

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

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

緣由:除非 Key 的列很是大,有連續多個 Key 佔滿了一個 page,纔會引發一次 page 的 IO,這樣纔會產生比較明顯的性能差別,從均攤上看,差別幾乎能夠不算。spa

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

更新過程

爲了說明普通索引和惟一索引對更新語句性能的影響這個問題,須要先介紹一下 change buffer

  • 當須要更新一個數據頁時,若是數據頁在內存中就直接更新,
  • 而若是這個數據頁尚未在內存中的話,在不影響數據一致性的前提下:
  1.  InnoDB 會將這些 更新操做 緩存在 change buffer 中,這樣就不須要從磁盤中讀入這個數據頁了。
  2. 在下次查詢須要訪問這個數據頁的時候,將數據頁讀入內存,
  3. 而後執行 change buffer 中與這個頁有關的操做。

    經過這種方式就能保證這個數據邏輯的正確性

須要說明的是,雖然名字叫做 change buffer,實際上它是能夠持久化的數據。也就是說,change buffer 在內存中有拷貝,也會被寫入到磁盤上。

把change buffer中的操做,應用到舊的數據頁,獲得新的數據頁的過程,應該稱爲merge。

Ps.  除了訪問這個數據頁會觸發 merge 外,系統有後臺線程會按期 merge。在數據庫正常關閉(shutdown)的過程當中,也會執行 merge 操做。

(change buffer的merge操做,先把change buffer的操做更新到內存的數據頁中,此操做寫到redo log中,mysql未宕機,redo log寫滿後須要移動check point點時,經過判斷內存中數據和磁盤是否一致便是否是髒頁來刷新到磁盤中,當mysql宕機後沒有內存即沒有髒頁,經過redo log來恢復。)

顯然,若是可以將更新操做先記錄在 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%。

image

Ps. 數據庫緩衝池(buffer pool) https://www.jianshu.com/p/f9ab1cb24230

分析:插入一個新記錄 InnoDB 的處理流程

理解了 change buffer 的機制,那麼若是要在這張表中插入一個新記錄 (4,400) 的話,InnoDB 的處理流程是怎樣的

一、第一種狀況是:這個記錄要更新的目標頁在內存中。

  •  這時,InnoDB 的處理流程以下:對於惟一索引來講,找到 3 和 5 之間的位置,判斷到沒有衝突,插入這個值,語句執行結束;
  • 對於普通索引來講,找到 3 和 5 之間的位置,插入這個值,語句執行結束。

這樣看來,普通索引和惟一索引對更新語句性能影響的差異,只是一個判斷,只會耗費微小的 CPU 時間。但,這不是關注的重點

二、第二種狀況是,這個記錄要更新的目標頁不在內存中。這時,InnoDB 的處理流程以下:

  • 對於惟一索引來講,須要將數據頁讀入內存,判斷到沒有衝突,插入這個值,語句執行結束;
  • 對於普通索引來講,則是將更新記錄在 change buffer,語句執行就結束了。

將數據從磁盤讀入內存涉及隨機 IO 的訪問,是數據庫裏面成本最高的操做之一。change buffer 由於減小了隨機磁盤訪問,因此對更新性能的提高是會很明顯的。

change buffer主要是將更新操做緩存起來,異步處理. 這樣每次更新過來,直接記下change buffer便可,速度很快,將屢次寫磁盤變爲一次寫磁盤

change buffer 的使用場景

經過上面的分析,已經清楚了使用 change buffer 對更新過程的加速做用,也清楚了 change buffer 只限於用在普通索引的場景下,而不適用於惟一索引。

普通索引的全部場景,使用 change buffer 均可以起到加速做用嗎?

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

所以,對於寫多讀少的業務來講,頁面在寫完之後立刻被訪問到的機率比較小,此時 change buffer 的使用效果最好。這種業務模型常見的就是帳單類、日誌類的系統。(適合寫多讀少的場景,讀多寫少反倒會增長change buffer的維護代價)

反過來,假設一個業務的更新模式是寫入以後立刻會作查詢,那麼即便知足了條件,將更新先記錄在 change buffer,但以後因爲立刻要訪問這個數據頁,會當即觸發 merge 過程。這樣隨機訪問 IO 的次數不會減小,反而增長了 change buffer 的維護代價。因此,對於這種業務模式來講,change buffer 反而起到了反作用。(若是當即對普通索引的更新操做結果執行查詢,就會觸發merge操做,磁盤中的數據會和change buffer 的操做記錄進行合併,產生大量io)

索引選擇和實踐

綜上分析,普通索引和惟一索引應該怎麼選擇:

其實,這兩類索引在查詢能力上是沒差異的,主要考慮的是對更新性能的影響。因此,建議儘可能選擇普通索引

若是全部的更新後面,都立刻伴隨着對這個記錄的查詢,那麼應該關閉 change buffer。

而在其餘狀況下,change buffer 都能提高更新性能。在實際使用中,普通索引和 change buffer 的配合使用,對於數據量大的表的更新優化仍是很明顯的。

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

change buffer 和 redo log

理解了 change buffer 的原理,可能會聯想到 redo log 和 WAL(Write-Ahead Logging,它的關鍵點就是先寫日誌,再寫磁盤)。

WAL 提高性能的核心機制,也的確是儘可能減小隨機讀寫

在表上執行這個插入語句:

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

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

image

圖3  帶 change buffer 的更新過程

分析這條更新語句,你會發現它涉及了四個部分:

內存、redo log(ib_log_fileX)、 數據表空間(t.ibd)、系統表空間(ibdata1)。

數據表空間:就是一個個的表數據文件,對應的磁盤文件就是「表名.ibd」; 系統表空間:用來放系統信息,如數據字典等,對應的磁盤文件是「ibdata1」

數據表空間 和 系統表空間 彷佛表明的就是B+樹對應的那個複雜的結構

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

  1. Page 1 在內存中,直接更新內存;
  2. Page 2 沒有在內存中,就在內存的 change buffer 區域,記錄下「我要往 Page 2 插入一行」
  3. 這個信息將上述兩個動做記入 redo log 中(圖中 3 和 4)。

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

change buffer和redo log顆粒度不同,由於change buffer只是針對若是更改的數據所在頁不在內存中才暫時儲存在change buffer中。而redo log會記錄一個事務內進行數據更改的全部操做,即便修改的數據已經在內存中了,那也會記錄下來

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

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

好比,咱們如今要執行 select * from t where k in (k1, k2)。

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

image

圖 4 帶 change buffer 的讀過程

從圖中能夠看到:讀 Page 1 的時候,直接從內存返回。

WAL 以後若是讀數據,是否是必定要讀盤,是否是必定要從 redo log 裏面把數據更新之後才能夠返回?

實際上是不用的。雖然磁盤上仍是以前的數據,可是這裏直接從內存返回結果,結果是正確的。要讀 Page 2 的時候,須要把 Page 2 從磁盤讀入內存中,而後應用 change buffer 裏面的操做日誌,生成一個正確的版本並返回結果。能夠看到,直到須要讀 Page 2 的時候,這個數據頁纔會被讀入內存。

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

思考題:

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

答:

1.change buffer有一部分在內存有一部分在ibdata.

作purge操做,應該就會把change buffer裏相應的數據持久化到ibdata

2.redo log裏記錄了數據頁的修改以及change buffer新寫入的信息

若是掉電,持久化的change buffer數據已經purge,不用恢復。主要分析沒有持久化的數據

狀況又分爲如下幾種:

(1)change buffer寫入,redo log雖然作了fsync但未commit,binlog未fsync到磁盤,這部分數據丟失

(2)change buffer寫入,redo log寫入但沒有commit,binlog以及fsync到磁盤,先從binlog恢復redo log,再從redo log恢復change buffer

(3)change buffer寫入,redo log和binlog都已經fsync.那麼直接從redo log裏恢復。

相關文章
相關標籤/搜索