說起MySQL的查詢優(yōu)化姜骡,相信大家收藏了一堆奇技淫巧:不能使用SELECT *、不使用NULL字段城菊、合理創(chuàng)建索引传于、為字段選擇合適的數(shù)據(jù)類型..... 你是否真的理解這些優(yōu)化技巧?是否理解其背后的工作原理菩彬?在實(shí)際場(chǎng)景下性能真有提升嗎缠劝?我想未必。因而理解這些優(yōu)化建議背后的原理就尤為重要挤巡,希望本文能讓你重新審視這些優(yōu)化建議剩彬,并在實(shí)際業(yè)務(wù)場(chǎng)景下合理的運(yùn)用。
1)MySQL邏輯架構(gòu)
如果能在頭腦中構(gòu)建一幅MySQL各組件之間如何協(xié)同工作的架構(gòu)圖矿卑,有助于深入理解MySQL服務(wù)器喉恋。下圖展示了MySQL的邏輯架構(gòu)圖。
MySQL邏輯架構(gòu)整體分為三層轻黑,最上層為客戶端層,并非MySQL所獨(dú)有琴昆,諸如:連接處理氓鄙、授權(quán)認(rèn)證、安全等功能均在這一層處理业舍。
MySQL大多數(shù)核心服務(wù)均在中間這一層抖拦,包括查詢解析升酣、分析、優(yōu)化态罪、緩存噩茄、內(nèi)置函數(shù)(比如:時(shí)間、數(shù)學(xué)复颈、加密等函數(shù))绩聘。所有的跨存儲(chǔ)引擎的功能也在這一層實(shí)現(xiàn):存儲(chǔ)過程、觸發(fā)器耗啦、視圖等凿菩。
最下層為存儲(chǔ)引擎,其負(fù)責(zé)MySQL中的數(shù)據(jù)存儲(chǔ)和提取帜讲。和Linux下的文件系統(tǒng)類似衅谷,每種存儲(chǔ)引擎都有其優(yōu)勢(shì)和劣勢(shì)。中間的服務(wù)層通過API與存儲(chǔ)引擎通信舒帮,這些API接口屏蔽了不同存儲(chǔ)引擎間的差異会喝。
2)MySQL查詢過程
我們總是希望MySQL能夠獲得更高的查詢性能陡叠,最好的辦法是弄清楚MySQL是如何優(yōu)化和執(zhí)行查詢的玩郊。一旦理解了這一點(diǎn),就會(huì)發(fā)現(xiàn):很多的查詢優(yōu)化工作實(shí)際上就是遵循一些原則讓MySQL的優(yōu)化器能夠按照預(yù)想的合理方式運(yùn)行而已枉阵。
當(dāng)向MySQL發(fā)送一個(gè)請(qǐng)求的時(shí)候译红,MySQL到底做了些什么呢?
客戶端/服務(wù)端通信協(xié)議
MySQL客戶端/服務(wù)端通信協(xié)議是“半雙工”的:在任一時(shí)刻兴溜,要么是服務(wù)器向客戶端發(fā)送數(shù)據(jù)侦厚,要么是客戶端向服務(wù)器發(fā)送數(shù)據(jù),這兩個(gè)動(dòng)作不能同時(shí)發(fā)生拙徽。一旦一端開始發(fā)送消息刨沦,另一端要接收完整個(gè)消息才能響應(yīng)它,所以我們無法也無須將一個(gè)消息切成小塊獨(dú)立發(fā)送膘怕,也沒有辦法進(jìn)行流量控制想诅。
客戶端用一個(gè)單獨(dú)的數(shù)據(jù)包將查詢請(qǐng)求發(fā)送給服務(wù)器,所以當(dāng)查詢語句很長的時(shí)候岛心,需要設(shè)置max_allowed_packet參數(shù)来破。但是需要注意的是,如果查詢實(shí)在是太大忘古,服務(wù)端會(huì)拒絕接收更多數(shù)據(jù)并拋出異常徘禁。
與之相反的是,服務(wù)器響應(yīng)給用戶的數(shù)據(jù)通常會(huì)很多髓堪,由多個(gè)數(shù)據(jù)包組成送朱。但是當(dāng)服務(wù)器響應(yīng)客戶端請(qǐng)求時(shí)娘荡,客戶端必須完整的接收整個(gè)返回結(jié)果,而不能簡單的只取前面幾條結(jié)果驶沼,然后讓服務(wù)器停止發(fā)送它改。因而在實(shí)際開發(fā)中,盡量保持查詢簡單且只返回必需的數(shù)據(jù)商乎,減小通信間數(shù)據(jù)包的大小和數(shù)量是一個(gè)非常好的習(xí)慣央拖,這也是查詢中盡量避免使用SELECT *以及加上LIMIT限制的原因之一。
查詢緩存
在解析一個(gè)查詢語句前鹉戚,如果查詢緩存是打開的鲜戒,那么MySQL會(huì)檢查這個(gè)查詢語句是否命中查詢緩存中的數(shù)據(jù)。如果當(dāng)前查詢恰好命中查詢緩存抹凳,在檢查一次用戶權(quán)限后直接返回緩存中的結(jié)果遏餐。這種情況下,查詢不會(huì)被解析赢底,也不會(huì)生成執(zhí)行計(jì)劃失都,更不會(huì)執(zhí)行。
MySQL將緩存存放在一個(gè)引用表(不要理解成table幸冻,可以認(rèn)為是類似于HashMap的數(shù)據(jù)結(jié)構(gòu))粹庞,通過一個(gè)哈希值索引,這個(gè)哈希值通過查詢本身洽损、當(dāng)前要查詢的數(shù)據(jù)庫庞溜、客戶端協(xié)議版本號(hào)等一些可能影響結(jié)果的信息計(jì)算得來。所以兩個(gè)查詢?cè)谌魏巫址系牟煌ɡ纾嚎崭癖ā⒆⑨專┝髀耄紩?huì)導(dǎo)致緩存不會(huì)命中。
如果查詢中包含任何用戶自定義函數(shù)延刘、存儲(chǔ)函數(shù)漫试、用戶變量、臨時(shí)表碘赖、mysql庫中的系統(tǒng)表驾荣,其查詢結(jié)果都不會(huì)被緩存。比如函數(shù)NOW()或者CURRENT_DATE()會(huì)因?yàn)椴煌牟樵儠r(shí)間崖疤,返回不同的查詢結(jié)果秘车,再比如包含CURRENT_USER或者CONNECION_ID()的查詢語句會(huì)因?yàn)椴煌挠脩舳祷夭煌慕Y(jié)果,將這樣的查詢結(jié)果緩存起來沒有任何的意義劫哼。
既然是緩存叮趴,就會(huì)失效,那查詢緩存何時(shí)失效呢权烧?MySQL的查詢緩存系統(tǒng)會(huì)跟蹤查詢中涉及的每個(gè)表眯亦,如果這些表(數(shù)據(jù)或結(jié)構(gòu))發(fā)生變化伤溉,那么和這張表相關(guān)的所有緩存數(shù)據(jù)都將失效。正因?yàn)槿绱似蘼剩谌魏蔚膶懖僮鲿r(shí)乱顾,MySQL必須將對(duì)應(yīng)表的所有緩存都設(shè)置為失效。如果查詢緩存非常大或者碎片很多宫静,這個(gè)操作就可能帶來很大的系統(tǒng)消耗走净,甚至導(dǎo)致系統(tǒng)僵死一會(huì)兒。而且查詢緩存對(duì)系統(tǒng)的額外消耗也不僅僅在寫操作孤里,讀操作也不例外:任何的查詢語句在開始之前都必須經(jīng)過檢查伏伯,即使這條SQL語句永遠(yuǎn)不會(huì)命中緩存。如果查詢結(jié)果可以被緩存捌袜,那么執(zhí)行完成后说搅,會(huì)將結(jié)果存入緩存,也會(huì)帶來額外的系統(tǒng)消耗
基于此虏等,我們要知道并不是什么情況下查詢緩存都會(huì)提高系統(tǒng)性能弄唧,緩存和失效都會(huì)帶來額外消耗,只有當(dāng)緩存帶來的資源節(jié)約大于其本身消耗的資源時(shí)霍衫,才會(huì)給系統(tǒng)帶來性能提升候引。但要如何評(píng)估打開緩存是否能夠帶來性能提升是一件非常困難的事情,也不在本文討論的范疇內(nèi)慕淡。如果系統(tǒng)確實(shí)存在一些性能問題背伴,可以嘗試打開查詢緩存沸毁,并在數(shù)據(jù)庫設(shè)計(jì)上做一些優(yōu)化峰髓,比如:
a)用多個(gè)小表代替一個(gè)大表,注意不要過度設(shè)計(jì)
b)批量插入代替循環(huán)單條插入
c)合理控制緩存空間大小息尺,一般來說其大小設(shè)置為幾十兆比較合適
d)可以通過SQL_CACHE和SQL_NO_CACHE來控制某個(gè)查詢語句是否需要進(jìn)行緩存
最后的忠告是不要輕易打開查詢緩存携兵,特別是寫密集型應(yīng)用。如果你實(shí)在是忍不住搂誉,可以將query_cache_type設(shè)置為DEMAND徐紧,這時(shí)只有加入SQL_CACHE的查詢才會(huì)走緩存,其他查詢則不會(huì)炭懊,這樣可以非常自由地控制哪些查詢需要被緩存并级。
當(dāng)然查詢緩存系統(tǒng)本身是非常復(fù)雜的,這里討論的也只是很小的一部分侮腹,其他更深入的話題嘲碧,比如:緩存是如何使用內(nèi)存的?如何控制內(nèi)存的碎片化父阻?事務(wù)對(duì)查詢緩存有何影響等等愈涩,讀者可以自行閱讀相關(guān)資料望抽,這里權(quán)當(dāng)拋磚引玉吧。
語法解析和預(yù)處理
MySQL通過關(guān)鍵字將SQL語句進(jìn)行解析履婉,并生成一顆對(duì)應(yīng)的解析樹煤篙。這個(gè)過程解析器主要通過語法規(guī)則來驗(yàn)證和解析。比如SQL中是否使用了錯(cuò)誤的關(guān)鍵字或者關(guān)鍵字的順序是否正確等等毁腿。預(yù)處理則會(huì)根據(jù)MySQL規(guī)則進(jìn)一步檢查解析樹是否合法辑奈。比如檢查要查詢的數(shù)據(jù)表和數(shù)據(jù)列是否存在等等。
查詢優(yōu)化
經(jīng)過前面的步驟生成的語法樹被認(rèn)為是合法的了已烤,并且由優(yōu)化器將其轉(zhuǎn)化成查詢計(jì)劃身害。多數(shù)情況下,一條查詢可以有很多種執(zhí)行方式草戈,最后都返回相應(yīng)的結(jié)果塌鸯。優(yōu)化器的作用就是找到這其中最好的執(zhí)行計(jì)劃。
MySQL使用基于成本的優(yōu)化器唐片,它嘗試預(yù)測(cè)一個(gè)查詢使用某種執(zhí)行計(jì)劃時(shí)的成本丙猬,并選擇其中成本最小的一個(gè)。在MySQL可以通過查詢當(dāng)前會(huì)話的last_query_cost的值來得到其計(jì)算當(dāng)前查詢的成本费韭。
示例中的結(jié)果表示優(yōu)化器認(rèn)為大概需要做6391個(gè)數(shù)據(jù)頁的隨機(jī)查找才能完成上面的查詢茧球。這個(gè)結(jié)果是根據(jù)一些列的統(tǒng)計(jì)信息計(jì)算得來的,這些統(tǒng)計(jì)信息包括:每張表或者索引的頁面?zhèn)€數(shù)星持、索引的基數(shù)抢埋、索引和數(shù)據(jù)行的長度、索引的分布情況等等督暂。
有非常多的原因會(huì)導(dǎo)致MySQL選擇錯(cuò)誤的執(zhí)行計(jì)劃揪垄,比如統(tǒng)計(jì)信息不準(zhǔn)確、不會(huì)考慮不受其控制的操作成本(用戶自定義函數(shù)逻翁、存儲(chǔ)過程)饥努、MySQL認(rèn)為的最優(yōu)跟我們想的不一樣(我們希望執(zhí)行時(shí)間盡可能短,但MySQL值選擇它認(rèn)為成本小的八回,但成本小并不意味著執(zhí)行時(shí)間短)等等酷愧。
MySQL的查詢優(yōu)化器是一個(gè)非常復(fù)雜的部件,它使用了非常多的優(yōu)化策略來生成一個(gè)最優(yōu)的執(zhí)行計(jì)劃:
????●?重新定義表的關(guān)聯(lián)順序(多張表關(guān)聯(lián)查詢時(shí)缠诅,并不一定按照SQL中指定的順序進(jìn)行溶浴,但有一些技巧可以指定關(guān)聯(lián)順序)
????● 優(yōu)化MIN()和MAX()函數(shù)(找某列的最小值,如果該列有索引管引,只需要查找B+Tree索引最左端士败,反之則可以找到最大值,具體原理見下文)
????●?提前終止查詢(比如:使用Limit時(shí)汉匙,查找到滿足數(shù)量的結(jié)果集后會(huì)立即終止查詢)
????●?優(yōu)化排序(在老版本MySQL會(huì)使用兩次傳輸排序拱烁,即先讀取行指針和需要排序的字段在內(nèi)存中對(duì)其排序生蚁,然后再根據(jù)排序結(jié)果去讀取數(shù)據(jù)行,而新版本采用的是單次傳輸排序戏自,也就是一次讀取所有的數(shù)據(jù)行邦投,然后根據(jù)給定的列排序。對(duì)于I/O密集型應(yīng)用擅笔,效率會(huì)高很多)
隨著MySQL的不斷發(fā)展志衣,優(yōu)化器使用的優(yōu)化策略也在不斷的進(jìn)化,這里僅僅介紹幾個(gè)非常常用且容易理解的優(yōu)化策略猛们,其他的優(yōu)化策略念脯,大家自行查閱吧。
查詢執(zhí)行引擎
在完成解析和優(yōu)化階段以后弯淘,MySQL會(huì)生成對(duì)應(yīng)的執(zhí)行計(jì)劃绿店,查詢執(zhí)行引擎根據(jù)執(zhí)行計(jì)劃給出的指令逐步執(zhí)行得出結(jié)果。整個(gè)執(zhí)行過程的大部分操作均是通過調(diào)用存儲(chǔ)引擎實(shí)現(xiàn)的接口來完成庐橙,這些接口被稱為handler API假勿。查詢過程中的每一張表由一個(gè)handler實(shí)例表示。實(shí)際上态鳖,MySQL在查詢優(yōu)化階段就為每一張表創(chuàng)建了一個(gè)handler實(shí)例转培,優(yōu)化器可以根據(jù)這些實(shí)例的接口來獲取表的相關(guān)信息,包括表的所有列名浆竭、索引統(tǒng)計(jì)信息等浸须。存儲(chǔ)引擎接口提供了非常豐富的功能,但其底層僅有幾十個(gè)接口邦泄,這些接口像搭積木一樣完成了一次查詢的大部分操作删窒。
返回結(jié)果給客戶端
查詢執(zhí)行的最后一個(gè)階段就是將結(jié)果返回給客戶端。即使查詢不到數(shù)據(jù)虎韵,MySQL仍然會(huì)返回這個(gè)查詢的相關(guān)信息易稠,比如該查詢影響到的行數(shù)以及執(zhí)行時(shí)間等等。如果查詢緩存被打開且這個(gè)查詢可以被緩存包蓝,MySQL也會(huì)將結(jié)果存放到緩存中。
結(jié)果集返回客戶端是一個(gè)增量且逐步返回的過程企量。有可能MySQL在生成第一條結(jié)果時(shí)测萎,就開始向客戶端逐步返回結(jié)果集了。這樣服務(wù)端就無需存儲(chǔ)太多結(jié)果而消耗過多內(nèi)存届巩,也可以讓客戶端第一時(shí)間獲得返回結(jié)果硅瞧。需要注意的是,結(jié)果集中的每一行都會(huì)以一個(gè)滿足①中所描述的通信協(xié)議的數(shù)據(jù)包發(fā)送恕汇,再通過TCP協(xié)議進(jìn)行傳輸腕唧,在傳輸過程中或辖,可能對(duì)MySQL的數(shù)據(jù)包進(jìn)行緩存然后批量發(fā)送。
回頭總結(jié)一下MySQL整個(gè)查詢執(zhí)行過程枣接,總的來說分為5個(gè)步驟:
(1)客戶端向MySQL服務(wù)器發(fā)送一條查詢請(qǐng)求
(2)服務(wù)器首先檢查查詢緩存颂暇,如果命中緩存,則立刻返回存儲(chǔ)在緩存中的結(jié)果但惶。否則進(jìn)入下一階段
(3)服務(wù)器進(jìn)行SQL解析耳鸯、預(yù)處理、再由優(yōu)化器生成對(duì)應(yīng)的執(zhí)行計(jì)劃
(4)MySQL根據(jù)執(zhí)行計(jì)劃膀曾,調(diào)用存儲(chǔ)引擎的API來執(zhí)行查詢
(5)將結(jié)果返回給客戶端县爬,同時(shí)緩存查詢結(jié)果
性能優(yōu)化建議
看了這么多,你可能會(huì)期待給出一些優(yōu)化手段添谊,是的财喳,下面會(huì)從3個(gè)不同方面給出一些優(yōu)化建議。但請(qǐng)等等斩狱,還有一句忠告要先送給你:不要聽信你看到的關(guān)于優(yōu)化的“絕對(duì)真理”纲缓,包括本文所討論的內(nèi)容,而應(yīng)該是在實(shí)際的業(yè)務(wù)場(chǎng)景下通過測(cè)試來驗(yàn)證你關(guān)于執(zhí)行計(jì)劃以及響應(yīng)時(shí)間的假設(shè)喊废。
1祝高、Scheme設(shè)計(jì)與數(shù)據(jù)類型優(yōu)化
選擇數(shù)據(jù)類型只要遵循小而簡單的原則就好,越小的數(shù)據(jù)類型通常會(huì)更快污筷,占用更少的磁盤工闺、內(nèi)存,處理時(shí)需要的CPU周期也更少瓣蛀。越簡單的數(shù)據(jù)類型在計(jì)算時(shí)需要更少的CPU周期陆蟆,比如,整型就比字符操作代價(jià)低惋增,因而會(huì)使用整型來存儲(chǔ)ip地址叠殷,使用DATETIME來存儲(chǔ)時(shí)間,而不是使用字符串诈皿。
這里總結(jié)幾個(gè)可能容易理解錯(cuò)誤的技巧:
????? 通常來說把可為NULL的列改為NOT NULL不會(huì)對(duì)性能提升有多少幫助林束,只是如果計(jì)劃在列上創(chuàng)建索引,就應(yīng)該將該列設(shè)置為NOT NULL稽亏。
??????對(duì)整數(shù)類型指定寬度壶冒,比如INT(11),沒有任何卵用截歉。INT使用32位(4個(gè)字節(jié))存儲(chǔ)空間胖腾,那么它的表示范圍已經(jīng)確定,所以INT(1)和INT(20)對(duì)于存儲(chǔ)和計(jì)算是相同的。
??????UNSIGNED表示不允許負(fù)值咸作,大致可以使正數(shù)的上限提高一倍锨阿。比如TINYINT存儲(chǔ)范圍是-128 ~ 127,而UNSIGNED TINYINT存儲(chǔ)的范圍卻是0 - 255记罚。
??????通常來講墅诡,沒有太大的必要使用DECIMAL數(shù)據(jù)類型。即使是在需要存儲(chǔ)財(cái)務(wù)數(shù)據(jù)時(shí)毫胜,仍然可以使用BIGINT书斜。比如需要精確到萬分之一,那么可以將數(shù)據(jù)乘以一百萬然后使用BIGINT存儲(chǔ)酵使。這樣可以避免浮點(diǎn)數(shù)計(jì)算不準(zhǔn)確和DECIMAL精確計(jì)算代價(jià)高的問題荐吉。
??????TIMESTAMP使用4個(gè)字節(jié)存儲(chǔ)空間,DATETIME使用8個(gè)字節(jié)存儲(chǔ)空間口渔。因而刃滓,TIMESTAMP只能表示1970 - 2038年鲫咽,比DATETIME表示的范圍小得多移斩,而且TIMESTAMP的值因時(shí)區(qū)不同而不同劳秋。
??????大多數(shù)情況下沒有使用枚舉類型的必要,其中一個(gè)缺點(diǎn)是枚舉的字符串列表是固定的攻礼,添加和刪除字符串(枚舉選項(xiàng))必須使用ALTER TABLE(如果只只是在列表末尾追加元素业踢,不需要重建表)。
????? schema的列不要太多礁扮。原因是存儲(chǔ)引擎的API工作時(shí)需要在服務(wù)器層和存儲(chǔ)引擎層之間通過行緩沖格式拷貝數(shù)據(jù)知举,然后在服務(wù)器層將緩沖內(nèi)容解碼成各個(gè)列,這個(gè)轉(zhuǎn)換過程的代價(jià)是非常高的太伊。如果列太多而實(shí)際使用的列又很少的話雇锡,有可能會(huì)導(dǎo)致CPU占用過高。
??????大表ALTER TABLE非常耗時(shí)僚焦,MySQL執(zhí)行大部分修改表結(jié)果操作的方法是用新的結(jié)構(gòu)創(chuàng)建一個(gè)張空表锰提,從舊表中查出所有的數(shù)據(jù)插入新表,然后再刪除舊表芳悲。尤其當(dāng)內(nèi)存不足而表又很大立肘,而且還有很大索引的情況下,耗時(shí)更久芭概。當(dāng)然有一些奇技淫巧可以解決這個(gè)問題赛不,有興趣可自行查閱。
2罢洲、創(chuàng)建高性能索引
索引是提高M(jìn)ySQL查詢性能的一個(gè)重要途徑,但過多的索引可能會(huì)導(dǎo)致過高的磁盤使用率以及過高的內(nèi)存占用,從而影響應(yīng)用程序的整體性能惹苗。應(yīng)當(dāng)盡量避免事后才想起添加索引殿较,因?yàn)槭潞罂赡苄枰O(jiān)控大量的SQL才能定位到問題所在,而且添加索引的時(shí)間肯定是遠(yuǎn)大于初始添加索引所需要的時(shí)間桩蓉,可見索引的添加也是非常有技術(shù)含量的淋纲。
接下來將向你展示一系列創(chuàng)建高性能索引的策略,以及每條策略其背后的工作原理院究。但在此之前洽瞬,先了解與索引相關(guān)的一些算法和數(shù)據(jù)結(jié)構(gòu),將有助于更好的理解后文的內(nèi)容业汰。
索引相關(guān)的數(shù)據(jù)結(jié)構(gòu)和算法
通常我們所說的索引是指B-Tree索引伙窃,它是目前關(guān)系型數(shù)據(jù)庫中查找數(shù)據(jù)最為常用和有效的索引,大多數(shù)存儲(chǔ)引擎都支持這種索引样漆。使用B-Tree這個(gè)術(shù)語为障,是因?yàn)镸ySQL在CREATE TABLE或其它語句中使用了這個(gè)關(guān)鍵字,但實(shí)際上不同的存儲(chǔ)引擎可能使用不同的數(shù)據(jù)結(jié)構(gòu)放祟,比如InnoDB就是使用的B+Tree鳍怨。
B+Tree中的B是指balance,意為平衡跪妥。需要注意的是鞋喇,B+樹索引并不能找到一個(gè)給定鍵值的具體行,它找到的只是被查找數(shù)據(jù)行所在的頁眉撵,接著數(shù)據(jù)庫會(huì)把頁讀入到內(nèi)存侦香,再在內(nèi)存中進(jìn)行查找,最后得到要查找的數(shù)據(jù)执桌。
在介紹B+Tree前鄙皇,先了解一下二叉查找樹,它是一種經(jīng)典的數(shù)據(jù)結(jié)構(gòu)仰挣,其左子樹的值總是小于根的值伴逸,右子樹的值總是大于根的值,如下圖①膘壶。如果要在這課樹中查找值為5的記錄错蝴,其大致流程:先找到根,其值為6颓芭,大于5顷锰,所以查找左子樹,找到3亡问,而5大于3官紫,接著找3的右子樹肛宋,總共找了3次。同樣的方法束世,如果查找值為8的記錄酝陈,也需要查找3次。所以二叉查找樹的平均查找次數(shù)為(3 + 3 + 3 + 2 + 2 + 1) / 6 = 2.3次毁涉,而順序查找的話沉帮,查找值為2的記錄,僅需要1次贫堰,但查找值為8的記錄則需要6次穆壕,所以順序查找的平均查找次數(shù)為:(1 + 2 + 3 + 4 + 5 + 6) / 6 = 3.3次,因此大多數(shù)情況下二叉查找樹的平均查找速度比順序查找要快其屏。
由于二叉查找樹可以任意構(gòu)造喇勋,同樣的值,可以構(gòu)造出如圖②的二叉查找樹漫玄,顯然這棵二叉樹的查詢效率和順序查找差不多茄蚯。若想二叉查找數(shù)的查詢性能最高,需要這棵二叉查找樹是平衡的睦优,也即平衡二叉樹(AVL樹)渗常。
平衡二叉樹首先需要符合二叉查找樹的定義,其次必須滿足任何節(jié)點(diǎn)的兩個(gè)子樹的高度差不能大于1汗盘。顯然圖②不滿足平衡二叉樹的定義皱碘,而圖①是一課平衡二叉樹。平衡二叉樹的查找性能是比較高的(性能最好的是最優(yōu)二叉樹)隐孽,查詢性能越好癌椿,維護(hù)的成本就越大。比如圖①的平衡二叉樹菱阵,當(dāng)用戶需要插入一個(gè)新的值9的節(jié)點(diǎn)時(shí)踢俄,就需要做出如下變動(dòng)。
通過一次左旋操作就將插入后的樹重新變?yōu)槠胶舛鏄涫亲詈唵蔚那闆r了晴及,實(shí)際應(yīng)用場(chǎng)景中可能需要旋轉(zhuǎn)多次都办。至此我們可以考慮一個(gè)問題,平衡二叉樹的查找效率還不錯(cuò)虑稼,實(shí)現(xiàn)也非常簡單琳钉,相應(yīng)的維護(hù)成本還能接受,為什么MySQL索引不直接使用平衡二叉樹蛛倦?
隨著數(shù)據(jù)庫中數(shù)據(jù)的增加歌懒,索引本身大小隨之增加,不可能全部存儲(chǔ)在內(nèi)存中溯壶,因此索引往往以索引文件的形式存儲(chǔ)的磁盤上及皂。這樣的話甫男,索引查找過程中就要產(chǎn)生磁盤I/O消耗,相對(duì)于內(nèi)存存取躲庄,I/O存取的消耗要高幾個(gè)數(shù)量級(jí)查剖〖嘏埃可以想象一下一棵幾百萬節(jié)點(diǎn)的二叉樹的深度是多少噪窘?如果將這么大深度的一顆二叉樹放磁盤上,每讀取一個(gè)節(jié)點(diǎn)效扫,需要一次磁盤的I/O讀取倔监,整個(gè)查找的耗時(shí)顯然是不能夠接受的。那么如何減少查找過程中的I/O存取次數(shù)菌仁?
一種行之有效的解決方法是減少樹的深度浩习,將二叉樹變?yōu)閙叉樹(多路搜索樹),而B+Tree就是一種多路搜索樹济丘。理解B+Tree時(shí)谱秽,只需要理解其最重要的兩個(gè)特征即可:第一,所有的關(guān)鍵字(可以理解為數(shù)據(jù))都存儲(chǔ)在葉子節(jié)點(diǎn)(Leaf Page)摹迷,非葉子節(jié)點(diǎn)(Index Page)并不存儲(chǔ)真正的數(shù)據(jù)疟赊,所有記錄節(jié)點(diǎn)都是按鍵值大小順序存放在同一層葉子節(jié)點(diǎn)上。其次峡碉,所有的葉子節(jié)點(diǎn)由指針連接近哟。如下圖為高度為2的簡化了的B+Tree。
怎么理解這兩個(gè)特征鲫寄?MySQL將每個(gè)節(jié)點(diǎn)的大小設(shè)置為一個(gè)頁的整數(shù)倍(原因下文會(huì)介紹)吉执,也就是在節(jié)點(diǎn)空間大小一定的情況下,每個(gè)節(jié)點(diǎn)可以存儲(chǔ)更多的內(nèi)結(jié)點(diǎn)地来,這樣每個(gè)結(jié)點(diǎn)能索引的范圍更大更精確戳玫。所有的葉子節(jié)點(diǎn)使用指針鏈接的好處是可以進(jìn)行區(qū)間訪問,比如上圖中未斑,如果查找大于20而小于30的記錄咕宿,只需要找到節(jié)點(diǎn)20,就可以遍歷指針依次找到25颂碧、30荠列。如果沒有鏈接指針的話,就無法進(jìn)行區(qū)間查找载城。這也是MySQL使用B+Tree作為索引存儲(chǔ)結(jié)構(gòu)的重要原因肌似。
MySQL為何將節(jié)點(diǎn)大小設(shè)置為頁的整數(shù)倍,這就需要理解磁盤的存儲(chǔ)原理诉瓦。磁盤本身存取就比主存慢很多川队,在加上機(jī)械運(yùn)動(dòng)損耗(特別是普通的機(jī)械硬盤)力细,磁盤的存取速度往往是主存的幾百萬分之一,為了盡量減少磁盤I/O固额,磁盤往往不是嚴(yán)格按需讀取眠蚂,而是每次都會(huì)預(yù)讀,即使只需要一個(gè)字節(jié)斗躏,磁盤也會(huì)從這個(gè)位置開始逝慧,順序向后讀取一定長度的數(shù)據(jù)放入內(nèi)存,預(yù)讀的長度一般為頁的整數(shù)倍啄糙。
????????頁是計(jì)算機(jī)管理存儲(chǔ)器的邏輯塊笛臣,硬件及OS往往將主存和磁盤存儲(chǔ)區(qū)分割為連續(xù)的大小相等的塊,每個(gè)存儲(chǔ)塊稱為一頁(許多OS中隧饼,? ? ? ? ? 頁的大小通常為4K)沈堡。主存和磁盤以頁為單位交換數(shù)據(jù)。當(dāng)程序要讀取的數(shù)據(jù)不在主存中時(shí)燕雁,會(huì)觸發(fā)一個(gè)缺頁異常诞丽,此時(shí)系統(tǒng)會(huì)向磁盤? ? ? ? ? 發(fā)出讀盤信號(hào),磁盤會(huì)找到數(shù)據(jù)的起始位置并向后連續(xù)讀取一頁或幾頁載入內(nèi)存中拐格,然后一起返回僧免,程序繼續(xù)運(yùn)行。
MySQL巧妙利用了磁盤預(yù)讀原理禁荒,將一個(gè)節(jié)點(diǎn)的大小設(shè)為等于一個(gè)頁猬膨,這樣每個(gè)節(jié)點(diǎn)只需要一次I/O就可以完全載入。為了達(dá)到這個(gè)目的呛伴,每次新建節(jié)點(diǎn)時(shí)勃痴,直接申請(qǐng)一個(gè)頁的空間,這樣就保證一個(gè)節(jié)點(diǎn)物理上也存儲(chǔ)在一個(gè)頁里热康,加之計(jì)算機(jī)存儲(chǔ)分配都是按頁對(duì)齊的沛申,就實(shí)現(xiàn)了讀取一個(gè)節(jié)點(diǎn)只需一次I/O。假設(shè)B+Tree的高度為h姐军,一次檢索最多需要h-1次I/O(根節(jié)點(diǎn)常駐內(nèi)存)铁材,復(fù)雜度O(h) = O(logmN)。實(shí)際應(yīng)用場(chǎng)景中奕锌,M通常較大著觉,常常超過100,因此樹的高度一般都比較小惊暴,通常不超過3饼丘。
最后簡單了解下B+Tree節(jié)點(diǎn)的操作,在整體上對(duì)索引的維護(hù)有一個(gè)大概的了解辽话,雖然索引可以大大提高查詢效率肄鸽,但維護(hù)索引仍要花費(fèi)很大的代價(jià)卫病,因此合理的創(chuàng)建索引也就尤為重要。
仍以上面的樹為例典徘,我們假設(shè)每個(gè)節(jié)點(diǎn)只能存儲(chǔ)4個(gè)內(nèi)節(jié)點(diǎn)蟀苛。首先要插入第一個(gè)節(jié)點(diǎn)28,如下圖所示逮诲。
接著插入下一個(gè)節(jié)點(diǎn)70帜平,在Index Page中查詢后得知應(yīng)該插入到50 - 70之間的葉子節(jié)點(diǎn),但葉子節(jié)點(diǎn)已滿汛骂,這時(shí)候就需要進(jìn)行也分裂的操作罕模,當(dāng)前的葉子節(jié)點(diǎn)起點(diǎn)為50,所以根據(jù)中間值來拆分葉子節(jié)點(diǎn)帘瞭,如下圖所示。
最后插入一個(gè)節(jié)點(diǎn)95蒿讥,這時(shí)候Index Page和Leaf Page都滿了蝶念,就需要做兩次拆分,如下圖所示芋绸。
拆分后最終形成了這樣一顆樹媒殉。
B+Tree為了保持平衡,對(duì)于新插入的值需要做大量的拆分頁操作摔敛,而頁的拆分需要I/O操作廷蓉,為了盡可能的減少頁的拆分操作,B+Tree也提供了類似于平衡二叉樹的旋轉(zhuǎn)功能马昙。當(dāng)Leaf Page已滿但其左右兄弟節(jié)點(diǎn)沒有滿的情況下桃犬,B+Tree并不急于去做拆分操作,而是將記錄移到當(dāng)前所在頁的兄弟節(jié)點(diǎn)上行楞。通常情況下攒暇,左兄弟會(huì)被先檢查用來做旋轉(zhuǎn)操作。就比如上面第二個(gè)示例子房,當(dāng)插入70的時(shí)候形用,并不會(huì)去做頁拆分,而是左旋操作证杭。
通過旋轉(zhuǎn)操作可以最大限度的減少頁分裂田度,從而減少索引維護(hù)過程中的磁盤的I/O操作,也提高索引維護(hù)效率解愤。需要注意的是镇饺,刪除節(jié)點(diǎn)跟插入節(jié)點(diǎn)類似,仍然需要旋轉(zhuǎn)和拆分操作琢歇,這里就不再說明兰怠。
高性能策略
通過上文梦鉴,相信你對(duì)B+Tree的數(shù)據(jù)結(jié)構(gòu)已經(jīng)有了大致的了解,但MySQL中索引是如何組織數(shù)據(jù)的存儲(chǔ)呢揭保?以一個(gè)簡單的示例來說明肥橙,假如有如下數(shù)據(jù)表:
對(duì)于表中每一行數(shù)據(jù),索引中包含了last_name秸侣、first_name存筏、dob列的值,下圖展示了索引是如何組織數(shù)據(jù)存儲(chǔ)的味榛。
可以看到,索引首先根據(jù)第一個(gè)字段來排列順序搏色,當(dāng)名字相同時(shí)善茎,則根據(jù)第三個(gè)字段,即出生日期來排序频轿,正是因?yàn)檫@個(gè)原因垂涯,才有了索引的“最左原則”。
????? MySQL不會(huì)使用索引的情況:非獨(dú)立的列
“獨(dú)立的列”是指索引列不能是表達(dá)式的一部分航邢,也不能是函數(shù)的參數(shù)耕赘。比如:select * from where id + 1 = 5,我們很容易看出其等價(jià)于 id = 4膳殷,但是MySQL無法自動(dòng)解析這個(gè)表達(dá)式操骡,使用函數(shù)是同樣的道理。
??????前綴索引
如果列很長赚窃,通巢嵴校可以索引開始的部分字符,這樣可以有效節(jié)約索引空間考榨,從而提高索引效率跨细。
??????多列索引和索引順序
在多數(shù)情況下,在多個(gè)列上建立獨(dú)立的索引并不能提高查詢性能河质。理由非常簡單冀惭,MySQL不知道選擇哪個(gè)索引的查詢效率更好,所以在老版本掀鹅,比如MySQL5.0之前就會(huì)隨便選擇一個(gè)列的索引散休,而新的版本會(huì)采用合并索引的策略。舉個(gè)簡單的例子乐尊,在一張電影演員表中戚丸,在actor_id和film_id兩個(gè)列上都建立了獨(dú)立的索引,然后有如下查詢:select film_id,actor_id from film_actor where actor_id = 1 orfilm_id = 1。老版本的MySQL會(huì)隨機(jī)選擇一個(gè)索引限府,但新版本做如下的優(yōu)化:
select film_id,actor_id from film_actor where actor_id = 1 unionall select film_id,actor_id from film_actor where film_id = 1 and actor_id<> 1
當(dāng)出現(xiàn)多個(gè)索引做相交操作時(shí)(多個(gè)AND條件)夺颤,通常來說一個(gè)包含所有相關(guān)列的索引要優(yōu)于多個(gè)獨(dú)立索引。
當(dāng)出現(xiàn)多個(gè)索引做聯(lián)合操作時(shí)(多個(gè)OR條件)胁勺,對(duì)結(jié)果集的合并世澜、排序等操作需要耗費(fèi)大量的CPU和內(nèi)存資源,特別是當(dāng)其中的某些索引的選擇性不高署穗,需要返回合并大量數(shù)據(jù)時(shí)寥裂,查詢成本更高。所以這種情況下還不如走全表掃描案疲。
因此explain時(shí)如果發(fā)現(xiàn)有索引合并(Extra字段出現(xiàn)Using union)封恰,應(yīng)該好好檢查一下查詢和表結(jié)構(gòu)是不是已經(jīng)是最優(yōu)的,如果查詢和表都沒有問題褐啡,那只能說明索引建的非常糟糕诺舔,應(yīng)當(dāng)慎重考慮索引是否合適,有可能一個(gè)包含所有相關(guān)列的多列索引更適合春贸。
前面我們提到過索引如何組織數(shù)據(jù)存儲(chǔ)的混萝,從圖中可以看到多列索引時(shí),索引的順序?qū)τ诓樵兪侵陵P(guān)重要的萍恕,很明顯應(yīng)該把選擇性更高的字段放到索引的前面,這樣通過第一個(gè)字段就可以過濾掉大多數(shù)不符合條件的數(shù)據(jù)车要。
索引選擇性是指不重復(fù)的索引值和數(shù)據(jù)表的總記錄數(shù)的比值允粤,選擇性越高查詢效率越高,因?yàn)檫x擇性越高的索引可以讓MySQL在查詢時(shí)過濾掉更多的行翼岁。唯一索引的選擇性是1类垫,這是最好的索引選擇性,性能也是最好的琅坡。
理解索引選擇性的概念后悉患,就不難確定哪個(gè)字段的選擇性較高了,查一下就知道了榆俺,比如:SELECT * FROM payment where staff_id = 2 and customer_id = 584是應(yīng)該創(chuàng)建(staff_id,customer_id)的索引還是應(yīng)該顛倒一下順序售躁?執(zhí)行下面的查詢,哪個(gè)字段的選擇性更接近1就把哪個(gè)字段索引前面就好茴晋。select count(distinct staff_id)/count(*) as staff_id_selectivity,count(distinct customer_id)/count(*) as customer_id_selectivity, count(*) frompayment
多數(shù)情況下使用這個(gè)原則沒有任何問題陪捷,但仍然注意你的數(shù)據(jù)中是否存在一些特殊情況。舉個(gè)簡單的例子诺擅,比如要查詢某個(gè)用戶組下有過交易的用戶信息:select user_id from trade where user_group_id = 1 and trade_amount> 0
MySQL為這個(gè)查詢選擇了索引(user_group_id,trade_amount)市袖,如果不考慮特殊情況,這看起來沒有任何問題烁涌,但實(shí)際情況是這張表的大多數(shù)數(shù)據(jù)都是從老系統(tǒng)中遷移過來的苍碟,由于新老系統(tǒng)的數(shù)據(jù)不兼容酒觅,所以就給老系統(tǒng)遷移過來的數(shù)據(jù)賦予了一個(gè)默認(rèn)的用戶組。這種情況下微峰,通過索引掃描的行數(shù)跟全表掃描基本沒什么區(qū)別舷丹,索引也就起不到任何作用。
推廣開來說县忌,經(jīng)驗(yàn)法則和推論在多數(shù)情況下是有用的掂榔,可以指導(dǎo)我們開發(fā)和設(shè)計(jì),但實(shí)際情況往往會(huì)更復(fù)雜症杏,實(shí)際業(yè)務(wù)場(chǎng)景下的某些特殊情況可能會(huì)摧毀你的整個(gè)設(shè)計(jì)装获。
????? 避免多個(gè)范圍條件
實(shí)際開發(fā)中,我們會(huì)經(jīng)常使用多個(gè)范圍條件厉颤,比如想查詢某個(gè)時(shí)間段內(nèi)登錄過的用戶:select user.* from user where login_time > '2017-04-01' and agebetween 18 and 30穴豫;這個(gè)查詢有一個(gè)問題:它有兩個(gè)范圍條件,login_time列和age列逼友,MySQL可以使用login_time列的索引或者age列的索引精肃,但無法同時(shí)使用它們。
????? 覆蓋索引
如果一個(gè)索引包含或者說覆蓋所有需要查詢的字段的值帜乞,那么就沒有必要再回表查詢司抱,這就稱為覆蓋索引。覆蓋索引是非常有用的工具黎烈,可以極大的提高性能习柠,因?yàn)椴樵冎恍枰獟呙杷饕龝?huì)帶來許多好處:
索引條目遠(yuǎn)小于數(shù)據(jù)行大小,如果只讀取索引照棋,極大減少數(shù)據(jù)訪問量
索引是有按照列值順序存儲(chǔ)的资溃,對(duì)于I/O密集型的范圍查詢要比隨機(jī)從磁盤讀取每一行數(shù)據(jù)的IO要少的多
??????使用索引掃描來排序
MySQL有兩種方式可以生產(chǎn)有序的結(jié)果集,其一是對(duì)結(jié)果集進(jìn)行排序的操作烈炭,其二是按照索引順序掃描得出的結(jié)果自然是有序的溶锭。如果explain的結(jié)果中type列的值為index表示使用了索引掃描來做排序。
掃描索引本身很快符隙,因?yàn)橹恍枰獜囊粭l索引記錄移動(dòng)到相鄰的下一條記錄趴捅。但如果索引本身不能覆蓋所有需要查詢的列,那么就不得不每掃描一條索引記錄就回表查詢一次對(duì)應(yīng)的行膏执。這個(gè)讀取操作基本上是隨機(jī)I/O驻售,因此按照索引順序讀取數(shù)據(jù)的速度通常要比順序地全表掃描要慢。在設(shè)計(jì)索引時(shí)更米,如果一個(gè)索引既能夠滿足排序欺栗,又滿足查詢,是最好的。
只有當(dāng)索引的列順序和ORDER BY子句的順序完全一致迟几,并且所有列的排序方向也一樣時(shí)消请,才能夠使用索引來對(duì)結(jié)果做排序。如果查詢需要關(guān)聯(lián)多張表类腮,則只有ORDER BY子句引用的字段全部為第一張表時(shí)臊泰,才能使用索引做排序。ORDER BY子句和查詢的限制是一樣的蚜枢,都要滿足最左前綴的要求(有一種情況例外缸逃,就是最左的列被指定為常數(shù),下面是一個(gè)簡單的示例)厂抽,其他情況下都需要執(zhí)行排序操作需频,而無法利用索引排序。
// 最左列為常數(shù)筷凤,索引:(date,staff_id,customer_id)select staff_id,customer_id from demowhere date = '2015-06-01' order by staff_id,customer_id
??????冗余和重復(fù)索引
冗余索引是指在相同的列上按照相同的順序創(chuàng)建的相同類型的索引昭殉,應(yīng)當(dāng)盡量避免這種索引,發(fā)現(xiàn)后立即刪除藐守。比如有一個(gè)索引(A,B)挪丢,再創(chuàng)建索引(A)就是冗余索引。冗余索引經(jīng)常發(fā)生在為表添加新索引時(shí)卢厂,比如有人新建了索引(A,B)乾蓬,但這個(gè)索引不是擴(kuò)展已有的索引(A)。
大多數(shù)情況下都應(yīng)該盡量擴(kuò)展已有的索引而不是創(chuàng)建新索引慎恒。但有極少情況下出現(xiàn)性能方面的考慮需要冗余索引巢块,比如擴(kuò)展已有索引而導(dǎo)致其變得過大,從而影響到其他使用該索引的查詢巧号。
??????刪除長期未使用的索引
定期刪除一些長時(shí)間未使用過的索引是一個(gè)非常好的習(xí)慣。
關(guān)于索引這個(gè)話題打算就此打住姥闭,最后要說一句丹鸿,索引并不總是最好的工具,只有當(dāng)索引幫助提高查詢速度帶來的好處大于其帶來的額外工作時(shí)棚品,索引才是有效的靠欢。對(duì)于非常小的表,簡單的全表掃描更高效铜跑。對(duì)于中到大型的表门怪,索引就非常有效。對(duì)于超大型的表锅纺,建立和維護(hù)索引的代價(jià)隨之增長掷空,這時(shí)候其他技術(shù)也許更有效,比如分區(qū)表。最后的最后坦弟,explain后再提測(cè)是一種美德护锤。
3、特定類型查詢優(yōu)化
? ??? 優(yōu)化COUNT()查詢
COUNT()可能是被大家誤解最多的函數(shù)了酿傍,它有兩種不同的作用烙懦,其一是統(tǒng)計(jì)某個(gè)列值的數(shù)量,其二是統(tǒng)計(jì)行數(shù)赤炒。統(tǒng)計(jì)列值時(shí)氯析,要求列值是非空的,它不會(huì)統(tǒng)計(jì)NULL莺褒。如果確認(rèn)括號(hào)中的表達(dá)式不可能為空時(shí)掩缓,實(shí)際上就是在統(tǒng)計(jì)行數(shù)。最簡單的就是當(dāng)使用COUNT(*)時(shí)癣朗,并不是我們所想象的那樣擴(kuò)展成所有的列拾因,實(shí)際上,它會(huì)忽略所有的列而直接統(tǒng)計(jì)行數(shù)旷余。
我們最常見的誤解也就在這兒绢记,在括號(hào)內(nèi)指定了一列卻希望統(tǒng)計(jì)結(jié)果是行數(shù),而且還常常誤以為前者的性能會(huì)更好正卧。但實(shí)際并非這樣蠢熄,如果要統(tǒng)計(jì)行數(shù),直接使用COUNT(*)炉旷,意義清晰签孔,且性能更好。
有時(shí)候某些業(yè)務(wù)場(chǎng)景并不需要完全精確的COUNT值窘行,可以用近似值來代替饥追,EXPLAIN出來的行數(shù)就是一個(gè)不錯(cuò)的近似值,而且執(zhí)行EXPLAIN并不需要真正地去執(zhí)行查詢罐盔,所以成本非常低但绕。通常來說,執(zhí)行COUNT()都需要掃描大量的行才能獲取到精確的數(shù)據(jù)惶看,因此很難優(yōu)化捏顺,MySQL層面還能做得也就只有覆蓋索引了。如果不還能解決問題纬黎,只有從架構(gòu)層面解決了幅骄,比如添加匯總表,或者使用redis這樣的外部緩存系統(tǒng)本今。
? ????優(yōu)化關(guān)聯(lián)查詢
在大數(shù)據(jù)場(chǎng)景下拆座,表與表之間通過一個(gè)冗余字段來關(guān)聯(lián)主巍,要比直接使用JOIN有更好的性能。如果確實(shí)需要使用關(guān)聯(lián)查詢的情況下懂拾,需要特別注意的是:確保ON和USING字句中的列上有索引煤禽。在創(chuàng)建索引的時(shí)候就要考慮到關(guān)聯(lián)的順序。當(dāng)表A和表B用列c關(guān)聯(lián)的時(shí)候岖赋,如果優(yōu)化器關(guān)聯(lián)的順序是A檬果、B,那么就不需要在A表的對(duì)應(yīng)列上創(chuàng)建索引唐断。沒有用到的索引會(huì)帶來額外的負(fù)擔(dān)选脊,一般來說,除非有其他理由脸甘,只需要在關(guān)聯(lián)順序中的第二張表的相應(yīng)列上創(chuàng)建索引(具體原因下文分析)恳啥。確保任何的GROUP BY和ORDER BY中的表達(dá)式只涉及到一個(gè)表中的列,這樣MySQL才有可能使用索引來優(yōu)化丹诀。
要理解優(yōu)化關(guān)聯(lián)查詢的第一個(gè)技巧钝的,就需要理解MySQL是如何執(zhí)行關(guān)聯(lián)查詢的。當(dāng)前MySQL關(guān)聯(lián)執(zhí)行的策略非常簡單铆遭,它對(duì)任何的關(guān)聯(lián)都執(zhí)行嵌套循環(huán)關(guān)聯(lián)操作硝桩,即先在一個(gè)表中循環(huán)取出單條數(shù)據(jù),然后在嵌套循環(huán)到下一個(gè)表中尋找匹配的行枚荣,依次下去碗脊,直到找到所有表中匹配的行為為止。然后根據(jù)各個(gè)表匹配的行橄妆,返回查詢中需要的各個(gè)列衙伶。
太抽象了?以上面的示例來說明害碾,比如有這樣的一個(gè)查詢:SELECT A.xx,B.yy FROM A INNER JOIN B USING(c)WHERE A.xx IN (5,6)
假設(shè)MySQL按照查詢中的關(guān)聯(lián)順序A矢劲、B來進(jìn)行關(guān)聯(lián)操作,那么可以用下面的偽代碼表示MySQL如何完成這個(gè)查詢:
可以看到慌随,最外層的查詢是根據(jù)A.xx列來查詢的卧须,A.c上如果有索引的話,整個(gè)關(guān)聯(lián)查詢也不會(huì)使用儒陨。再看內(nèi)層的查詢,很明顯B.c上如果有索引的話笋籽,能夠加速查詢蹦漠,因此只需要在關(guān)聯(lián)順序中的第二張表的相應(yīng)列上創(chuàng)建索引即可。
? ??? 優(yōu)化LIMIT分頁
當(dāng)需要分頁操作時(shí)车海,通常會(huì)使用LIMIT加上偏移量的辦法實(shí)現(xiàn)笛园,同時(shí)加上合適的ORDER BY字句隘击。如果有對(duì)應(yīng)的索引,通常效率會(huì)不錯(cuò)研铆,否則埋同,MySQL需要做大量的文件排序操作。
一個(gè)常見的問題是當(dāng)偏移量非常大的時(shí)候棵红,比如:LIMIT 10000 20這樣的查詢凶赁,MySQL需要查詢10020條記錄然后只返回20條記錄,前面的10000條都將被拋棄逆甜,這樣的代價(jià)非常高虱肄。
優(yōu)化這種查詢一個(gè)最簡單的辦法就是盡可能的使用覆蓋索引掃描,而不是查詢所有的列交煞。然后根據(jù)需要做一次關(guān)聯(lián)查詢?cè)俜祷厮械牧杏搅?duì)于偏移量很大時(shí),這樣做的效率會(huì)提升非常大素征〖叮考慮下面的查詢:SELECT film_id,description FROM film ORDER BY title LIMIT 50,5;
如果這張表非常大,那么這個(gè)查詢最好改成下面的樣子:
SELECT film.film_id,film.descriptionFROM film INNER JOIN ( SELECTfilm_id FROM film ORDER BY title LIMIT 50,5) AS tmp USING(film_id);
這里的延遲關(guān)聯(lián)將大大提升查詢效率御毅,讓MySQL掃描盡可能少的頁面根欧,獲取需要訪問的記錄后在根據(jù)關(guān)聯(lián)列回原表查詢所需要的列。有時(shí)候如果可以使用書簽記錄上次取數(shù)據(jù)的位置亚享,那么下次就可以直接從該書簽記錄的位置開始掃描咽块,這樣就可以避免使用OFFSET,比如下面的查詢:SELECT id FROM t LIMIT 10000, 10;? 改為:SELECT id FROM t WHERE id > 10000 LIMIT 10;
其他優(yōu)化的辦法還包括使用預(yù)先計(jì)算的匯總表欺税,或者關(guān)聯(lián)到一個(gè)冗余表侈沪,冗余表中只包含主鍵列和需要做排序的列。
??????優(yōu)化UNION
MySQL處理UNION的策略是先創(chuàng)建臨時(shí)表晚凿,然后再把各個(gè)查詢結(jié)果插入到臨時(shí)表中亭罪,最后再來做查詢法挨。因此很多優(yōu)化策略在UNION查詢中都沒有辦法很好的時(shí)候饥漫。經(jīng)常需要手動(dòng)將WHERE、LIMIT唆阿、ORDER BY等字句“下推”到各個(gè)子查詢中燥筷,以便優(yōu)化器可以充分利用這些條件先優(yōu)化箩祥。
除非確實(shí)需要服務(wù)器去重,否則就一定要使用UNION ALL肆氓,如果沒有ALL關(guān)鍵字袍祖,MySQL會(huì)給臨時(shí)表加上DISTINCT選項(xiàng),這會(huì)導(dǎo)致整個(gè)臨時(shí)表的數(shù)據(jù)做唯一性檢查谢揪,這樣做的代價(jià)非常高蕉陋。當(dāng)然即使使用ALL關(guān)鍵字捐凭,MySQL總是將結(jié)果放入臨時(shí)表,然后再讀出凳鬓,再返回給客戶端茁肠。雖然很多時(shí)候沒有這個(gè)必要,比如有時(shí)候可以直接把每個(gè)子查詢的結(jié)果返回給客戶端缩举。
結(jié)語
理解查詢是如何執(zhí)行以及時(shí)間都消耗在哪些地方垦梆,再加上一些優(yōu)化過程的知識(shí),可以幫助大家更好的理解MySQL蚁孔,理解常見優(yōu)化技巧背后的原理奶赔。希望本文中的原理、示例能夠幫助大家更好的將理論和實(shí)踐聯(lián)系起來杠氢,更多的將理論知識(shí)運(yùn)用到實(shí)踐中站刑。
其他也沒啥說的了,給大家留兩個(gè)思考題吧鼻百,可以在腦袋里想想答案绞旅,這也是大家經(jīng)常掛在嘴邊的,但很少有人會(huì)思考為什么:
有非常多的程序員在分享時(shí)都會(huì)拋出這樣一個(gè)觀點(diǎn):盡可能不要使用存儲(chǔ)過程温艇,存儲(chǔ)過程非常不容易維護(hù)因悲,也會(huì)增加使用成本,應(yīng)該把業(yè)務(wù)邏輯放到客戶端勺爱。既然客戶端都能干這些事晃琳,那為什么還要存儲(chǔ)過程?
JOIN本身也挺方便的琐鲁,直接查詢就好了卫旱,為什么還需要視圖呢?
參考: