MySQL憑借著出色的性能、低廉的成本、豐富的資源,已經(jīng)成為絕大多數(shù)互聯(lián)網(wǎng)公司的首選關(guān)系型數(shù)據(jù)庫。雖然性能出色,但所謂“好馬配好鞍”,如何能夠更好的使用它,已經(jīng)成為開發(fā)工程師的必修課,我們經(jīng)常會從職位描述上看到諸如“精通MySQL”、“SQL語句優(yōu)化”、“了解數(shù)據(jù)庫原理”等要求。我們知道一般的應(yīng)用系統(tǒng),讀寫比例在10:1左右,而且插入操作和一般的更新操作很少出現(xiàn)性能問題,遇到最多的,也是最容易出問題的,還是一些復(fù)雜的查詢操作,所以查詢語句的優(yōu)化顯然是重中之重。
本人從13年7月份起,一直在美團(tuán)核心業(yè)務(wù)系統(tǒng)部做慢查詢的優(yōu)化工作,共計十余個系統(tǒng),累計解決和積累了上百個慢查詢案例。隨著業(yè)務(wù)的復(fù)雜性提升,遇到的問題千奇百怪,五花八門,匪夷所思。本文旨在以開發(fā)工程師的角度來解釋數(shù)據(jù)庫索引的原理和如何優(yōu)化慢查詢。
一個慢查詢引發(fā)的思考
select count(*) from task where status=2 and operator_id=20839 and operate_time>1371169729 and operate_time<1371174603 and type=2;
系統(tǒng)使用者反應(yīng)有一個功能越來越慢,于是工程師找到了上面的SQL。
并且興致沖沖的找到了我,“這個SQL需要優(yōu)化,給我把每個字段都加上索引”
我很驚訝,問道“為什么需要每個字段都加上索引?”
“把查詢的字段都加上索引會更快”工程師信心滿滿
“這種情況完全可以建一個聯(lián)合索引,因為是最左前綴匹配,所以operate_time需要放到最后,而且還需要把其他相關(guān)的查詢都拿來,需要做一個綜合評估。”
“聯(lián)合索引?最左前綴匹配?綜合評估?”工程師不禁陷入了沉思。
多數(shù)情況下,我們知道索引能夠提高查詢效率,但應(yīng)該如何建立索引?索引的順序如何?許多人卻只知道大概。其實理解這些概念并不難,而且索引的原理遠(yuǎn)沒有想象的那么復(fù)雜。
MySQL索引原理
索引目的
索引的目的在于提高查詢效率,可以類比字典,如果要查“mysql”這個單詞,我們肯定需要定位到m字母,然后從下往下找到y(tǒng)字母,再找到剩下的sql。如果沒有索引,那么你可能需要把所有單詞看一遍才能找到你想要的,如果我想找到m開頭的單詞呢?或者ze開頭的單詞呢?是不是覺得如果沒有索引,這個事情根本無法完成?
索引原理
除了詞典,生活中隨處可見索引的例子,如火車站的車次表、圖書的目錄等。它們的原理都是一樣的,通過不斷的縮小想要獲得數(shù)據(jù)的范圍來篩選出最終想要的結(jié)果,同時把隨機(jī)的事件變成順序的事件,也就是我們總是通過同一種查找方式來鎖定數(shù)據(jù)。
數(shù)據(jù)庫也是一樣,但顯然要復(fù)雜許多,因為不僅面臨著等值查詢,還有范圍查詢(>、<、between、in)、模糊查詢(like)、并集查詢(or)等等。數(shù)據(jù)庫應(yīng)該選擇怎么樣的方式來應(yīng)對所有的問題呢?我們回想字典的例子,能不能把數(shù)據(jù)分成段,然后分段查詢呢?最簡單的如果1000條數(shù)據(jù),1到100分成第一段,101到200分成第二段,201到300分成第三段......這樣查第250條數(shù)據(jù),只要找第三段就可以了,一下子去除了90%的無效數(shù)據(jù)。但如果是1千萬的記錄呢,分成幾段比較好?稍有算法基礎(chǔ)的同學(xué)會想到搜索樹,其平均復(fù)雜度是lgN,具有不錯的查詢性能。但這里我們忽略了一個關(guān)鍵的問題,復(fù)雜度模型是基于每次相同的操作成本來考慮的,數(shù)據(jù)庫實現(xiàn)比較復(fù)雜,數(shù)據(jù)保存在磁盤上,而為了提高性能,每次又可以把部分?jǐn)?shù)據(jù)讀入內(nèi)存來計算,因為我們知道訪問磁盤的成本大概是訪問內(nèi)存的十萬倍左右,所以簡單的搜索樹難以滿足復(fù)雜的應(yīng)用場景。
磁盤IO與預(yù)讀
前面提到了訪問磁盤,那么這里先簡單介紹一下磁盤IO和預(yù)讀,磁盤讀取數(shù)據(jù)靠的是機(jī)械運動,每次讀取數(shù)據(jù)花費的時間可以分為尋道時間、旋轉(zhuǎn)延遲、傳輸時間三個部分,尋道時間指的是磁臂移動到指定磁道所需要的時間,主流磁盤一般在5ms以下;旋轉(zhuǎn)延遲就是我們經(jīng)常聽說的磁盤轉(zhuǎn)速,比如一個磁盤7200轉(zhuǎn),表示每分鐘能轉(zhuǎn)7200次,也就是說1秒鐘能轉(zhuǎn)120次,旋轉(zhuǎn)延遲就是1/120/2 = 4.17ms;傳輸時間指的是從磁盤讀出或?qū)?shù)據(jù)寫入磁盤的時間,一般在零點幾毫秒,相對于前兩個時間可以忽略不計。那么訪問一次磁盤的時間,即一次磁盤IO的時間約等于5+4.17 = 9ms左右,聽起來還挺不錯的,但要知道一臺500 -MIPS的機(jī)器每秒可以執(zhí)行5億條指令,因為指令依靠的是電的性質(zhì),換句話說執(zhí)行一次IO的時間可以執(zhí)行40萬條指令,數(shù)據(jù)庫動輒十萬百萬乃至千萬級數(shù)據(jù),每次9毫秒的時間,顯然是個災(zāi)難。下圖是計算機(jī)硬件延遲的對比圖,供大家參考:

