這篇文章主要講解了“MySQL中為什么簡單的一行查詢也會慢”,文中的講解內(nèi)容簡單清晰,易于學(xué)習(xí)與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學(xué)習(xí)“MySQL中為什么簡單的一行查詢也會慢”吧!
成都創(chuàng)新互聯(lián)主要從事網(wǎng)站設(shè)計、成都網(wǎng)站制作、網(wǎng)頁設(shè)計、企業(yè)做網(wǎng)站、公司建網(wǎng)站等業(yè)務(wù)。立足成都服務(wù)乳山,10余年網(wǎng)站建設(shè)經(jīng)驗,價格優(yōu)惠、服務(wù)專業(yè),歡迎來電咨詢建站服務(wù):028-86922220
在MySQL中,有很多看上去邏輯相同,但性能卻差異巨大的SQL語句。對這些語句使用不當(dāng)?shù)脑挘蜁唤?jīng)意間導(dǎo)致整個數(shù)據(jù)庫的壓力變大。
假設(shè)你現(xiàn)在維護了一個交易系統(tǒng),其中交易記錄表tradelog包含交易流水號(tradeid)、交易員id(operator)、交易時間(t_modified)等字段。為了便于描述,我們先忽略其他字段。這個表的建表語句如下:
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ù),運營部門有一個需求是,要統(tǒng)計發(fā)生在所有年份中7月份的交易記錄總數(shù)。這個邏輯看上去并不復(fù)雜,你的SQL語句可能會這么寫:
mysql> select count(*) from tradelog where month(t_modified)=7;
由于t_modified字段上有索引,于是你就很放心地在生產(chǎn)庫中執(zhí)行了這條語句,但卻發(fā)現(xiàn)執(zhí)行了特別久,才返回了結(jié)果。
如果你問DBA同事為什么會出現(xiàn)這樣的情況,他大概會告訴你:如果對字段做了函數(shù)計算,就用不上索引了,這是MySQL的規(guī)定。
現(xiàn)在你已經(jīng)學(xué)過了InnoDB的索引結(jié)構(gòu)了,可以再追問一句為什么?為什么條件是where t_modified='2018-7-1’的時候可以用上索引,而改成where month(t_modified)=7的時候就不行了?
下面是這個t_modified索引的示意圖。方框上面的數(shù)字就是month()函數(shù)對應(yīng)的值。
圖1 t_modified索引示意圖
如果你的SQL語句條件用的是where t_modified='2018-7-1’的話,引擎就會按照上面綠色箭頭的路線,快速定位到 t_modified='2018-7-1’需要的結(jié)果。
實際上,B+樹提供的這個快速定位能力,來源于同一層兄弟節(jié)點的有序性。
但是,如果計算month()函數(shù)的話,你會看到傳入7的時候,在樹的第一層就不知道該怎么辦了。
也就是說,對索引字段做函數(shù)操作,可能會破壞索引值的有序性,因此優(yōu)化器就決定放棄走樹搜索功能。
需要注意的是,優(yōu)化器并不是要放棄使用這個索引。
在這個例子里,放棄了樹搜索功能,優(yōu)化器可以選擇遍歷主鍵索引,也可以選擇遍歷索引t_modified,優(yōu)化器對比索引大小后發(fā)現(xiàn),索引t_modified更小,遍歷這個索引比遍歷主鍵索引來得更快。因此最終還是會選擇索引t_modified。
接下來,我們使用explain命令,查看一下這條SQL語句的執(zhí)行結(jié)果。
圖2 explain 結(jié)果
key="t_modified"表示的是,使用了t_modified這個索引;我在測試表數(shù)據(jù)中插入了10萬行數(shù)據(jù),rows=100335,說明這條語句掃描了整個索引的所有值;Extra字段的Using index,表示的是使用了覆蓋索引。
也就是說,由于在t_modified字段加了month()函數(shù)操作,導(dǎo)致了全索引掃描。為了能夠用上索引的快速定位能力,我們就要把SQL語句改成基于字段本身的范圍查詢。按照下面這個寫法,優(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ù)據(jù)的話,你就需要再把其他年份補齊。
到這里我給你說明了,由于加了month()函數(shù)操作,MySQL無法再使用索引快速定位功能,而只能使用全索引掃描。
不過優(yōu)化器在個問題上確實有“偷懶”行為,即使是對于不改變有序性的函數(shù),也不會考慮使用索引。比如,對于select * from tradelog where id + 1 = 10000這個SQL語句,這個加1操作并不會改變有序性,但是MySQL優(yōu)化器還是不能用id索引快速定位到9999這一行。所以,需要你在寫SQL語句的時候,手動改寫成 where id = 10000 -1才可以。
接下來我再跟你說一說,另一個經(jīng)常讓程序員掉坑里的例子。
我們一起看一下這條SQL語句:
mysql> select * from tradelog where tradeid=110717;
交易編號tradeid這個字段上,本來就有索引,但是explain的結(jié)果卻顯示,這條語句需要走全表掃描。你可能也發(fā)現(xiàn)了,tradeid的字段類型是varchar(32),而輸入的參數(shù)卻是整型,所以需要做類型轉(zhuǎn)換。
那么,現(xiàn)在這里就有兩個問題:
數(shù)據(jù)類型轉(zhuǎn)換的規(guī)則是什么?
為什么有數(shù)據(jù)類型轉(zhuǎn)換,就需要走全索引掃描?
先來看第一個問題,你可能會說,數(shù)據(jù)庫里面類型這么多,這種數(shù)據(jù)類型轉(zhuǎn)換規(guī)則更多,我記不住,應(yīng)該怎么辦呢?
這里有一個簡單的方法,看 select “10” > 9的結(jié)果:
如果規(guī)則是“將字符串轉(zhuǎn)成數(shù)字”,那么就是做數(shù)字比較,結(jié)果應(yīng)該是1;
如果規(guī)則是“將數(shù)字轉(zhuǎn)成字符串”,那么就是做字符串比較,結(jié)果應(yīng)該是0。
驗證結(jié)果如圖3所示。
圖3 MySQL中字符串和數(shù)字轉(zhuǎn)換的效果示意圖
從圖中可知,select “10” > 9返回的是1,所以你就能確認(rèn)MySQL里的轉(zhuǎn)換規(guī)則了:在MySQL中,字符串和數(shù)字做比較的話,是將字符串轉(zhuǎn)換成數(shù)字。
這時,你再看這個全表掃描的語句:
mysql> select * from tradelog where tradeid=110717;
就知道對于優(yōu)化器來說,這個語句相當(dāng)于:
mysql> select * from tradelog where CAST(tradid AS signed int) = 110717;
也就是說,這條語句觸發(fā)了我們上面說到的規(guī)則:對索引字段做函數(shù)操作,優(yōu)化器會放棄走樹搜索功能。
現(xiàn)在,我留給你一個小問題,id的類型是int,如果執(zhí)行下面這個語句,是否會導(dǎo)致全表掃描呢?
select * from tradelog where id="83126";
你可以先自己分析一下,再到數(shù)據(jù)庫里面去驗證確認(rèn)。
接下來,我們再來看一個稍微復(fù)雜點的例子。
假設(shè)系統(tǒng)里還有另外一個表trade_detail,用于記錄交易的操作細(xì)節(jié)。為了便于量化分析和復(fù)現(xiàn),我往交易日志表tradelog和交易詳情表trade_detail這兩個表里插入一些數(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');
這時候,如果要查詢id=2的交易的所有操作步驟信息,SQL語句可以這么寫:
mysql> select d.* from tradelog l, trade_detail d where d.tradeid=l.tradeid and l.id=2; /*語句Q1*/
圖5 語句Q1的執(zhí)行過程
圖中:
第1步,是根據(jù)id在tradelog表里找到L2這一行;
第2步,是從L2中取出tradeid字段的值;
第3步,是根據(jù)tradeid值到trade_detail表中查找條件匹配的行。explain的結(jié)果里面第二行的key=NULL表示的就是,這個過程是通過遍歷主鍵索引的方式,一個一個地判斷tradeid的值是否匹配。
進行到這里,你會發(fā)現(xiàn)第3步不符合我們的預(yù)期。因為表trade_detail里tradeid字段上是有索引的,我們本來是希望通過使用tradeid索引能夠快速定位到等值的行。但,這里并沒有。
如果你去問DBA同學(xué),他們可能會告訴你,因為這兩個表的字符集不同,一個是utf8,一個是utf8mb4,所以做表連接查詢的時候用不上關(guān)聯(lián)字段的索引。這個回答,也是通常你搜索這個問題時會得到的答案。
但是你應(yīng)該再追問一下,為什么字符集不同就用不上索引呢?
我們說問題是出在執(zhí)行步驟的第3步,如果單獨把這一步改成SQL語句的話,那就是:
mysql> select * from trade_detail where tradeid=$L2.tradeid.value;
其中,$L2.tradeid.value的字符集是utf8mb4。
參照前面的兩個例子,你肯定就想到了,字符集utf8mb4是utf8的超集,所以當(dāng)這兩個類型的字符串在做比較的時候,MySQL內(nèi)部的操作是,先把utf8字符串轉(zhuǎn)成utf8mb4字符集,再做比較。
這個設(shè)定很好理解,utf8mb4是utf8的超集。類似地,在程序設(shè)計語言里面,做自動類型轉(zhuǎn)換的時候,為了避免數(shù)據(jù)在轉(zhuǎn)換過程中由于截斷導(dǎo)致數(shù)據(jù)錯誤,也都是“按數(shù)據(jù)長度增加的方向”進行轉(zhuǎn)換的。
因此, 在執(zhí)行上面這個語句的時候,需要將被驅(qū)動數(shù)據(jù)表里的字段一個個地轉(zhuǎn)換成utf8mb4,再跟L2做比較。
也就是說,實際上這個語句等同于下面這個寫法:
select * from trade_detail where CONVERT(traideid USING utf8mb4)=$L2.tradeid.value;
CONVERT()函數(shù),在這里的意思是把輸入的字符串轉(zhuǎn)成utf8mb4字符集。
這就再次觸發(fā)了我們上面說到的原則:對索引字段做函數(shù)操作,優(yōu)化器會放棄走樹搜索功能。
到這里,你終于明確了,字符集不同只是條件之一,連接過程中要求在被驅(qū)動表的索引字段上加函數(shù)操作,是直接導(dǎo)致對被驅(qū)動表做全表掃描的原因。
作為對比驗證,我給你提另外一個需求,“查找trade_detail表里id=4的操作,對應(yīng)的操作者是誰”,再來看下這個語句和它的執(zhí)行計劃。
mysql>select l.operator from tradelog l , trade_detail d where d.tradeid=l.tradeid and d.id=4;
圖7 SQL語句優(yōu)化后的explain結(jié)果
這里,我主動把 l.tradeid轉(zhuǎn)成utf8,就避免了被驅(qū)動表上的字符編碼轉(zhuǎn)換,從explain結(jié)果可以看到,這次索引走對了。
今天我給你舉了三個例子,其實是在說同一件事兒,即:對索引字段做函數(shù)操作,可能會破壞索引值的有序性,因此優(yōu)化器就決定放棄走樹搜索功能。
第二個例子是隱式類型轉(zhuǎn)換,第三個例子是隱式字符編碼轉(zhuǎn)換,它們都跟第一個例子一樣,因為要求在索引字段上做函數(shù)操作而導(dǎo)致了全索引掃描。
MySQL的優(yōu)化器確實有“偷懶”的嫌疑,即使簡單地把where id+1=1000改寫成where id=1000-1就能夠用上索引快速查找,也不會主動做這個語句重寫。
因此,每次你的業(yè)務(wù)代碼升級時,把可能出現(xiàn)的、新的SQL語句explain一下,是一個很好的習(xí)慣。
為了便于描述,我還是構(gòu)造一個表,基于這個表來說明今天的問題。這個表有兩個字段id和c,并且我在里面插入了10萬行記錄。
mysql> CREATE TABLE `t` ( `id` int(11) NOT NULL, `c` int(11) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB; delimiter ;; create procedure idata() begin declare i int; set i=1; while(i<=100000)do insert into t values(i,i); set i=i+1; end while; end;; delimiter ; call idata();
接下來,我會用幾個不同的場景來舉例,有些是前面的文章中我們已經(jīng)介紹過的知識點,你看看能不能一眼看穿,來檢驗一下吧。
如圖1所示,在表t執(zhí)行下面的SQL語句:
mysql> select * from t where id=1;
查詢結(jié)果長時間不返回。
圖2 Waiting for table metadata lock狀態(tài)示意圖
出現(xiàn)這個狀態(tài)表示的是,現(xiàn)在有一個線程正在表t上請求或者持有MDL寫鎖,把select語句堵住了。
在MySQL 5.7版本下復(fù)現(xiàn)這個場景,也很容易。如圖3所示,我給出了簡單的復(fù)現(xiàn)步驟。
圖4 查獲加表鎖的線程id
接下來,我給你舉另外一種查詢被堵住的情況。
我在表t上,執(zhí)行下面的SQL語句:
mysql> select * from information_schema.processlist where id=1;
這里,我先賣個關(guān)子。
你可以看一下圖5。我查出來這個線程的狀態(tài)是Waiting for table flush,你可以設(shè)想一下這是什么原因。
圖5 Waiting for table flush狀態(tài)示意圖
這個狀態(tài)表示的是,現(xiàn)在有一個線程正要對表t做flush操作。MySQL里面對表做flush操作的用法,一般有以下兩個:
flush tables t with read lock; flush tables with read lock;
這兩個flush語句,如果指定表t的話,代表的是只關(guān)閉表t;如果沒有指定具體的表名,則表示關(guān)閉MySQL里所有打開的表。
但是正常這兩個語句執(zhí)行起來都很快,除非它們也被別的線程堵住了。
所以,出現(xiàn)Waiting for table flush狀態(tài)的可能情況是:有一個flush tables命令被別的語句堵住了,然后它又堵住了我們的select語句。
現(xiàn)在,我們一起來復(fù)現(xiàn)一下這種情況,復(fù)現(xiàn)步驟如圖6所示:
圖6 Waiting for table flush的復(fù)現(xiàn)步驟
在session A中,我故意每行都調(diào)用一次sleep(1),這樣這個語句默認(rèn)要執(zhí)行10萬秒,在這期間表t一直是被session A“打開”著。然后,session B的flush tables t命令再要去關(guān)閉表t,就需要等session A的查詢結(jié)束。這樣,session C要再次查詢的話,就會被flush 命令堵住了。
圖7是這個復(fù)現(xiàn)步驟的show processlist結(jié)果。這個例子的排查也很簡單,你看到這個show processlist的結(jié)果,肯定就知道應(yīng)該怎么做了。
圖 8 行鎖復(fù)現(xiàn)
圖10 通過sys.innodb_lock_waits 查行鎖
可以看到,這個信息很全,4號線程是造成堵塞的罪魁禍?zhǔn)?。而干掉這個罪魁禍?zhǔn)椎姆绞剑褪荎ILL QUERY 4或KILL 4。
不過,這里不應(yīng)該顯示“KILL QUERY 4”。這個命令表示停止4號線程當(dāng)前正在執(zhí)行的語句,而這個方法其實是沒有用的。因為占有行鎖的是update語句,這個語句已經(jīng)是之前執(zhí)行完成了的,現(xiàn)在執(zhí)行KILL QUERY,無法讓這個事務(wù)去掉id=1上的行鎖。
實際上,KILL 4才有效,也就是說直接斷開這個連接。這里隱含的一個邏輯就是,連接被斷開的時候,會自動回滾這個連接里面正在執(zhí)行的線程,也就釋放了id=1上的行鎖。
經(jīng)過了重重封“鎖”,我們再來看看一些查詢慢的例子。
先來看一條你一定知道原因的SQL語句:
mysql> select * from t where c=50000 limit 1;
由于字段c上沒有索引,這個語句只能走id主鍵順序掃描,因此需要掃描5萬行。
作為確認(rèn),你可以看一下慢查詢?nèi)罩尽W⒁?,這里為了把所有語句記錄到slow log里,我在連接后先執(zhí)行了 set long_query_time=0,將慢查詢?nèi)罩镜臅r間閾值設(shè)置為0。
圖12 掃描一行卻執(zhí)行得很慢
是不是有點奇怪呢,這些時間都花在哪里了?
如果我把這個slow log的截圖再往下拉一點,你可以看到下一個語句,select * from t where id=1 lock in share mode,執(zhí)行時掃描行數(shù)也是1行,執(zhí)行時間是0.2毫秒。
圖14 兩個語句的輸出結(jié)果
第一個語句的查詢結(jié)果里c=1,帶lock in share mode的語句返回的是c=1000001??吹竭@里應(yīng)該有更多的同學(xué)知道原因了。如果你還是沒有頭緒的話,也別著急。我先跟你說明一下復(fù)現(xiàn)步驟,再分析原因。
圖16 id=1的數(shù)據(jù)狀態(tài)
session B更新完100萬次,生成了100萬個回滾日志(undo log)。
帶lock in share mode的SQL語句,是當(dāng)前讀,因此會直接讀到1000001這個結(jié)果,所以速度很快;而select * from t where id=1這個語句,是一致性讀,因此需要從1000001開始,依次執(zhí)行undo log,執(zhí)行了100萬次以后,才將1這個結(jié)果返回。
注意,undo log里記錄的其實是“把2改成1”,“把3改成2”這樣的操作邏輯,畫成減1的目的是方便你看圖。
今天我給你舉了在一個簡單的表上,執(zhí)行“查一行”,可能會出現(xiàn)的被鎖住和執(zhí)行慢的例子。這其中涉及到了表鎖、行鎖和一致性讀的概念。
在實際使用中,碰到的場景會更復(fù)雜。但大同小異,你可以按照我在文章中介紹的定位方法,來定位并解決問題。
最后,我給你留一個問題吧。
我們在舉例加鎖讀的時候,用的是這個語句,select * from t where id=1 lock in share mode。由于id上有索引,所以可以直接定位到id=1這一行,因此讀鎖也是只加在了這一行上。
但如果是下面的SQL語句,
begin; select * from t where c=5 for update; commit;
這個語句序列是怎么加鎖的呢?加的鎖又是什么時候釋放呢?
表結(jié)構(gòu)如下:
mysql> CREATE TABLE `table_a` ( `id` int(11) NOT NULL, `b` varchar(10) DEFAULT NULL, PRIMARY KEY (`id`), KEY `b` (`b`) ) ENGINE=InnoDB;
假設(shè)現(xiàn)在表里面,有100萬行數(shù)據(jù),其中有10萬行數(shù)據(jù)的b的值是’1234567890’, 假設(shè)現(xiàn)在執(zhí)行語句是這么寫的:
mysql> select * from table_a where b='1234567890abcd';
這時候,MySQL會怎么執(zhí)行呢?
最理想的情況是,MySQL看到字段b定義的是varchar(10),那肯定返回空呀。可惜,MySQL并沒有這么做。
那要不,就是把’1234567890abcd’拿到索引里面去做匹配,肯定也沒能夠快速判斷出索引樹b上并沒有這個值,也很快就能返回空結(jié)果。
但實際上,MySQL也不是這么做的。
這條SQL語句的執(zhí)行很慢,流程是這樣的:
在傳給引擎執(zhí)行的時候,做了字符截斷。因為引擎里面這個行只定義了長度是10,所以只截了前10個字節(jié),就是’1234567890’進去做匹配;
這樣滿足條件的數(shù)據(jù)有10萬行;
因為是select *, 所以要做10萬次回表;
但是每次回表以后查出整行,到server層一判斷,b的值都不是’1234567890abcd’;
返回結(jié)果是空。
感謝各位的閱讀,以上就是“MySQL中為什么簡單的一行查詢也會慢”的內(nèi)容了,經(jīng)過本文的學(xué)習(xí)后,相信大家對MySQL中為什么簡單的一行查詢也會慢這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是創(chuàng)新互聯(lián),小編將為大家推送更多相關(guān)知識點的文章,歡迎關(guān)注!
網(wǎng)站標(biāo)題:MySQL中為什么簡單的一行查詢也會慢
當(dāng)前網(wǎng)址:http://jinyejixie.com/article34/jopose.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供外貿(mào)網(wǎng)站建設(shè)、網(wǎng)站排名、電子商務(wù)、云服務(wù)器、、Google
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時需注明來源: 創(chuàng)新互聯(lián)