你分得清MySQL普通索引和唯一索引了嗎?

走過路過不要錯(cuò)過

點(diǎn)擊藍(lán)字關(guān)注我們

0 概念區(qū)分


普通索引和唯一索引


普通索引可以重復(fù),唯一索引和主鍵一樣不能重復(fù)。


唯一索引可以作為數(shù)據(jù)的一個(gè)合法驗(yàn)證手段,例如學(xué)生表的身份證號(hào)碼字段,我們?nèi)藶橐?guī)定該字段不得重復(fù),那么就使用唯一索引。

(一般設(shè)置學(xué)號(hào)字段為主鍵)

主鍵和唯一索引


主鍵保證數(shù)據(jù)庫里面的每一行都是唯一的,比如身份證,學(xué)號(hào)等,在表中要求唯一,不重復(fù)。

唯一索引的作用跟主鍵的作用一樣。


不同的是,在一張表里面只能有一個(gè)主鍵,主鍵不能為空,唯一索引可以有多個(gè),唯一索引可以有一條記錄為空,即保證跟別人不一樣就行。


比如學(xué)生表,在學(xué)校里面一般用學(xué)號(hào)做主鍵,身份證則弄成唯一索引;

而到了教育局,他們就把身份證號(hào)弄成主鍵,學(xué)號(hào)換成了唯一索引。


選誰做表的主鍵,要看實(shí)際應(yīng)用,主鍵不能為空。

1 示例

一個(gè)市民系統(tǒng),每個(gè)人都有個(gè)唯一身份證號(hào);


業(yè)務(wù)代碼已保證不會(huì)寫入兩個(gè)重復(fù)的身份證號(hào);


如果市民系統(tǒng)需要按照身份證號(hào)查姓名,就會(huì)執(zhí)行類似SQL:

select name from CUser where id_card = 'xxxxxxxyyyyyyzzzzz';

相信你一定會(huì)在id_card字段上建索引。

由于身份證號(hào)字段比較大,不建推薦把身份證號(hào)做主鍵。
因此現(xiàn)在有兩個(gè)選擇

給id_card字段創(chuàng)建唯一索引

創(chuàng)建一個(gè)普通索引

如果業(yè)務(wù)代碼已保證不會(huì)寫入重復(fù)的身份證號(hào),那這兩個(gè)選擇邏輯上都正確。

但從性能角度考慮,唯一索引還是普通索引呢?
假設(shè)字段 k 上的值都不重復(fù)。

InnoDB的索引組織結(jié)構(gòu)

查詢語句

select id from T where k=5

該語句在索引樹查找的過程:
先通過B+樹從樹根開始,按層搜索到葉節(jié)點(diǎn),即圖中右下角的數(shù)據(jù)頁,然后可認(rèn)為數(shù)據(jù)頁內(nèi)部是通過二分法定位記錄。

對(duì)普通索引,查找到滿足條件的第一個(gè)記錄(5,500)后,需查找下個(gè)記錄,直到碰到第一個(gè)不滿足k=5條件的記錄

對(duì)唯一索引,由于索引定義了唯一性,查找到第一個(gè)滿足條件的記錄后,就會(huì)停止檢索。

該不同點(diǎn)帶來的性能差距會(huì)有多少呢?
微乎其微!

InnoDB數(shù)據(jù)是按數(shù)據(jù)頁為單位讀寫。即當(dāng)需讀一條記錄時(shí),并非將該記錄本身從磁盤讀出,而是以頁為單位,將其整體讀入內(nèi)存。

InnoDB中,每個(gè)數(shù)據(jù)頁的大小默認(rèn)是16KB。

因引擎按頁讀寫,所以,當(dāng)找到k=5記錄時(shí),它所在數(shù)據(jù)頁就都在內(nèi)存了。
對(duì)普通索引,要多做的那一次“查找和判斷下一條記錄”的操作,就只需要一次指針尋找和一次計(jì)算。
如果k=5記錄剛好是該數(shù)據(jù)頁的最后一個(gè)記錄,那么要取下個(gè)記錄,必須讀取下個(gè)數(shù)據(jù)頁,操作會(huì)稍微復(fù)雜。
對(duì)于整型字段,一個(gè)數(shù)據(jù)頁可存近千個(gè)key,因此這種情況概率很低。所以,計(jì)算平均性能差異時(shí),仍可認(rèn)為該操作成本對(duì)現(xiàn)在的CPU可忽略不計(jì)。

需更新一個(gè)數(shù)據(jù)頁時(shí)

若數(shù)據(jù)頁在內(nèi)存,直接更新

若該數(shù)據(jù)頁不在內(nèi)存,在不影響數(shù)據(jù)一致性前提下,InooDB會(huì)將這些更新操作緩存在change buffer,無需從磁盤讀入該數(shù)據(jù)頁。


在下次查詢需要訪問該數(shù)據(jù)頁時(shí),將數(shù)據(jù)頁讀入內(nèi)存,然后執(zhí)行change buffer中與這個(gè)頁有關(guān)的操作。

