MySQL唯一索引和普通索引選哪個(gè)?
想象這樣一個(gè)場(chǎng)景,在設(shè)計(jì)一張用戶表時(shí),每人的身份證號(hào)是唯一的,需要搜索。但由于身份證號(hào)字段較大,不好將其作為主鍵。在業(yè)務(wù)代碼已經(jīng)保證插入身份證唯一的情況下,可以選擇建立唯一索引和普通索引,這時(shí)該如何選擇呢?接下來(lái),將從查詢和更新的執(zhí)行過(guò)程進(jìn)行分析。
查詢過(guò)程
假設(shè) k 是表 t 上的索引,在搜索 select id from t where k=5 時(shí),會(huì)先從 k 這棵 B+ 的樹(shù)根開(kāi)始,按層搜索葉子節(jié)點(diǎn),找到 k=5 的數(shù)據(jù)頁(yè),然后在數(shù)據(jù)頁(yè)內(nèi)容進(jìn)行二分法定位。
對(duì)于普通索引,找到 k=5 的記錄后,會(huì)繼續(xù)向下查找一個(gè),直到碰到第一個(gè)不是 5 的記錄結(jié)束。
對(duì)于唯一索引,由于取值唯一,找到后直接停止。
由于 InnoDB 是按照數(shù)據(jù)頁(yè)為單位(數(shù)據(jù)頁(yè)默認(rèn) 16 KB)進(jìn)行讀寫的,在讀取一條數(shù)據(jù)時(shí),會(huì)將整個(gè)數(shù)據(jù)頁(yè)整體讀到內(nèi)存。 在讀入內(nèi)存的數(shù)據(jù)頁(yè)中,如果包含 k=5 的記錄,在查詢的情況下,唯一索引比普通索引多了一次查找和判斷的過(guò)程,可以忽略。
如果 k=5 是當(dāng)前數(shù)據(jù)頁(yè)的最后一條,就需要在讀取下一個(gè)數(shù)據(jù)頁(yè)。但這發(fā)生的概率較低,也可以忽略。
所以總得來(lái)說(shuō),普通索引和唯一索引在查詢的過(guò)程中差異不大。
change buffer
在分析唯一索引和普通索引的影響前,先來(lái)認(rèn)識(shí)一下 change buffer 這個(gè)結(jié)構(gòu)。
什么是 change buffer ?
在執(zhí)行更新操作時(shí),如果要更新的數(shù)據(jù)頁(yè)在內(nèi)存中就直接更新,否則的話,在不影響數(shù)據(jù)一致性的前提下,InnoDB 會(huì)將更新操作緩存在 change buffer 中,從而省去了從磁盤讀取數(shù)據(jù)頁(yè)的過(guò)程。在下次查詢操作讀取到恰好需要更新的數(shù)據(jù)頁(yè)時(shí),會(huì)將 change buffer 的更新語(yǔ)句執(zhí)行,寫入數(shù)據(jù)頁(yè)。將操作應(yīng)用到硬盤的過(guò)程叫 merge. 后臺(tái)線程會(huì)定期 merge 或 數(shù)據(jù)庫(kù)正常關(guān)閉時(shí),也會(huì)進(jìn)行 merge 操作。
merge 的執(zhí)行流程:
- 從磁盤讀入老版本數(shù)據(jù)頁(yè)。
- 從 change buffer中找出和該數(shù)據(jù)頁(yè)關(guān)聯(lián)的記錄,依次應(yīng)用,得到新版數(shù)據(jù)頁(yè)。
- 寫 redo log,記錄數(shù)據(jù)的變更和 change buffer 的變更。
change buffer 實(shí)際上是可以持久化到硬盤中的數(shù)據(jù),也就是說(shuō)在內(nèi)存和硬盤上都 change buffer 的存在。change buffer 之前叫 insert buffer,開(kāi)始只對(duì) insert buffer 有優(yōu)化,后來(lái)加上了對(duì) delete 和 update 的支持,進(jìn)而改名叫 change buffer。
可以看到,先將更新操作記錄在 change buffer,減少了將磁盤數(shù)據(jù)頁(yè)讀取到內(nèi)存的過(guò)程,語(yǔ)句的執(zhí)行速度會(huì)有很明顯的提升。同時(shí),將數(shù)據(jù)讀入內(nèi)存,會(huì)占用 buffer pool 內(nèi)存,所以減少讀操作,還提高了內(nèi)存使用率。
Buffer Pool 是內(nèi)存中的一個(gè)區(qū)域,InnoDB 在訪問(wèn)表和索引數(shù)據(jù)時(shí)會(huì)在其中進(jìn)行緩存。允許在內(nèi)存中直接更新經(jīng)常使用的數(shù)據(jù),來(lái)加快處理速度。在一些專用的服務(wù)器上,會(huì)將 80% 的物理內(nèi)存分為 buffer pool.
可以通過(guò) innodb_change_buffer_max_size 來(lái)設(shè)置 change buffer 占用 buffer pool 的大小。
change buffer 應(yīng)用場(chǎng)景?
如上面提到,change buffer 預(yù)先保存了更新記錄,減少了讀取數(shù)據(jù)頁(yè)的過(guò)程,從而提高性能。也就是說(shuō)如果 change buffer 中針對(duì)不同的數(shù)據(jù)頁(yè)如果包含的更新記錄越多,其實(shí)收益也就越大。
因此對(duì)于寫多讀少的業(yè)務(wù)(更新完立即查詢)change buffer 發(fā)揮的作用也就越大。如常見(jiàn)的賬單類,日志類等系統(tǒng)。
如果業(yè)務(wù)是更新完立即查詢,雖然可以將更新記錄放在 change buffer 中,但由于之后要馬上查詢數(shù)據(jù)頁(yè),所以會(huì)立即觸發(fā) merge 過(guò)程。這樣隨機(jī)訪問(wèn) IO 次數(shù)并不會(huì)減少,反而增加了 change buffer 的維護(hù)代價(jià),起到反效果。
更新過(guò)程
對(duì)于唯一索引來(lái)說(shuō),所有的更新操作都需要判斷是否違反唯一性約束。所以必須把所需要的數(shù)據(jù)頁(yè)讀入內(nèi)存,然后直接更新就可以,不需要使用 change buffer. 所以 change buffer 只對(duì)普通索引有用。
具體分析下,對(duì)于一張表插入一個(gè)新記錄:
如果新記錄要更新的數(shù)據(jù)頁(yè)在內(nèi)存中:
對(duì)于唯一索引,找到合適的位置,判斷有沒(méi)有沖突,插入值,語(yǔ)句結(jié)束。
對(duì)于普通索引:找到位置,插入值,語(yǔ)句結(jié)束。
所以數(shù)據(jù)頁(yè)在內(nèi)存時(shí),唯一和普通索引就差一個(gè)判斷的過(guò)程??梢院雎?。
如果新記錄要更新的數(shù)據(jù)頁(yè)不在內(nèi)存中:
對(duì)于唯一索引,將數(shù)據(jù)頁(yè)讀入內(nèi)存,判斷沖突,插入,語(yǔ)句結(jié)束。
對(duì)于普通索引,將語(yǔ)句記錄在 change buffer 中,語(yǔ)句結(jié)束。
由于從磁盤到內(nèi)存涉及隨機(jī) IO 訪問(wèn),是數(shù)據(jù)庫(kù)成本最高的操作之一。普通索引比唯一索引減少的讀入操作,可以有很好的性能提升。
唯一或普通索引的選擇
通過(guò)在查詢和更新方面,兩者的比較。我們知道,在查詢過(guò)程中,除了極特殊情況,其實(shí)兩者的差異并不大。
主要的差異是在更新過(guò)程中,要更新的數(shù)據(jù)頁(yè)并不在內(nèi)容中的情況。這時(shí)唯一索引,由于需要唯一性檢查,不能利用 change buffer. 多了從磁盤到內(nèi)容讀取數(shù)據(jù)的過(guò)程,其中涉及隨機(jī) IO 的訪問(wèn),相對(duì)來(lái)說(shuō)效率就低了。
所以如果業(yè)務(wù)需要更新不錯(cuò)的性能,這時(shí)可以選用普通索引。當(dāng)然一切都是建立在能保證數(shù)據(jù)準(zhǔn)確性的前提下。
當(dāng)如果更新后來(lái)緊接著查詢操作,可以考慮關(guān)掉 change buffer. 其他的情況,change buffer 都能有很好的提升。
特別針對(duì)機(jī)械硬盤,change buffer 效果很顯著。
redo log 和 change buffer 的比較
InnoDB 中 redo log 的出現(xiàn)使其具有了 crash-safe 的能力,同時(shí)還提高了效率,通過(guò) WAL 先寫日志,再寫磁盤。
而 change buffer 是節(jié)省了從磁盤讀入數(shù)據(jù)頁(yè)到內(nèi)存的隨機(jī)IO過(guò)程。
下面通過(guò)一條插入語(yǔ)句來(lái)分析下兩者間的關(guān)系:
mysql> insert into t(id,k) values(id1,k1),(id2,k2);
假設(shè) k 為普通索引,k1 所插入的數(shù)據(jù)頁(yè)在內(nèi)存中, k2 不在。

