探究MySQL優(yōu)化器對(duì)索引和JOIN順序的選擇
本文通過(guò)一個(gè)案例來(lái)看看MySQL優(yōu)化器如何選擇索引和JOIN順序。表結(jié)構(gòu)和數(shù)據(jù)準(zhǔn)備參考本文最后部分"測(cè)試環(huán)境"。這里主要介紹MySQL優(yōu)化器的主要執(zhí)行流程,而不是介紹一個(gè)優(yōu)化器的各個(gè)組件(這是另一個(gè)話題)。
我們知道,MySQL優(yōu)化器只有兩個(gè)自由度:順序選擇;單表訪問(wèn)方式;這里將詳細(xì)剖析下面的SQL,看看MySQL優(yōu)化器如何做出每一步的選擇。
explain select * from employee as A,department as B where A.LastName = 'zhou' and B.DepartmentID = A.DepartmentID and B.DepartmentName = 'TBX';
1. 可能的選擇
這里看到JOIN的順序可以是A|B或者B|A,單表訪問(wèn)方式也有多種,對(duì)于A表可以選擇:全表掃描和索引`IND_L_D`(A.LastName = 'zhou')或者`IND_DID`(B.DepartmentID = A.DepartmentID)。對(duì)于B也有三個(gè)選擇:全表掃描、索引IND_D、IND_DN。
2. MySQL優(yōu)化器如何做
2.1 概述
MySQL優(yōu)化器主要工作包括以下幾部分:Query Rewrite(包括Outer Join轉(zhuǎn)換等)、const table detection、range analysis、JOIN optimization(順序和訪問(wèn)方式選擇)、plan refinement。這個(gè)案例從range analysis開始。
2.2 range analysis
這部分包括所有Range和index merge成本評(píng)估(參考1 參考2)。這里,等值表達(dá)式也是一個(gè)range,所以這里會(huì)評(píng)估其成本,計(jì)算出found records(表示對(duì)應(yīng)的等值表達(dá)式,大概會(huì)選擇出多少條記錄)。
本案例中,range analysis會(huì)針對(duì)A表的條件A.LastName = 'zhou'和B表的B.DepartmentName = 'TBX'分別做分析。其中:
表A A.LastName = 'zhou' found records: 51
表B B.DepartmentName = 'TBX' found records: 1
這兩個(gè)條件都不是range,但是這里計(jì)算的值仍然會(huì)存儲(chǔ),在后面的ref訪問(wèn)方式評(píng)估的時(shí)候使用。這里的值是根據(jù)records_in_range接口返回,而對(duì)于InnoDB每次調(diào)用這個(gè)函數(shù)都會(huì)進(jìn)行一次索引頁(yè)的采樣,這是一個(gè)很消耗性能的操作,對(duì)于很多其他的關(guān)系數(shù)據(jù)庫(kù)是使用"直方圖"的統(tǒng)計(jì)數(shù)據(jù)來(lái)避免這次操作(相信MariaDB后續(xù)版本也將實(shí)現(xiàn)直方圖統(tǒng)計(jì)信息)。
2.3 順序和訪問(wèn)方式的選擇:窮舉
MySQL通過(guò)枚舉所有的left-deep樹(也可以說(shuō)所有的left-deep樹就是整個(gè)MySQL優(yōu)化器的搜索空間),來(lái)找到最優(yōu)的執(zhí)行順序和訪問(wèn)方式。
2.3.1 排序
優(yōu)化器先根據(jù)found records對(duì)所有表進(jìn)行一個(gè)排序,記錄少的放前面。所以,這里順序是B、A。
2.3.2 greedy search
當(dāng)表的數(shù)量較少(少于search_depth,默認(rèn)是63)的時(shí)候,這里直接蛻化為一個(gè)窮舉搜索,優(yōu)化器將窮舉所有的left-deep樹找到最優(yōu)的執(zhí)行計(jì)劃。另外,優(yōu)化器為了減少因?yàn)樗阉骺臻g龐大帶來(lái)巨大的窮舉消耗,所以使用了一個(gè)"偷懶"的參數(shù)prune_level(默認(rèn)打開),具體如何"偷懶",可以參考JOIN順序選擇的復(fù)雜度。不過(guò)至少需要有三個(gè)表以上的關(guān)聯(lián)才會(huì)有"偷懶",所以本案例不適用。
2.3.3 窮舉
JOIN的第一個(gè)表可以是:A或者B;如果第一個(gè)表選擇了A,第二個(gè)表可以選擇B;如果第一個(gè)表選擇了B,第二個(gè)表可以選擇A;
因?yàn)榍懊娴呐判?,B表的found records更少,所以JOIN順序窮舉時(shí)的第一個(gè)表先選擇B(這個(gè)是有講究的)。
(*) 選擇第一個(gè)JOIN的表為B
(**) 確定B表的訪問(wèn)方式
因?yàn)锽表為第一個(gè)表,所以無(wú)法使用索引IND_D(B.DepartmentID = A.DepartmentID),而只能使用IND_DN(B.DepartmentName = 'TBX')
使用IND_DN索引的成本計(jì)算:1.2;其中IO成本為1。
是否使用全表掃描:這里會(huì)比較使用索引的IO成本和全表掃描的IO成本,前者為1,后者為2;所以忽略全表掃描
所以,B表的訪問(wèn)方式ref,使用索引IND_D
(**) 從剩余的表中窮舉選出第二個(gè)JOIN的表,這里剩余的表為:A
(**) 將A表加入JOIN,并確定其訪問(wèn)方式
可以使用的索引為:`IND_L_D`(A.LastName = 'zhou')或者`IND_DID`(B.DepartmentID = A.DepartmentID)
依次計(jì)算使用索引IND_L_D、IND_DID的成本:
(***) IND_L_D A.LastName = 'zhou'
在range analysis階段給出了A.LastName = 'zhou'對(duì)應(yīng)的記錄約為:51。
所以,計(jì)算IO成本為:51;ref做IO成本計(jì)算時(shí)會(huì)做一次修正,將其修正為worst_seek(參考)
修正后IO成本為:15,總成本為:25.2
(***) IND_DID B.DepartmentID = A.DepartmentID
這是一個(gè)需要知道前面表的結(jié)果,才能計(jì)算的成本。所以range analysis是無(wú)法分析的
這里,我們看到前面表為B,found_record是1,所以A.DepartmentID只需要對(duì)應(yīng)一條記錄就可以了
因?yàn)榫唧w取值不知道,也沒有直方圖,所以只能簡(jiǎn)單依據(jù)索引統(tǒng)計(jì)信息來(lái)計(jì)算:
索引IND_DID的列A.DepartmentID的Cardinality為1349,全表記錄數(shù)為1349
所以,每一個(gè)值對(duì)應(yīng)一條記錄,而前面表B只有一條記錄,所以這里的found_record計(jì)算為1*1 = 1
所以IO成本為:1,總成本為1.2
(***) IND_L_D成本為25.2;IND_DID成本為1.2,所以選擇后者為當(dāng)前表的訪問(wèn)方式
(**) 確定A使用索引IND_DID,訪問(wèn)方式為ref
(**) JOIN順序B|A,總成本為:1.2+1.2 = 2.4
(*) 選擇第一個(gè)JOIN的表為A
(**) 確定A表的訪問(wèn)方式
因?yàn)锳表是第一個(gè)表,所以無(wú)法使用索引`IND_DID`(B.DepartmentID = A.DepartmentID)
那么只能使用索引`IND_L_D`(A.LastName = 'zhou')
使用IND_L_D索引的成本計(jì)算,總成本為25.2;參考前面計(jì)算;
(**) 這里訪問(wèn)A表的成本已經(jīng)是25.2,比之前的最優(yōu)成本2.4要大,忽略該順序
所以,這次窮舉搜索到此結(jié)束
把上面的過(guò)程簡(jiǎn)化如下:
(*) 選擇第一個(gè)JOIN的表為B
(**) 確定B表的訪問(wèn)方式
(**) 從剩余的表中窮舉選出第二個(gè)JOIN的表,這里剩余的表為:A
(**) 將A表加入JOIN,并確定其訪問(wèn)方式
(***) IND_L_D A.LastName = 'zhou'
(***) IND_DID B.DepartmentID = A.DepartmentID
(***) IND_L_D成本為25.2;IND_DID成本為1.2,所以選擇后者為當(dāng)前表的訪問(wèn)方式
(**) 確定A使用索引IND_DID,訪問(wèn)方式為ref
(**) JOIN順序B|A,總成本為:1.2+1.2 = 2.4
(*) 選擇第一個(gè)JOIN的表為A
(**) 確定A表的訪問(wèn)方式
(**) 這里訪問(wèn)A表的成本已經(jīng)是25.2,比之前的最優(yōu)成本2.4要大,忽略該順序
至此,MySQL優(yōu)化器就確定了所有表的最佳JOIN順序和訪問(wèn)方式。
3. 測(cè)試環(huán)境
MySQL: 5.1.48-debug-log innodb plugin 1.0.9 CREATE TABLE `department` ( `DepartmentID` int(11) DEFAULT NULL, `DepartmentName` varchar(20) DEFAULT NULL, KEY `IND_D` (`DepartmentID`), KEY `IND_DN` (`DepartmentName`) ) ENGINE=InnoDB DEFAULT CHARSET=gbk; CREATE TABLE `employee` ( `LastName` varchar(20) DEFAULT NULL, `DepartmentID` int(11) DEFAULT NULL, KEY `IND_L_D` (`LastName`), KEY `IND_DID` (`DepartmentID`) ) ENGINE=InnoDB DEFAULT CHARSET=gbk; for i in `seq 1 1000` ; do mysql -vvv -uroot test -e 'insert into department values (600000*rand(),repeat(char(65+rand()*58),rand()*20))'; done for i in `seq 1 1000` ; do mysql -vvv -uroot test -e 'insert into employee values (repeat(char(65+rand()*58),rand()*20),600000*rand())'; done for i in `seq 1 50` ; do mysql -vvv -uroot test -e 'insert into employee values ("zhou",27760)'; done for i in `seq 1 200` ; do mysql -vvv -uroot test -e 'insert into employee values (repeat(char(65+rand()*58),rand()*20),27760)'; done for i in `seq 1 1` ; do mysql -vvv -uroot test -e 'insert into department values (27760,"TBX")'; done show index from employee; +----------+------------+----------+--------------+--------------+-----------+-------------+----------+--------+------+------------+---------+ | Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | +----------+------------+----------+--------------+--------------+-----------+-------------+----------+--------+------+------------+---------+ | employee | 1 | IND_L_D | 1 | LastName | A | 1349 | NULL | NULL | YES | BTREE | | | employee | 1 | IND_DID | 1 | DepartmentID | A | 1349 | NULL | NULL | YES | BTREE | | +----------+------------+----------+--------------+--------------+-----------+-------------+----------+--------+------+------------+---------+ show index from department; +------------+------------+----------+--------------+----------------+-----------+-------------+----------+--------+------+------------+---------+ | Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | +------------+------------+----------+--------------+----------------+-----------+-------------+----------+--------+------+------------+---------+ | department | 1 | IND_D | 1 | DepartmentID | A | 1001 | NULL | NULL | YES | BTREE | | | department | 1 | IND_DN | 1 | DepartmentName | A | 1001 | NULL | NULL | YES | BTREE | | +------------+------------+----------+--------------+----------------+-----------+-------------+----------+--------+------+------------+---------+
4. 構(gòu)造一個(gè)Bad case
因?yàn)殛P(guān)聯(lián)條件中MySQL使用索引統(tǒng)計(jì)信息做成本預(yù)估,所以數(shù)據(jù)分布不均勻的時(shí)候,就容易做出錯(cuò)誤的判斷。簡(jiǎn)單的我們構(gòu)造下面的案例:
表和索引結(jié)構(gòu)不變,按照下面的方式構(gòu)造數(shù)據(jù):
for i in `seq 1 10000` ; do mysql -uroot test -e 'insert into department values (600000*rand(),repeat(char(65+rand()*58),rand()*20))'; done for i in `seq 1 10000` ; do mysql -uroot test -e 'insert into employee values (repeat(char(65+rand()*58),rand()*20),600000*rand())'; done for i in `seq 1 1` ; do mysql -uroot test -e 'insert into employee values ("zhou",27760)'; done for i in `seq 1 10` ; do mysql -uroot test -e 'insert into department values (27760,"TBX")'; done for i in `seq 1 1000` ; do mysql -uroot test -e 'insert into department values (27760,repeat(char(65+rand()*58),rand()*20))'; done explain select * from employee as A,department as B where A.LastName = 'zhou' and B.DepartmentID = A.DepartmentID and B.DepartmentName = 'TBX'; +----+-------------+-------+------+-----------------+---------+---------+---------------------+------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-------+------+-----------------+---------+---------+---------------------+------+-------------+ | 1 | SIMPLE | A | ref | IND_L_D,IND_DID | IND_L_D | 43 | const | 1 | Using where | | 1 | SIMPLE | B | ref | IND_D,IND_DN | IND_D | 5 | test.A.DepartmentID | 1 | Using where | +----+-------------+-------+------+-----------------+---------+---------+---------------------+------+-------------+
可以看到這里,MySQL執(zhí)行計(jì)劃對(duì)表department使用了索引IND_D,那么A表命中一條記錄為(zhou,27760);根據(jù)B.DepartmentID=27760將返回1010條記錄,然后根據(jù)條件DepartmentName = 'TBX'進(jìn)行過(guò)濾。
這里可以看到如果B表選擇索引IND_DN,效果要更好,因?yàn)镈epartmentName = 'TBX'僅僅返回10條記錄,再根據(jù)條件A.DepartmentID=B.DepartmentID過(guò)濾之。
相關(guān)文章
MySQL系列多表連接查詢92及99語(yǔ)法示例詳解教程
這篇文章主要為大家介紹了MySQL系列多表連接查詢92及99語(yǔ)法示例詳解教程,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2021-10-10mysql?sum(if())和count(if())的用法說(shuō)明
這篇文章主要介紹了mysql?sum(if())和count(if())的用法說(shuō)明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-01-01mysql 8.0.18.zip安裝配置方法圖文教程(windows 64位)
這篇文章主要為大家詳細(xì)介紹了mysql 8.0.18.zip安裝配置方法圖文教程,以及卸載以前數(shù)據(jù)庫(kù)的方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-10-10在大數(shù)據(jù)情況下MySQL的一種簡(jiǎn)單分頁(yè)優(yōu)化方法
這篇文章主要介紹了在大數(shù)據(jù)情況下MySQL的一種簡(jiǎn)單分頁(yè)優(yōu)化方法,分頁(yè)優(yōu)化是MySQL優(yōu)化的常用手段之一,需要的朋友可以參考下2015-05-05MySQL實(shí)現(xiàn)主從復(fù)制的原理詳解
這篇文章主要為大家介紹了MySQL的主從復(fù)制是怎么實(shí)現(xiàn)的,文中有相關(guān)的圖文介紹和代碼示例,具有一定的參考價(jià)值,感興趣的同學(xué)跟著小編一起來(lái)學(xué)習(xí)吧2023-07-07MySQL查詢優(yōu)化:用子查詢代替非主鍵連接查詢實(shí)例介紹
對(duì)多的兩張表,一般是一張表的外鍵關(guān)聯(lián)到另一個(gè)表的主鍵,接下來(lái)為大家介紹下用子查詢代替非主鍵連接查詢,感興趣的朋友可以參考下哈,希望對(duì)你有所幫助2013-04-04mysql多個(gè)TimeStamp設(shè)置的方法解讀
timestamp設(shè)置默認(rèn)值是Default CURRENT_TIMESTAMP;timestamp設(shè)置隨著表變化而自動(dòng)更新是ON UPDATE CURRENT_TIMESTAMP;接下來(lái)為您詳細(xì)介紹2012-11-11