考慮到磁盤IO是非常高昂的操作,計算機(jī)操作系統(tǒng)做了一些優(yōu)化,當(dāng)一次IO時,不光把當(dāng)前磁盤地址的數(shù)據(jù),而是把相鄰的數(shù)據(jù)也都讀取到內(nèi)存緩沖區(qū)內(nèi),因為局部預(yù)讀性原理告訴我們,當(dāng)計算機(jī)訪問一個地址的數(shù)據(jù)的時候,與其相鄰的數(shù)據(jù)也會很快被訪問到。每一次IO讀取的數(shù)據(jù)我們稱之為一頁(page)。具體一頁有多大數(shù)據(jù)跟操作系統(tǒng)有關(guān),一般為4k或8k,也就是我們讀取一頁內(nèi)的數(shù)據(jù)時候,實際上才發(fā)生了一次IO,這個理論對于索引的數(shù)據(jù)結(jié)構(gòu)設(shè)計非常有幫助。
索引的數(shù)據(jù)結(jié)構(gòu)
前面講了生活中索引的例子,索引的基本原理,數(shù)據(jù)庫的復(fù)雜性,又講了操作系統(tǒng)的相關(guān)知識,目的就是讓大家了解,任何一種數(shù)據(jù)結(jié)構(gòu)都不是憑空產(chǎn)生的,一定會有它的背景和使用場景,我們現(xiàn)在總結(jié)一下,我們需要這種數(shù)據(jù)結(jié)構(gòu)能夠做些什么,其實很簡單,那就是:每次查找數(shù)據(jù)時把磁盤IO次數(shù)控制在一個很小的數(shù)量級,最好是常數(shù)數(shù)量級。那么我們就想到如果一個高度可控的多路搜索樹是否能滿足需求呢?就這樣,b+樹應(yīng)運而生。
詳解b+樹

如上圖,是一顆b+樹,關(guān)于b+樹的定義可以參見B+樹,這里只說一些重點,淺藍(lán)色的塊我們稱之為一個磁盤塊,可以看到每個磁盤塊包含幾個數(shù)據(jù)項(深藍(lán)色所示)和指針(黃色所示),如磁盤塊1包含數(shù)據(jù)項17和35,包含指針P1、P2、P3,P1表示小于17的磁盤塊,P2表示在17和35之間的磁盤塊,P3表示大于35的磁盤塊。真實的數(shù)據(jù)存在于葉子節(jié)點即3、5、9、10、13、15、28、29、36、60、75、79、90、99。非葉子節(jié)點只不存儲真實的數(shù)據(jù),只存儲指引搜索方向的數(shù)據(jù)項,如17、35并不真實存在于數(shù)據(jù)表中。
b+樹的查找過程
如圖所示,如果要查找數(shù)據(jù)項29,那么首先會把磁盤塊1由磁盤加載到內(nèi)存,此時發(fā)生一次IO,在內(nèi)存中用二分查找確定29在17和35之間,鎖定磁盤塊1的P2指針,內(nèi)存時間因為非常短(相比磁盤的IO)可以忽略不計,通過磁盤塊1的P2指針的磁盤地址把磁盤塊3由磁盤加載到內(nèi)存,發(fā)生第二次IO,29在26和30之間,鎖定磁盤塊3的P2指針,通過指針加載磁盤塊8到內(nèi)存,發(fā)生第三次IO,同時內(nèi)存中做二分查找找到29,結(jié)束查詢,總計三次IO。真實的情況是,3層的b+樹可以表示上百萬的數(shù)據(jù),如果上百萬的數(shù)據(jù)查找只需要三次IO,性能提高將是巨大的,如果沒有索引,每個數(shù)據(jù)項都要發(fā)生一次IO,那么總共需要百萬次的IO,顯然成本非常非常高。
b+樹性質(zhì)
1.通過上面的分析,我們知道IO次數(shù)取決于b+數(shù)的高度h,假設(shè)當(dāng)前數(shù)據(jù)表的數(shù)據(jù)為N,每個磁盤塊的數(shù)據(jù)項的數(shù)量是m,則有h=
主站蜘蛛池模板:
灵武市|
怀仁县|
长白|
会理县|
高台县|
松潘县|
金华市|
信阳市|
五指山市|
锡林浩特市|
兴安盟|
惠安县|
东丰县|
新津县|
枣庄市|
乌兰察布市|
龙门县|
黎川县|
昭苏县|
莲花县|
宜宾县|
图们市|
仁寿县|
苗栗县|
左云县|
托克托县|
宁化县|
阿拉善右旗|
竹溪县|
禹州市|
宜春市|
磴口县|
城固县|
革吉县|
镇沅|
普兰店市|
中江县|
桂阳县|
光山县|
丰县|
丰县|