通過該方式就能保證這個(gè)數(shù)據(jù)邏輯的正確性。

雖然叫change buffer,實(shí)際上是可持久化的數(shù)據(jù)。
即change buffer在內(nèi)存中有拷貝,也會(huì)被寫進(jìn)磁盤。

將change buffer中的操作應(yīng)用到原數(shù)據(jù)頁,得到最新結(jié)果的過程。

訪問該數(shù)據(jù)頁會(huì)觸發(fā)merge
系統(tǒng)有后臺(tái)線程會(huì)定期merge
在數(shù)據(jù)庫正常關(guān)閉(shutdown)的過程中,也會(huì)執(zhí)行merge。

若能將更新操作先記錄在change buffer,減少讀盤,語句執(zhí)行速度會(huì)明顯提升。
且數(shù)據(jù)讀入內(nèi)存需要占用buffer pool,所以該方式還能避免占用內(nèi)存,提高內(nèi)存利用率。

對(duì)于唯一索引,所有更新操作要先判斷該操作是否違反唯一性約束。

比如,要插入(4,400)記錄,要先判斷表中是否已存k=4記錄,而這必須要將數(shù)據(jù)頁讀入內(nèi)存才能判斷。
如果都已經(jīng)讀入到內(nèi)存,那直接更新內(nèi)存會(huì)更快,就沒必要使用change buffer。
因此,唯一索引的更新就不能使用change buffer,實(shí)際上也只有普通索引可使用。

change buffer用的是buffer pool里的內(nèi)存,因此不能無限增大。
change buffer的大小,可通過參數(shù)innodb_change_buffer_max_size動(dòng)態(tài)設(shè)置。
參數(shù)設(shè)置為50時(shí),表示change buffer的大小最多只能占用buffer pool的50%。

理解了change buffer機(jī)制,看看要在這張表中插入一個(gè)新記錄(4,400),InnoDB處理流程。

分情況討論該記錄要更新的目標(biāo)頁是否在內(nèi)存中:

唯一索引


找到3和5之間位置,判斷到?jīng)]有沖突,插入值,語句執(zhí)行結(jié)束。

普通索引


找到3和5之間位置,插入值,語句執(zhí)行結(jié)束。


普通索引和唯一索引對(duì)更新語句性能影響的差別,只是一個(gè)判斷,只會(huì)耗費(fèi)微小CPU時(shí)間。

唯一索引


需要將數(shù)據(jù)頁讀入內(nèi)存,判斷到?jīng)]有沖突,插入值,語句執(zhí)行結(jié)束

普通索引


將更新記錄在change buffer,語句執(zhí)行結(jié)束

將數(shù)據(jù)從磁盤讀入內(nèi)存涉及隨機(jī)IO訪問,是數(shù)據(jù)庫里面成本最高操作之一。
change buffer因減少隨機(jī)磁盤訪問,所以對(duì)更新性能提升明顯。

問題案例:某業(yè)務(wù)的庫內(nèi)存命中率突然從99%降低到了75%,整個(gè)系統(tǒng)處于阻塞狀態(tài),更新語句全部堵住。
探究其原因,發(fā)現(xiàn)該業(yè)務(wù)有大量插入數(shù)據(jù)操作,而DBA在前天把其中的某個(gè)普通索引改成了唯一索引。

普通索引的所有場景,使用change buffer都可加速嗎?

因?yàn)閙erge才是真正進(jìn)行數(shù)據(jù)更新時(shí)刻;
change buffer主要目的是將記錄的變更動(dòng)作緩存下來;
所以在一個(gè)數(shù)據(jù)頁做merge前,change buffer記錄變更越多(即該數(shù)據(jù)頁上要更新的次數(shù)越多),收益越大。

對(duì)寫多讀少業(yè)務(wù),頁面在寫完后馬上被訪問到的概率較小,change buffer使用效果最好。該類業(yè)務(wù)模型常見為賬單、日志類的系統(tǒng)。

反之,假設(shè)一業(yè)務(wù)的更新模式是寫后馬上查詢,那么即使?jié)M足條件,將更新先記錄在change buffer,但之后由于馬上要訪問該數(shù)據(jù)頁,立即觸發(fā)merge。
這樣隨機(jī)訪問IO的次數(shù)不會(huì)減少,反而增加change buffer維護(hù)代價(jià)。
所以,對(duì)于這種業(yè)務(wù)模式,change buffer起副作用。

普通索引和唯一索引如何抉擇。
這兩類索引在查詢性能上沒差別,主要考慮對(duì)更新性能影響。
所以,推薦盡量選擇普通索引。

如果所有更新后面,都緊跟對(duì)該記錄的查詢,那么該關(guān)閉change buffer。
而在其他情況下,change buffer都能提升更新性能。
普通索引和change buffer的配合使用,對(duì)于數(shù)據(jù)量大的表的更新優(yōu)化還是很明顯的。

