MySQL 普通索引和唯一索引的區別詳解
1 概念區分
- 普通索引和唯一索引
普通索引可重復,唯一索引和主鍵一樣不能重復。 唯一索引可作為數據的一個合法驗證手段,例如學生表的身份證號碼字段,我們人為規定該字段不得重復,那麼就使用唯一索引。(一般設置學號字段為主鍵)
- 主鍵和唯一索引
主鍵保證數據庫裡面的每一行都是唯一的,比如身份證,學號等,在表中要求唯一,不重復。唯一索引的作用跟主鍵的作用一樣。 不同的是,在一張表裡面隻能有一個主鍵,主鍵不能為空,唯一索引可以有多個,唯一索引可以有一條記錄為空,即保證跟別人不一樣就行。 比如學生表,在學校裡面一般用學號做主鍵,身份證則弄成唯一索引;而到瞭教育局,他們就把身份證號弄成主鍵,學號換成瞭唯一索引。 選誰做表的主鍵,要看實際應用,主鍵不能為空。
2 案例引入
某居民系統,每人有唯一身份證號。如果系統需要按身份證號查姓名,就會執行類似如下SQL:
select name from CUser where id_card = 'ooxx';
然後你肯定會在id_card字段建索引。但id_card字段較大,不推薦將其做主鍵。於是現有倆選擇:
- 給id_card字段創建唯一索引
- 創建一個普通索引
假定業務代碼已保證不會寫入重復的身份證號,這兩個選擇邏輯上都正確。但從性能角度考慮,唯一索引還是普通索引呢?
再看如下案例:假設字段 k 上的值都不重復。
- InnoDB的索引組織結構:
接下來分析性能。
3 查詢性能
select id from T where k=4
通過B+樹從樹根開始層序遍歷到葉節點,可認為數據頁內部是通過二分法搜索。
- 普通索引,查找到滿足條件的第一個記錄(4,400)後,需查找下個記錄,直到碰到第一個不滿足k=4的記錄
- 唯一索引,由於索引具備唯一性,查找到第一個滿足條件的記錄後,就會停止檢索
看起來性能差距很微小。
InnoDB數據按數據頁單位讀寫。即讀一條記錄時,並非將該一個記錄從磁盤讀出,而以頁為單位,將其整體讀入內存。
因此普通索引,要多做一次“查找和判斷下一條記錄”的操作,也就一次指針尋找和一次計算。 如果k=4記錄恰為該數據頁最後一個記錄,那麼要取下個記錄,還得讀取下個數據頁,操作稍微復雜。 對整型字段,一個數據頁可存近千key,因此這種情況概率其實也很低。因此計算平均性能差異時,可認為該操作成本對現在CPU開銷忽略不計。
我們知道 MySQL 有 change buffer。
4 更新性能
現在來看往表中插入一個新記錄(4,400),InnoDB會做什麼?
需要區分該記錄要更新的目標頁是否在內存:
4.1 在內存
- 唯一索引
找到3和5之間位置,判斷到沒有沖突,插入值,語句執行結束。
- 普通索引
找到3和5之間位置,插入值,語句執行結束。
普通索引和唯一索引對更新語句性能影響的差別,隻是一個判斷,耗費微小CPU時間。
4.2 不在內存
- 唯一索引
需將數據頁讀入內存,判斷到沒有沖突,插入值,語句執行結束。
- 普通索引
將更新記錄在change buffer,語句執行結束。
將數據從磁盤讀入內存涉及隨機IO訪問,是數據庫裡面成本最高操作之一。而change buffer減少隨機磁盤訪問,所以更新性能提升明顯。
5 實踐中的索引選擇
普通索引和唯一索引究竟如何抉擇?這兩類索引在查詢性能上沒差別,主要考慮對更新性能影響。所以,推薦盡量選擇普通索引。
如果所有更新後面,都緊跟對該記錄的查詢,那麼該關閉change buffer。 而在其他情況下,change buffer都能提升更新性能。 普通索引和change buffer的配合使用,對於數據量大的表的更新優化還是很明顯的。
在使用機械硬盤時,change buffer機制的收效非常顯著。 所以,當你有一個類似“歷史數據”的庫,並且出於成本考慮用機械硬盤時,應該關註這些表裡的索引,盡量使用普通索引,把change buffer 開大,確保“歷史數據”表的數據寫速度。
6 change buffer 和 redo log
WAL 提升性能的核心機制,也是盡量減少隨機讀寫,這兩個概念易混淆。 所以,這裡我把它們放到瞭同一個流程裡來說明區分。
6.1 插入流程
insert into t(id,k) values(id1,k1),(id2,k2);
假設當前k索引樹的狀態,查找到位置後,k1所在數據頁在內存(InnoDB buffer pool),k2數據頁不在內存。
- 帶change buffer的更新流程圖,圖中兩個箭頭都是後臺操作,不影響更新響應。
該更新做瞭如下操作:
- Page1在內存,直接更新內存
- Page2不在內存,就在change buffer區,緩存下“往Page2插一行記錄”的信息
- 將前兩個動作記入redo log
之後事務完成。執行該更新語句成本很低,隻寫兩處內存,然後寫一處磁盤(前兩次操作合在一起寫瞭一次磁盤),還是順序寫。
6.2 怎麼處理之後的讀請求?
select * from t where k in (k1, k2);
讀語句緊隨更新語句,內存中的數據都還在,此時這倆讀操作就與系統表空間和 redo log 無關。所以在圖中就沒畫這倆。
- 帶change buffer的讀過程
讀Page1時,直接從內存返回。 WAL之後如果讀數據,是不是一定要讀盤,是不是一定要從redo log裡面把數據更新以後才可以返回?其實不用。 看上圖狀態,雖然磁盤上還是之前數據,但這裡直接從內存返回結果,結果正確。
要讀Page2時,需把Page2從磁盤讀入內存,然後應用change buffer裡面的操作日志,生成一個正確版本並返回結果。 可見直到需讀Page2時,該數據頁才被讀入內存。
所以,要簡單對比這倆機制對更新性能影響
- redo log 主要節省隨機寫磁盤的IO消耗(轉成順序寫)
- change buffer主要節省隨機讀磁盤的IO消耗
7 總結
由於唯一索引用不瞭change buffer的優化機制,因此如果業務可以接受,從性能角度,推薦優先考慮非唯一索引。
7.1 關於到底是否使用唯一索引
主要糾結在“業務可能無法確保”。本文前提是“業務代碼已經保證不會寫入重復數據”下,討論性能問題。
如果業務不能保證,或者業務就是要求數據庫來做約束,那麼沒得選,必須創建唯一索引。這種情況下,本文意義在於,如果碰上大量插入數據慢、內存命中率低時,多提供一個排查思路。
然後,在一些“歸檔庫”的場景,可考慮使用唯一索引的。比如,線上數據隻需保留半年,然後歷史數據保存在歸檔庫。此時,歸檔數據已是確保沒有唯一鍵沖突。要提高歸檔效率,可考慮把表的唯一索引改普通索引。
7.2 如果某次寫入使用change buffer,之後主機異常重啟,是否會丟失change buffer的數據?
不會丟失。 雖然是隻更新內存,但在事務提交時,我們把change buffer的操作也記錄到redo log,所以崩潰恢復時,change buffer也能找回。
7.3 merge的過程是否會把數據直接寫回磁盤?
merge執行流程
- 從磁盤讀入數據頁到內存(老版本數據頁)
- 從change buffer找出該數據頁的change buffer 記錄(可能有多個),依次應用,得到新版數據頁
- 寫redo log
該redo log包含數據的變更和change buffer的變更
至此merge過程結束。 這時,數據頁和內存中change buffer對應磁盤位置都尚未修改,是臟頁,之後各自刷回自己物理數據,就是另外一過程。
問題思考
在構造第一個例子的過程,通過session A的配合,讓session B刪除數據後又重新插入一遍數據,然後就發現explain結果中,rows字段從10001變成37000多。 而如果沒有session A的配合,隻是單獨執行delete from t 、call idata()、explain這三句話,會看到rows字段其實還是10000左右。這是什麼原因呢?
如果沒有復現,檢查
- 隔離級別是不是RR(Repeatable Read,可重復讀)
- 創建的表t是不是InnoDB引擎
為什麼經過這個操作序列,explain的結果就不對瞭? delete 語句刪掉瞭所有的數據,然後再通過call idata()插入瞭10萬行數據,看上去是覆蓋瞭原來10萬行。 但是,session A開啟瞭事務並沒有提交,所以之前插入的10萬行數據是不能刪除的。這樣,之前的數據每行數據都有兩個版本,舊版本是delete之前數據,新版本是標記deleted的數據。 這樣,索引a上的數據其實有兩份。
然後你會說,不對啊,主鍵上的數據也不能刪,那沒有使用force index的語句,使用explain命令看到的掃描行數為什麼還是100000左右?(潛臺詞,如果這個也翻倍,也許優化器還會認為選字段a作為索引更合適) 是的,不過這個是主鍵,主鍵是直接按照表的行數來估計的。而表的行數,優化器直接用的是show table status
的值。 大傢的機器如果IO能力比較差的話,做這個驗證的時候,可以把innodb_flush_log_at_trx_commit
和 sync_binlog
都設置成0。
以上就是MySQL 普通索引和唯一索引的區別詳解的詳細內容,更多關於MySQL 普通索引和唯一索引的資料請關註WalkonNet其它相關文章!
推薦閱讀:
- MySQL數據庫索引的弊端及合理使用
- mysql中的7種日志小結
- MySQL日志專項之redo log和undo log介紹
- 詳解MySQL中事務的持久性實現原理
- 一文搞懂MySQL持久化和回滾的原理