執(zhí)行插入操作時(shí),主要涉及了圖中這四部分的內(nèi)容:
InnoDB buffer pool:內(nèi)存區(qū)域
redo log:日志
system table space(ibdata1):系統(tǒng)表空間
data(t.idb): 數(shù)據(jù)表空間
innodb_file_per_table 開(kāi)啟時(shí),表被創(chuàng)建在獨(dú)立的表空間下,否則的話被創(chuàng)建在系統(tǒng)的表空間下。
執(zhí)行過(guò)程如下:
- k1 所在的 page1 在內(nèi)存中,直接更新內(nèi)存
- k2 所在的 page2 不在內(nèi)存中,記錄在 change buffer.
- 將 k1 和 k2 的操作記錄在 redo log.
- 提交事務(wù)。
可以看到這條更新語(yǔ)句(包括插入,刪除,更新操作)執(zhí)行成本很低,兩次寫入內(nèi)存,1次順序?qū)懭氪疟P。虛線的操作,是后臺(tái)操作,不影響響應(yīng)時(shí)間。
再來(lái)看一條查詢語(yǔ)句:
select * from t where k in (k1, k2)
假設(shè)讀語(yǔ)句發(fā)生在更新語(yǔ)句不久,內(nèi)存數(shù)據(jù)還在,此時(shí)讀操作就和系統(tǒng)表空間和 redo log 無(wú)關(guān)。