在使用機(jī)械硬盤時(shí),change buffer機(jī)制的收效非常顯著。
所以,當(dāng)你有一個(gè)類似“歷史數(shù)據(jù)”的庫,并且出于成本考慮用機(jī)械硬盤時(shí),應(yīng)該關(guān)注這些表里的索引,盡量使用普通索引,把change buffer 開大,確?!皻v史數(shù)據(jù)”表的數(shù)據(jù)寫速度。

WAL 提升性能的核心機(jī)制,也是盡量減少隨機(jī)讀寫,這兩個(gè)概念易混淆。
所以,這里我把它們放到了同一個(gè)流程里來說明區(qū)分。

在表上

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

假設(shè)當(dāng)前k索引樹的狀態(tài),查找到位置后
k1所在數(shù)據(jù)頁在內(nèi)存(InnoDB buffer pool),k2所在的數(shù)據(jù)頁不在內(nèi)存中

帶change buffer的更新狀態(tài)圖。

該更新語句涉及四部分:

內(nèi)存

redo log(ib_log_fileX)

數(shù)據(jù)表空間(t.ibd)

系統(tǒng)表空間(ibdata1)

該更新語句做了如下操作(按圖中數(shù)字順序):

Page1在內(nèi)存,直接更新內(nèi)存

Page2沒有在內(nèi)存中,就在內(nèi)存的change buffer區(qū),記錄下“我要往Page2插一行”的信息

將前兩個(gè)動(dòng)作記入redo log(圖中的3和4)

做完上面,事務(wù)完成。執(zhí)行這條更新語句的成本很低,就寫兩處內(nèi)存,然后寫一處磁盤(兩次操作合在一起寫了一次磁盤),還是順序?qū)憽?/p>

圖中兩個(gè)虛箭,是后臺(tái)操作,不影響更新的響應(yīng)時(shí)間。

這之后的讀請(qǐng)求,怎么處理?
現(xiàn)在執(zhí)行

select * from t where k in (k1, k2)

若讀語句緊隨在更新語句后,內(nèi)存中的數(shù)據(jù)都還在,那么此時(shí)這倆讀操作就與系統(tǒng)表空間(ibdata1)和 redo log(ib_log_fileX)無關(guān)。所以在圖中就沒畫這倆。

兩個(gè)讀請(qǐng)求的流程圖(帶change buffer的讀過程)

從圖中可見:
讀Page1時(shí),直接從內(nèi)存返回。
WAL之后如果讀數(shù)據(jù),是不是一定要讀盤,是不是一定要從redo log里面把數(shù)據(jù)更新以后才可以返回?其實(shí)不用。
看上圖狀態(tài),雖然磁盤上還是之前數(shù)據(jù),但這里直接從內(nèi)存返回結(jié)果,結(jié)果正確。

要讀Page2時(shí),需把Page2從磁盤讀入內(nèi)存,然后應(yīng)用change buffer里面的操作日志,生成一個(gè)正確版本并返回結(jié)果。
可見直到需讀Page2時(shí),該數(shù)據(jù)頁才被讀入內(nèi)存。

所以,要簡單對(duì)比這倆機(jī)制對(duì)更新性能影響

redo log 主要節(jié)省隨機(jī)寫磁盤的IO消耗(轉(zhuǎn)成順序?qū)懀?/p>

change buffer主要節(jié)省隨機(jī)讀磁盤的IO消耗

由于唯一索引用不了change buffer的優(yōu)化機(jī)制,因此如果業(yè)務(wù)可以接受,從性能角度,推薦優(yōu)先考慮非唯一索引。

主要糾結(jié)在“業(yè)務(wù)可能無法確?!薄1疚那疤崾恰皹I(yè)務(wù)代碼已經(jīng)保證不會(huì)寫入重復(fù)數(shù)據(jù)”下,討論性能問題。

如果業(yè)務(wù)不能保證,或者業(yè)務(wù)就是要求數(shù)據(jù)庫來做約束,那么沒得選,必須創(chuàng)建唯一索引。

這種情況下,本文意義在于,如果碰上大量插入數(shù)據(jù)慢、內(nèi)存命中率低時(shí),多提供一個(gè)排查思路。

然后,在一些“歸檔庫”的場景,可考慮使用唯一索引的。比如,線上數(shù)據(jù)只需保留半年,然后歷史數(shù)據(jù)保存在歸檔庫。此時(shí),歸檔數(shù)據(jù)已是確保沒有唯一鍵沖突。要提高歸檔效率,可考慮把表的唯一索引改普通索引。

參考并整理自《MySQL 實(shí)戰(zhàn) 45 講》

往期推薦

大廠Java面試題解(45)-來設(shè)計(jì)個(gè)高并發(fā)系統(tǒng)?

一看就懂的MySQL行鎖

一看就懂的圖文講解事務(wù)隔離

使用私有構(gòu)造器或枚舉來強(qiáng)化單例屬性吧

好文!點(diǎn)個(gè)好看!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容