18 | 为什么这些SQL语句逻辑相同,性能却差异巨大?
在MySQL中,有很多看上去邏輯相同,但性能卻差異巨大的SQL語(yǔ)句。對(duì)這些語(yǔ)句使用不當(dāng)?shù)脑?#xff0c;就會(huì)不經(jīng)意間導(dǎo)致整個(gè)數(shù)據(jù)庫(kù)的壓力變大。
我今天挑選了三個(gè)這樣的案例和你分享。希望再遇到相似的問(wèn)題時(shí),你可以做到舉一反三、快速解決問(wèn)題。
案例一:條件字段函數(shù)操作
假設(shè)你現(xiàn)在維護(hù)了一個(gè)交易系統(tǒng),其中交易記錄表tradelog包含交易流水號(hào)(tradeid)、交易員id(operator)、交易時(shí)間(t_modified)等字段。為了便于描述,我們先忽略其他字段。這個(gè)表的建表語(yǔ)句如下:
mysql> CREATE TABLE `tradelog` ( `id` int(11) NOT NULL, `tradeid` varchar(32) DEFAULT NULL, `operator` int(11) DEFAULT NULL, `t_modified` datetime DEFAULT NULL, PRIMARY KEY (`id`), KEY `tradeid` (`tradeid`), KEY `t_modified` (`t_modified`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;假設(shè),現(xiàn)在已經(jīng)記錄了從2016年初到2018年底的所有數(shù)據(jù),運(yùn)營(yíng)部門(mén)有一個(gè)需求是,要統(tǒng)計(jì)發(fā)生在所有年份中7月份的交易記錄總數(shù)。這個(gè)邏輯看上去并不復(fù)雜,你的SQL語(yǔ)句可能會(huì)這么寫(xiě):
mysql> select count(*) from tradelog where month(t_modified)=7;由于t_modified字段上有索引,于是你就很放心地在生產(chǎn)庫(kù)中執(zhí)行了這條語(yǔ)句,但卻發(fā)現(xiàn)執(zhí)行了特別久,才返回了結(jié)果。
如果你問(wèn)DBA同事為什么會(huì)出現(xiàn)這樣的情況,他大概會(huì)告訴你:如果對(duì)字段做了函數(shù)計(jì)算,就用不上索引了,這是MySQL的規(guī)定。
現(xiàn)在你已經(jīng)學(xué)過(guò)了InnoDB的索引結(jié)構(gòu)了,可以再追問(wèn)一句為什么?為什么條件是where t_modified='2018-7-1’的時(shí)候可以用上索引,而改成where month(t_modified)=7的時(shí)候就不行了?
下面是這個(gè)t_modified索引的示意圖。方框上面的數(shù)字就是month()函數(shù)對(duì)應(yīng)的值。
圖1 t_modified索引示意圖如果你的SQL語(yǔ)句條件用的是where t_modified='2018-7-1’的話,引擎就會(huì)按照上面綠色箭頭的路線,快速定位到 t_modified='2018-7-1’需要的結(jié)果。
實(shí)際上,B+樹(shù)提供的這個(gè)快速定位能力,來(lái)源于同一層兄弟節(jié)點(diǎn)的有序性。
但是,如果計(jì)算month()函數(shù)的話,你會(huì)看到傳入7的時(shí)候,在樹(shù)的第一層就不知道該怎么辦了。
也就是說(shuō),對(duì)索引字段做函數(shù)操作,可能會(huì)破壞索引值的有序性,因此優(yōu)化器就決定放棄走樹(shù)搜索功能。
需要注意的是,優(yōu)化器并不是要放棄使用這個(gè)索引。
在這個(gè)例子里,放棄了樹(shù)搜索功能,優(yōu)化器可以選擇遍歷主鍵索引,也可以選擇遍歷索引t_modified,優(yōu)化器對(duì)比索引大小后發(fā)現(xiàn),索引t_modified更小,遍歷這個(gè)索引比遍歷主鍵索引來(lái)得更快。因此最終還是會(huì)選擇索引t_modified。
接下來(lái),我們使用explain命令,查看一下這條SQL語(yǔ)句的執(zhí)行結(jié)果。
圖2 explain 結(jié)果key="t_modified"表示的是,使用了t_modified這個(gè)索引;我在測(cè)試表數(shù)據(jù)中插入了10萬(wàn)行數(shù)據(jù),rows=100335,說(shuō)明這條語(yǔ)句掃描了整個(gè)索引的所有值;Extra字段的Using index,表示的是使用了覆蓋索引。
也就是說(shuō),由于在t_modified字段加了month()函數(shù)操作,導(dǎo)致了全索引掃描。為了能夠用上索引的快速定位能力,我們就要把SQL語(yǔ)句改成基于字段本身的范圍查詢。按照下面這個(gè)寫(xiě)法,優(yōu)化器就能按照我們預(yù)期的,用上t_modified索引的快速定位能力了。
mysql> select count(*) from tradelog where -> (t_modified >= '2016-7-1' and t_modified<'2016-8-1') or -> (t_modified >= '2017-7-1' and t_modified<'2017-8-1') or -> (t_modified >= '2018-7-1' and t_modified<'2018-8-1');當(dāng)然,如果你的系統(tǒng)上線時(shí)間更早,或者后面又插入了之后年份的數(shù)據(jù)的話,你就需要再把其他年份補(bǔ)齊。
到這里我給你說(shuō)明了,由于加了month()函數(shù)操作,MySQL無(wú)法再使用索引快速定位功能,而只能使用全索引掃描。
不過(guò)優(yōu)化器在個(gè)問(wèn)題上確實(shí)有“偷懶”行為,即使是對(duì)于不改變有序性的函數(shù),也不會(huì)考慮使用索引。比如,對(duì)于select * from tradelog where id + 1 = 10000這個(gè)SQL語(yǔ)句,這個(gè)加1操作并不會(huì)改變有序性,但是MySQL優(yōu)化器還是不能用id索引快速定位到9999這一行。所以,需要你在寫(xiě)SQL語(yǔ)句的時(shí)候,手動(dòng)改寫(xiě)成 where id = 10000 -1才可以。
案例二:隱式類型轉(zhuǎn)換
接下來(lái)我再跟你說(shuō)一說(shuō),另一個(gè)經(jīng)常讓程序員掉坑里的例子。
我們一起看一下這條SQL語(yǔ)句:
mysql> select * from tradelog where tradeid=110717;交易編號(hào)tradeid這個(gè)字段上,本來(lái)就有索引,但是explain的結(jié)果卻顯示,這條語(yǔ)句需要走全表掃描。你可能也發(fā)現(xiàn)了,tradeid的字段類型是varchar(32),而輸入的參數(shù)卻是整型,所以需要做類型轉(zhuǎn)換。
那么,現(xiàn)在這里就有兩個(gè)問(wèn)題:
數(shù)據(jù)類型轉(zhuǎn)換的規(guī)則是什么?
為什么有數(shù)據(jù)類型轉(zhuǎn)換,就需要走全索引掃描?
先來(lái)看第一個(gè)問(wèn)題,你可能會(huì)說(shuō),數(shù)據(jù)庫(kù)里面類型這么多,這種數(shù)據(jù)類型轉(zhuǎn)換規(guī)則更多,我記不住,應(yīng)該怎么辦呢?
這里有一個(gè)簡(jiǎn)單的方法,看 select “10” > 9的結(jié)果:
如果規(guī)則是“將字符串轉(zhuǎn)成數(shù)字”,那么就是做數(shù)字比較,結(jié)果應(yīng)該是1;
如果規(guī)則是“將數(shù)字轉(zhuǎn)成字符串”,那么就是做字符串比較,結(jié)果應(yīng)該是0。
驗(yàn)證結(jié)果如圖3所示。
圖3 MySQL中字符串和數(shù)字轉(zhuǎn)換的效果示意圖從圖中可知,select “10” > 9返回的是1,所以你就能確認(rèn)MySQL里的轉(zhuǎn)換規(guī)則了:在MySQL中,字符串和數(shù)字做比較的話,是將字符串轉(zhuǎn)換成數(shù)字。
這時(shí),你再看這個(gè)全表掃描的語(yǔ)句:
mysql> select * from tradelog where tradeid=110717;就知道對(duì)于優(yōu)化器來(lái)說(shuō),這個(gè)語(yǔ)句相當(dāng)于:
mysql> select * from tradelog where CAST(tradid AS signed int) = 110717;也就是說(shuō),這條語(yǔ)句觸發(fā)了我們上面說(shuō)到的規(guī)則:對(duì)索引字段做函數(shù)操作,優(yōu)化器會(huì)放棄走樹(shù)搜索功能。
現(xiàn)在,我留給你一個(gè)小問(wèn)題,id的類型是int,如果執(zhí)行下面這個(gè)語(yǔ)句,是否會(huì)導(dǎo)致全表掃描呢?
select * from tradelog where id="83126";你可以先自己分析一下,再到數(shù)據(jù)庫(kù)里面去驗(yàn)證確認(rèn)。
接下來(lái),我們?cè)賮?lái)看一個(gè)稍微復(fù)雜點(diǎn)的例子。
案例三:隱式字符編碼轉(zhuǎn)換
假設(shè)系統(tǒng)里還有另外一個(gè)表trade_detail,用于記錄交易的操作細(xì)節(jié)。為了便于量化分析和復(fù)現(xiàn),我往交易日志表tradelog和交易詳情表trade_detail這兩個(gè)表里插入一些數(shù)據(jù)。
mysql> CREATE TABLE `trade_detail` ( `id` int(11) NOT NULL, `tradeid` varchar(32) DEFAULT NULL, `trade_step` int(11) DEFAULT NULL, /*操作步驟*/ `step_info` varchar(32) DEFAULT NULL, /*步驟信息*/ PRIMARY KEY (`id`), KEY `tradeid` (`tradeid`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;insert into tradelog values(1, 'aaaaaaaa', 1000, now()); insert into tradelog values(2, 'aaaaaaab', 1000, now()); insert into tradelog values(3, 'aaaaaaac', 1000, now());insert into trade_detail values(1, 'aaaaaaaa', 1, 'add'); insert into trade_detail values(2, 'aaaaaaaa', 2, 'update'); insert into trade_detail values(3, 'aaaaaaaa', 3, 'commit'); insert into trade_detail values(4, 'aaaaaaab', 1, 'add'); insert into trade_detail values(5, 'aaaaaaab', 2, 'update'); insert into trade_detail values(6, 'aaaaaaab', 3, 'update again'); insert into trade_detail values(7, 'aaaaaaab', 4, 'commit'); insert into trade_detail values(8, 'aaaaaaac', 1, 'add'); insert into trade_detail values(9, 'aaaaaaac', 2, 'update'); insert into trade_detail values(10, 'aaaaaaac', 3, 'update again'); insert into trade_detail values(11, 'aaaaaaac', 4, 'commit');這時(shí)候,如果要查詢id=2的交易的所有操作步驟信息,SQL語(yǔ)句可以這么寫(xiě):
mysql> select d.* from tradelog l, trade_detail d where d.tradeid=l.tradeid and l.id=2; /*語(yǔ)句Q1*/ 圖4 語(yǔ)句Q1的explain 結(jié)果我們一起來(lái)看下這個(gè)結(jié)果:
第一行顯示優(yōu)化器會(huì)先在交易記錄表tradelog上查到id=2的行,這個(gè)步驟用上了主鍵索引,rows=1表示只掃描一行;
第二行key=NULL,表示沒(méi)有用上交易詳情表trade_detail上的tradeid索引,進(jìn)行了全表掃描。
在這個(gè)執(zhí)行計(jì)劃里,是從tradelog表中取tradeid字段,再去trade_detail表里查詢匹配字段。因此,我們把tradelog稱為驅(qū)動(dòng)表,把trade_detail稱為被驅(qū)動(dòng)表,把tradeid稱為關(guān)聯(lián)字段。
接下來(lái),我們看下這個(gè)explain結(jié)果表示的執(zhí)行流程:
圖5 語(yǔ)句Q1的執(zhí)行過(guò)程圖中:
- 第1步,是根據(jù)id在tradelog表里找到L2這一行;
- 第2步,是從L2中取出tradeid字段的值;
- 第3步,是根據(jù)tradeid值到trade_detail表中查找條件匹配的行。explain的結(jié)果里面第二行的key=NULL表示的就是,這個(gè)過(guò)程是通過(guò)遍歷主鍵索引的方式,一個(gè)一個(gè)地判斷tradeid的值是否匹配。
進(jìn)行到這里,你會(huì)發(fā)現(xiàn)第3步不符合我們的預(yù)期。因?yàn)楸韙rade_detail里tradeid字段上是有索引的,我們本來(lái)是希望通過(guò)使用tradeid索引能夠快速定位到等值的行。但,這里并沒(méi)有。
如果你去問(wèn)DBA同學(xué),他們可能會(huì)告訴你,因?yàn)檫@兩個(gè)表的字符集不同,一個(gè)是utf8,一個(gè)是utf8mb4,所以做表連接查詢的時(shí)候用不上關(guān)聯(lián)字段的索引。這個(gè)回答,也是通常你搜索這個(gè)問(wèn)題時(shí)會(huì)得到的答案。
但是你應(yīng)該再追問(wèn)一下,為什么字符集不同就用不上索引呢?
我們說(shuō)問(wèn)題是出在執(zhí)行步驟的第3步,如果單獨(dú)把這一步改成SQL語(yǔ)句的話,那就是:
mysql> select * from trade_detail where tradeid=$L2.tradeid.value;其中,$L2.tradeid.value的字符集是utf8mb4。
參照前面的兩個(gè)例子,你肯定就想到了,字符集utf8mb4是utf8的超集,所以當(dāng)這兩個(gè)類型的字符串在做比較的時(shí)候,MySQL內(nèi)部的操作是,先把utf8字符串轉(zhuǎn)成utf8mb4字符集,再做比較。
這個(gè)設(shè)定很好理解,utf8mb4是utf8的超集。類似地,在程序設(shè)計(jì)語(yǔ)言里面,做自動(dòng)類型轉(zhuǎn)換的時(shí)候,為了避免數(shù)據(jù)在轉(zhuǎn)換過(guò)程中由于截?cái)鄬?dǎo)致數(shù)據(jù)錯(cuò)誤,也都是“按數(shù)據(jù)長(zhǎng)度增加的方向”進(jìn)行轉(zhuǎn)換的。
因此, 在執(zhí)行上面這個(gè)語(yǔ)句的時(shí)候,需要將被驅(qū)動(dòng)數(shù)據(jù)表里的字段一個(gè)個(gè)地轉(zhuǎn)換成utf8mb4,再跟L2做比較。
也就是說(shuō),實(shí)際上這個(gè)語(yǔ)句等同于下面這個(gè)寫(xiě)法:
select * from trade_detail where CONVERT(traideid USING utf8mb4)=$L2.tradeid.value;CONVERT()函數(shù),在這里的意思是把輸入的字符串轉(zhuǎn)成utf8mb4字符集。
這就再次觸發(fā)了我們上面說(shuō)到的原則:對(duì)索引字段做函數(shù)操作,優(yōu)化器會(huì)放棄走樹(shù)搜索功能。
到這里,你終于明確了,字符集不同只是條件之一,連接過(guò)程中要求在被驅(qū)動(dòng)表的索引字段上加函數(shù)操作,是直接導(dǎo)致對(duì)被驅(qū)動(dòng)表做全表掃描的原因。
作為對(duì)比驗(yàn)證,我給你提另外一個(gè)需求,“查找trade_detail表里id=4的操作,對(duì)應(yīng)的操作者是誰(shuí)”,再來(lái)看下這個(gè)語(yǔ)句和它的執(zhí)行計(jì)劃。
mysql>select l.operator from tradelog l , trade_detail d where d.tradeid=l.tradeid and d.id=4; 圖6 explain 結(jié)果這個(gè)語(yǔ)句里trade_detail 表成了驅(qū)動(dòng)表,但是explain結(jié)果的第二行顯示,這次的查詢操作用上了被驅(qū)動(dòng)表tradelog里的索引(tradeid),掃描行數(shù)是1。
這也是兩個(gè)tradeid字段的join操作,為什么這次能用上被驅(qū)動(dòng)表的tradeid索引呢?我們來(lái)分析一下。
假設(shè)驅(qū)動(dòng)表trade_detail里id=4的行記為R4,那么在連接的時(shí)候(圖5的第3步),被驅(qū)動(dòng)表tradelog上執(zhí)行的就是類似這樣的SQL 語(yǔ)句:
select operator from tradelog where traideid =$R4.tradeid.value;這時(shí)候$R4.tradeid.value的字符集是utf8, 按照字符集轉(zhuǎn)換規(guī)則,要轉(zhuǎn)成utf8mb4,所以這個(gè)過(guò)程就被改寫(xiě)成:
select operator from tradelog where traideid =CONVERT($R4.tradeid.value USING utf8mb4);你看,這里的CONVERT函數(shù)是加在輸入?yún)?shù)上的,這樣就可以用上被驅(qū)動(dòng)表的traideid索引。
理解了原理以后,就可以用來(lái)指導(dǎo)操作了。如果要優(yōu)化語(yǔ)句
select d.* from tradelog l, trade_detail d where d.tradeid=l.tradeid and l.id=2;的執(zhí)行過(guò)程,有兩種做法:
- 比較常見(jiàn)的優(yōu)化方法是,把trade_detail表上的tradeid字段的字符集也改成utf8mb4,這樣就沒(méi)有字符集轉(zhuǎn)換的問(wèn)題了。
- 如果能夠修改字段的字符集的話,是最好不過(guò)了。但如果數(shù)據(jù)量比較大, 或者業(yè)務(wù)上暫時(shí)不能做這個(gè)DDL的話,那就只能采用修改SQL語(yǔ)句的方法了。
這里,我主動(dòng)把 l.tradeid轉(zhuǎn)成utf8,就避免了被驅(qū)動(dòng)表上的字符編碼轉(zhuǎn)換,從explain結(jié)果可以看到,這次索引走對(duì)了。
小結(jié)
今天我給你舉了三個(gè)例子,其實(shí)是在說(shuō)同一件事兒,即:對(duì)索引字段做函數(shù)操作,可能會(huì)破壞索引值的有序性,因此優(yōu)化器就決定放棄走樹(shù)搜索功能。
第二個(gè)例子是隱式類型轉(zhuǎn)換,第三個(gè)例子是隱式字符編碼轉(zhuǎn)換,它們都跟第一個(gè)例子一樣,因?yàn)橐笤谒饕侄紊献龊瘮?shù)操作而導(dǎo)致了全索引掃描。
MySQL的優(yōu)化器確實(shí)有“偷懶”的嫌疑,即使簡(jiǎn)單地把where id+1=1000改寫(xiě)成where id=1000-1就能夠用上索引快速查找,也不會(huì)主動(dòng)做這個(gè)語(yǔ)句重寫(xiě)。
因此,每次你的業(yè)務(wù)代碼升級(jí)時(shí),把可能出現(xiàn)的、新的SQL語(yǔ)句explain一下,是一個(gè)很好的習(xí)慣。
最后,又到了思考題時(shí)間。
今天我留給你的課后問(wèn)題是,你遇到過(guò)別的、類似今天我們提到的性能問(wèn)題嗎?你認(rèn)為原因是什么,又是怎么解決的呢?
你可以把你經(jīng)歷和分析寫(xiě)在留言區(qū)里,我會(huì)在下一篇文章的末尾選取有趣的評(píng)論跟大家一起分享和分析。感謝你的收聽(tīng),也歡迎你把這篇文章分享給更多的朋友一起閱讀。
上期問(wèn)題時(shí)間
我在上篇文章的最后,留給你的問(wèn)題是:我們文章中最后的一個(gè)方案是,通過(guò)三次limit Y,1 來(lái)得到需要的數(shù)據(jù),你覺(jué)得有沒(méi)有進(jìn)一步的優(yōu)化方法。
這里我給出一種方法,取Y1、Y2和Y3里面最大的一個(gè)數(shù),記為M,最小的一個(gè)數(shù)記為N,然后執(zhí)行下面這條SQL語(yǔ)句:
mysql> select * from t limit N, M-N+1;再加上取整個(gè)表總行數(shù)的C行,這個(gè)方案的掃描行數(shù)總共只需要C+M+1行。
當(dāng)然也可以先取回id值,在應(yīng)用中確定了三個(gè)id值以后,再執(zhí)行三次where id=X的語(yǔ)句也是可以的。@倪大人 同學(xué)在評(píng)論區(qū)就提到了這個(gè)方法。
?
轉(zhuǎn)載于:https://www.cnblogs.com/a-phper/p/10313922.html
總結(jié)
以上是生活随笔為你收集整理的18 | 为什么这些SQL语句逻辑相同,性能却差异巨大?的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: vue - 响应式原理梳理(一)
- 下一篇: 流感疫情严重 港府宣布全港幼儿园26日起