執(zhí)行過(guò)程:
- 讀取 k1 所在的 page1,在內(nèi)存中,直接返回。注意,并沒(méi)有讀磁盤上的數(shù)據(jù),而且磁盤上的數(shù)據(jù)還有可能是之前的版本的。
- 讀取 k2 所在的 page2,這時(shí)需要將 page2 從磁盤加載到內(nèi)存,并應(yīng)用 change buffer 的內(nèi)容,然后返回正確的結(jié)果。從這里也能看出,change buffer 不適用于更新完立馬去讀的情況。
總結(jié)下 redo log 和 change buffer 的關(guān)系:
存儲(chǔ)位置:change buffer 也會(huì)持久化在硬盤里,但保存在系統(tǒng)表空間 ibdata1 里。而 redo log 是單獨(dú)的文件。
記錄內(nèi)容:change buffer 記錄的是更新操作的內(nèi)容,而 redo log 記錄的是普通數(shù)據(jù)頁(yè)的修改和 change buffer 的改動(dòng)。
同步磁盤過(guò)程:同步內(nèi)存中數(shù)據(jù)頁(yè)的修改時(shí)通過(guò) merge 操作進(jìn)行的,而不是根據(jù) redo log.
從更新的過(guò)程來(lái)看: redo log 將隨機(jī)寫磁盤的 IO 轉(zhuǎn)換成了順序?qū)懀?change buffer 則是節(jié)省了隨機(jī)讀磁盤的 IO 消耗。
如果服務(wù)器異常掉電,會(huì)不會(huì)導(dǎo)致 change buffer 丟失?
并不會(huì),因?yàn)?change buffer 中的數(shù)據(jù)已經(jīng)被記錄到 redo log 中,所以不會(huì)丟失。
由于 change buffer 一部分?jǐn)?shù)據(jù)在磁盤,一部分在內(nèi)存。對(duì)于在磁盤的數(shù)據(jù)已經(jīng) merge 所以不會(huì)丟失。
對(duì)于在內(nèi)存中的數(shù)據(jù):
- 如果 change buffer 寫入,但 redo log 未提交,binlog 未提交,事務(wù)會(huì)回滾,這部分?jǐn)?shù)據(jù)不存在。
- 如果 change buffer 寫入,redo log 寫入,binlog 寫入,并已提交,不會(huì)丟失。從 redo log 直接恢復(fù)。
- 如果 change buffer 寫入,redo log 寫入但未 commit,binlog 寫入,從 binlog 恢復(fù) redo log 再恢復(fù) change buffer.
參考資料
以上就是MySQL唯一索引和普通索引選哪個(gè)?的詳細(xì)內(nèi)容,更多關(guān)于MySQL唯一索引和普通索引的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
MySQL與PHP的基礎(chǔ)與應(yīng)用專題之索引
MySQL是一個(gè)關(guān)系型數(shù)據(jù)庫(kù)管理系統(tǒng),由瑞典MySQL?AB?公司開(kāi)發(fā),屬于?Oracle?旗下產(chǎn)品。MySQL?是最流行的關(guān)系型數(shù)據(jù)庫(kù)管理系統(tǒng)之一,本系列將帶你掌握php與mysql的基礎(chǔ)應(yīng)用,本篇從索引開(kāi)始2022-02-02
從底層簡(jiǎn)析Python程序的執(zhí)行過(guò)程
這篇文章主要介紹了從底層簡(jiǎn)析Python程序的執(zhí)行過(guò)程,包括注入操作碼和封裝程序等解釋器執(zhí)行層面的知識(shí),需要的朋友可以參考下2015-06-06
mysql重裝后出現(xiàn)亂碼設(shè)置為utf8可解決
mysql重裝后出現(xiàn)亂碼解決辦法:只能在配置文件中將database 和 server 字符集 設(shè)置為utf8 ,否則不起作用,具體如下感興趣的朋友可以參考下哈,希望對(duì)大家有所幫助2013-07-07
CentOS Mysql數(shù)據(jù)庫(kù)如何實(shí)現(xiàn)定時(shí)備份
這篇文章主要介紹了CentOS Mysql數(shù)據(jù)庫(kù)如何實(shí)現(xiàn)定時(shí)備份,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-06-06
詳解記錄MySQL中l(wèi)ower_case_table_names的坑
這篇文章主要介紹了詳解記錄MySQL中l(wèi)ower_case_table_names的坑,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-03-03
解決Can''t locate ExtUtils/MakeMaker.pm in @INC報(bào)錯(cuò)
今天小編就為大家分享一篇關(guān)于解決Can't locate ExtUtils/MakeMaker.pm in @INC報(bào)錯(cuò),小編覺(jué)得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來(lái)看看吧2019-01-01
mysql基礎(chǔ)架構(gòu)教程之查詢語(yǔ)句執(zhí)行的流程詳解
這篇文章主要給大家介紹了關(guān)于mysql基礎(chǔ)架構(gòu)教程之查詢語(yǔ)句執(zhí)行流程的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起看看吧2018-11-11
Ubuntu20下MySQL?8.0.28?安裝卸載方法圖文教程
這篇文章主要為大家詳細(xì)介紹了Ubuntu20下MySQL?8.0.28?安裝卸載方法圖文教程,文中安裝步驟介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-04-04

