今天的正文開始前,我要特地感謝一下評論區幾位留下高質量留言的同窗。用戶名是 @某、人 的同窗,對文章的知識點作了梳理,而後提了關於事務可見性的問
題,就是先啓動可是後提交的事務,對數據可見性的影響。@夏日雨同窗也提到了這個問題,我在置頂評論中回覆了,今天的文章末尾也會再展開說明。@Justin 和 @倪大人兩位
同窗提了兩個好問題。mysql
對於可以引起更深一步思考的問題,我會在回覆的內容中寫上「好問題」三個字,方便你搜索,你也能夠去看看他們的留言。sql
很是感謝你們很細緻地看文章,而且留下了那麼多和很高質量的留言。知道文章有給你們帶來一些新理解,對我來講是一個很好的鼓勵。同時,也讓其餘認真看評論區的同窗,有
機會發現一些本身尚未意識到的、但可能還不清晰的知識點,這也在整體上提升了整個專欄的質量。再次謝謝大家。數據庫
好了,如今就回到咱們今天的正文內容。數組
在前面的基礎篇文章中,我給你介紹過索引的基本概念,相信你已經瞭解了惟一索引和普通索引的區別。今天咱們就繼續來談談,在不一樣的業務場景下,應該選擇普通索引,仍是惟一索引?緩存
假設你在維護一個市民系統,每一個人都有一個惟一的身份證號,並且業務代碼已經保證了不會寫入兩個重複的身份證號。若是市民系統須要按照身份證號查姓名,就會執行相似這
樣的 SQL 語句:bash
select name from CUser where id_card = 'xxxxxxxyyyyyyzzzzz';
因此,你必定會考慮在 id_card 字段上建索引。session
因爲身份證號字段比較大,我不建議你把身份證號當作主鍵,那麼如今你有兩個選擇,要麼給 id_card 字段建立惟一索引,要麼建立一個普通索引。若是業務代碼已經保證了不會
寫入重複的身份證號,那麼這兩個選擇邏輯上都是正確的。性能
如今我要問你的是,從性能的角度考慮,你選擇惟一索引仍是普通索引呢?選擇的依據是什麼呢?學習
簡單起見,咱們仍是用第 4 篇文章《深刻淺出索引(上)》中的例子來講明,假設字段 k上的值都不重複。優化
圖 1 InnoDB 的索引組織結構
接下來,咱們就從這兩種索引對查詢語句和更新語句的性能影響來進行分析。
假設,執行查詢的語句是 select id from T where k=5。這個查詢語句在索引樹上查找的過程,先是經過 B+ 樹從樹根開始,按層搜索到葉子節點,也就是圖中右下角的這個數據
頁,而後能夠認爲數據頁內部經過二分法來定位記錄。
你知道的,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 的,因此這種方式還可以避免佔用內存,提升內存利用率。
對於惟一索引來講,全部的更新操做都要先判斷這個操做是否違反惟一性約束。好比,要插入 (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 的處理流程是怎樣的。
這樣看來,普通索引和惟一索引對更新語句性能影響的差異,只是一個判斷,只會耗費微小的 CPU 時間。
但,這不是咱們關注的重點。
將數據從磁盤讀入內存涉及隨機 IO 的訪問,是數據庫裏面成本最高的操做之一。changebuffer 由於減小了隨機磁盤訪問,因此對更新性能的提高是會很明顯的。
以前我就碰到過一件事兒,有個 DBA 的同窗跟我反饋說,他負責的某個業務的庫內存命中率忽然從 99% 下降到了 75%,整個系統處於阻塞狀態,更新語句所有堵住。而探究其
緣由後,我發現這個業務有大量插入數據的操做,而他在前一天把其中的某個普通索引改爲了惟一索引。
經過上面的分析,你已經清楚了使用 change buffer 對更新過程的加速做用,也清楚了change buffer 只限於用在普通索引的場景下,而不適用於惟一索引。那麼,如今有一個
問題就是:普通索引的全部場景,使用 change buffer 均可以起到加速做用嗎?
由於 merge 的時候是真正進行數據更新的時刻,而 change buffer 的主要目的就是將記錄的變動動做緩存下來,因此在一個數據頁作 merge 以前,change buffer 記錄的變動
越多(也就是這個頁面上要更新的次數越多),收益就越大。
所以,對於寫多讀少的業務來講,頁面在寫完之後立刻被訪問到的機率比較小,此時change buffer 的使用效果最好。這種業務模型常見的就是帳單類、日誌類的系統。
反過來,假設一個業務的更新模式是寫入以後立刻會作查詢,那麼即便知足了條件,將更新先記錄在 change buffer,
但以後因爲立刻要訪問這個數據頁,會當即觸發 merge 過程。
這樣隨機訪問 IO 的次數不會減小,反而增長了 change buffer 的維護代價。
因此,對於這種業務模式來講,change buffer 反而起到了反作用。
回到咱們文章開頭的問題,普通索引和惟一索引應該怎麼選擇。其實,這兩類索引在查詢能力上是沒差異的,主要考慮的是對更新性能的影響。因此,我建議你儘可能選擇普通索引。
若是全部的更新後面,都立刻伴隨着對這個記錄的查詢,那麼你應該關閉 changebuffer。而在其餘狀況下,change buffer 都能提高更新性能。
在實際使用中,你會發現,普通索引和 change buffer 的配合使用,對於數據量大的表的更新優化仍是很明顯的。
特別地,在使用機械硬盤時,change buffer 這個機制的收效是很是顯著的。因此,當你有一個相似「歷史數據」的庫,而且出於成本考慮用的是機械硬盤時,那你應該特別關注
這些表裏的索引,儘可能使用普通索引,而後把 change buffer 儘可能開大,以確保這個「歷史數據」表的數據寫入速度
理解了 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 所在的數據頁在內存 (InnoDBbuffer 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 看的內容也是可以復現我截圖的效果的。這個 sessionB’啓動的事務比 A 要早,實際上是上期咱們描述事務版本的可見性規則時留的彩蛋,由於
規則裏還有一個「活躍事務的判斷」,我是準備留到這裏再補充的。
當我試圖在這裏講述完整規則的時候,發現第 8 篇文章《事務究竟是隔離的仍是不隔離的?》中的解釋引入了太多的概念,以至於分析起來很是複雜。
所以,我重寫了第 8 篇,這樣咱們人工去判斷可見性的時候,纔會更方便。【看到這裏,我建議你可以再從新打開第 8 篇文章並認真學習一次。若是學習的過程當中,有任何問題,
也歡迎你給我留言】
用新的方式來分析 session B’的更新爲何對 session A 不可見就是:在 session A 視
圖數組建立的瞬間,session B’是活躍的,屬於「版本未提交,不可見」這種狀況。
業務中若是要繞過這類問題,@約書亞提供了一個「樂觀鎖」的解法,你們能夠去上一篇的留言區看一下。
a、普通索引,查到知足條件的第一個記錄後,繼續查找下一個記錄,知道第一個不知足條件的記錄
b、惟一索引,因爲索引惟一性,查到第一個知足條件的記錄後,中止檢索
可是,二者的性能差距微乎其微。由於InnoDB根據數據頁來讀寫的。
概念:change buffer
當須要更新一個數據頁,若是數據頁在內存中就直接更新,若是不在內存中,在不影響數據一致性的前提下,InnoDB會將這些更新操做緩存在change buffer中。下次查詢須要訪問這個數據頁的時候,將數據頁讀入內存,而後執行change buffer中的與這個頁有關的操做。
change buffer是能夠持久化的數據。在內存中有拷貝,也會被寫入到磁盤上
purge:將change buffer中的操做應用到原數據頁上,獲得最新結果的過程,成爲purge訪問這個數據頁會觸發purge,系統有後臺線程按期purge,在數據庫正常關閉的過程當中,也會執行purge
change buffer用的是buffer pool裏的內存,change buffer的大小,能夠經過參數innodb_change_buffer_max_size來動態設置。這個參數設置爲50的時候,表示change buffer的大小最多隻能佔用buffer pool的50%。
將數據從磁盤讀入內存涉及隨機IO的訪問,是數據庫裏面成本最高的操做之一。
change buffer 由於減小了隨機磁盤訪問,因此對更新性能的提高很明顯。
在一個數據頁作purge以前,change buffer記錄的變動越多,收益就越大。
對於寫多讀少的業務來講,頁面在寫完之後立刻被訪問到的機率比較小,此時change buffer的使用效果最好。這種業務模型常見的就是帳單類、日誌類的系統。
反過來,假設一個業務的更新模式是寫入以後立刻會作查詢,那麼即便知足了條件,將更新先記錄在change buffer,但以後因爲立刻要訪問這個數據頁,會當即觸發purge過程。
這樣隨機訪問IO的次數不會減小,反而增長了change buffer的維護代價。因此,對於這種業務模式來講,change buffer反而起到了反作用。
索引的選擇和實踐:儘量使用普通索引。redo log主要節省的是隨機寫磁盤的IO消耗(轉成順序寫),而change buffer主要節省的則是隨機讀磁盤的IO消耗。