MySQL -- 普通索引與唯一索引
- 維護一個市民系統,有一個欄位為身份證號
- 業務程式碼能保證不會寫入兩個重複的身份證號(如果業務無法保證,可以依賴資料庫的唯一索引來進行約束)
- 常用SQL查詢語句:
SELECT name FROM CUser WHERE id_card = 'XXX'
- 建立索引
- 身份證號比較大,不建議設定為主鍵
- 從 效能 角度出發,選擇 普通索引 還是 唯一索引 ?
假設欄位k上的值都不重複
查詢過程
- 查詢語句:
SELECT id FROM T WHERE k=5
- 查詢過程
- 通過B+樹從 樹根 開始, 按層搜尋到葉子節點 ,即上圖中右下角的資料頁
- 在 資料頁內部 通過 二分法 來定位具體的記錄
- 針對 普通索引
- 查詢滿足條件的第一個記錄
(5,500)
,然後查詢下一個記錄,直到找到第一個不滿足k=5
的記錄
- 查詢滿足條件的第一個記錄
- 針對 唯一索引
- 由於索引定義了 唯一性 ,查詢到第一個滿足條件的記錄後,就會停止繼續查詢
效能差異
- 效能差異: 微乎其微
- InnoDB的資料是按照 資料頁 為單位進行讀寫的,預設為16KB
- 當需要讀取一條記錄時,並不是將這個記錄本身從磁碟讀出來,而是以資料頁為單位進行讀取的
- 當找到k=5的記錄時,它所在的資料頁都已經在 記憶體 裡了
- 對於 普通索引 而言,只需要多一次 指標尋找 和多一次 計算 – CPU消耗很低
- 如果k=5這個記錄恰好是所在資料頁的最後一個記錄,那麼如果要取下一個記錄,就需要讀取 下一個資料頁
- 概率很低 :對於 整型欄位 索引,一個數據頁(16KB,compact格式)可以存放大概745個值
change buffer
- 當需要 更新一個數據頁 時,如果資料頁 在記憶體中 就 直接更新
- 如果這個資料頁 不在記憶體中 ,在不影響 資料一致性 的前提下
- InnoDB會將這些 更新操作 快取在change buffer
- 不需要從磁碟讀入這個資料頁 ( 隨機讀 )
- 在 下次查詢 需要訪問這個資料頁的時候, 將資料頁讀入記憶體
- 然後執行change buffer中與這個資料頁有關的操作(merge)
- change buffer是可以 持久化 的資料,在記憶體中有拷貝,也會被寫入到磁碟上
- 將更新操作先記錄在channge buffer, 減少隨機讀磁碟 ,提升語句的執行速度
- 另外資料頁讀入記憶體需要佔用buffer pool,使用channge buffer能避免佔用記憶體, 提高記憶體利用率
- change buffer用到是buffer pool裡的記憶體,不能無限增大,控制引數
innodb_change_buffer_max_size
# 預設25,最大50 mysql> SHOW VARIABLES LIKE '%innodb_change_buffer_max_size%'; +-------------------------------+-------+ | Variable_name| Value | +-------------------------------+-------+ | innodb_change_buffer_max_size | 25| +-------------------------------+-------+
merge
- merge:將change buffer中的操作 應用 到原資料頁
- merge的執行過程
- 從磁碟讀入資料頁到記憶體(老版本的資料頁)
- 從change buffer裡找出這個資料頁的change buffer記錄(可能多個)
- 然後 依次執行 ,得到 新版本的資料頁
- 寫入redolog,包含內容: 資料頁的表更 + change buffer的變更
- merge執行完後,記憶體中的資料頁和change buffer所對應的磁碟頁都還沒修改,屬於 髒頁
- 通過其他機制,髒頁會被重新整理到對應的物理磁碟頁
- 觸發時機
- 訪問這個資料頁
- 系統後臺執行緒 定期merge
- 資料庫 正常關閉
使用條件
- 對於 唯一索引 來說,所有的更新操作需要先判斷這個操作 是否違反唯一性約束
- 唯一索引的更新無法使用change buffer,只有普通索引可以使用change buffer
- 主鍵也是無法使用change buffer的
- 例如要插入
(4,400)
,必須先判斷表中是否存在k=4的記錄,這個判斷的前提是 將資料頁讀入記憶體 - 既然資料頁已經讀入到了記憶體,直接更新記憶體中的資料頁就好,無需再寫change buffer
使用場景
- 一個數據頁在 merge之前 ,change buffer 記錄關於這個資料頁的變更越多 , 收益越大
- 對於 寫多讀少 的業務,頁面在寫完後馬上被訪問的概率極低,此時 change buffer的使用效果最好
- 例如賬單類、日誌類的系統
- 如果一個業務的更新模式為: 寫入之後馬上會做查詢
- 雖然更新操作被記錄到change buffer,但之後馬上查詢,又會 從磁碟讀取 資料頁,觸發merge過程
- 沒有減少隨機讀,反而增加了維護change buffer的代價
更新過程
插入(4,400)
目標頁在記憶體中
- 對於 唯一索引 來說,找到3~5之間的位置, 判斷沒有衝突 ,插入這個值
- 對於 普通索引 來說,找到3~5之間的位置,插入這個值
- 效能差異: 微乎其微
目標頁不在記憶體中
- 對於 唯一索引 來說,需要 將資料頁讀入記憶體 , 判斷沒有衝突 ,插入這個值
- 磁碟隨機讀 ,成本很高
- 對於 普通索引 來說, 將更新操作記錄在change buffer 即可
- 減少了磁碟隨機讀 ,效能提升明顯
索引選擇
- 普通索引與唯一索引,在查詢效能上並沒有太大差異,主要考慮的是 更新效能 , 推薦選擇普通索引
- 建議 關閉change buffer 的場景
- 如果所有的更新後面,都伴隨著對這個記錄的查詢
- 控制引數
innodb_change_buffering
mysql> SHOW VARIABLES LIKE '%innodb_change_buffering%'; +-------------------------+-------+ | Variable_name| Value | +-------------------------+-------+ | innodb_change_buffering | all| +-------------------------+-------+ # Valid Values (>= 5.5.4) none / inserts / deletes / changes / purges / all # Valid Values (<= 5.5.3) none / inserts # change buffer的前身是insert buffer,只能對insert操作進行優化
change buffer + redolog
更新過程
當前k樹的狀態:找到對應的位置後,k1所在的資料頁 Page 1在記憶體中 ,k2所在的資料頁 Page 2不在記憶體中
INSERT INTO t(id,k) VALUES (id1,k1),(id2,k2);
# 記憶體:buffer pool # redolog:ib_logfileX # 資料表空間:t.ibd # 系統表空間:ibdata1
- Page 1在記憶體中,直接更新記憶體
- Page 2不在記憶體中,在changer buffer中記錄:
add (id2,k2) to Page 2
- 上述兩個動作計入redolog( 磁碟順序寫 )
- 至此事務完成,執行更新語句的成本很低
- 寫兩次記憶體+一次磁碟
- 由於在事務提交時,會把change buffer的操作記錄也記錄到redolog
- 因此可以在 崩潰恢復 時,恢復change buffer
- 虛線為 後臺操作 ,不影響更新操作的響應時間
讀過程
假設:讀語句發生在更新語句後不久, 記憶體中的資料都還在 ,與系統表空間(ibdata1)和redolog(ib_logfileX)無關
SELECT * FROM t WHERE k IN (k1,k2);
- 讀Page 1, 直接從記憶體返回 (此時Page 1有可能還是 髒頁 ,並未真正落盤)
- 讀Page 2,通過 磁碟隨機讀 將資料頁讀入記憶體,然後應用change buffer裡面的操作日誌( merge )
- 生成一個正確的版本並返回
提升更新效能
- redolog :節省 隨機寫 磁碟的IO消耗(順序寫)
- change buffer :節省 隨機讀 磁碟的IO消耗
參考資料
《MySQL實戰45講》
轉載請註明出處:http://zhongmingmao.me/2019/01/29/mysql-index-unique-common/
訪問原文「 MySQL -- 普通索引與唯一索引 」獲取最佳閱讀體驗並參與討論