中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

MySQL中為什么簡單的一行查詢也會慢

發布時間:2021-12-08 14:21:55 來源:億速云 閱讀:153 作者:iii 欄目:大數據

這篇文章主要講解了“MySQL中為什么簡單的一行查詢也會慢”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“MySQL中為什么簡單的一行查詢也會慢”吧!

在MySQL中,有很多看上去邏輯相同,但性能卻差異巨大的SQL語句。對這些語句使用不當的話,就會不經意間導致整個數據庫的壓力變大。

案例一:條件字段函數操作

假設你現在維護了一個交易系統,其中交易記錄表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;

假設,現在已經記錄了從2016年初到2018年底的所有數據,運營部門有一個需求是,要統計發生在所有年份中7月份的交易記錄總數。這個邏輯看上去并不復雜,你的SQL語句可能會這么寫:

mysql> select count(*) from tradelog where month(t_modified)=7;

由于t_modified字段上有索引,于是你就很放心地在生產庫中執行了這條語句,但卻發現執行了特別久,才返回了結果。

如果你問DBA同事為什么會出現這樣的情況,他大概會告訴你:如果對字段做了函數計算,就用不上索引了,這是MySQL的規定。

現在你已經學過了InnoDB的索引結構了,可以再追問一句為什么?為什么條件是where t_modified='2018-7-1’的時候可以用上索引,而改成where month(t_modified)=7的時候就不行了?

下面是這個t_modified索引的示意圖。方框上面的數字就是month()函數對應的值。

MySQL中為什么簡單的一行查詢也會慢

                                                                圖1 t_modified索引示意圖

如果你的SQL語句條件用的是where t_modified='2018-7-1’的話,引擎就會按照上面綠色箭頭的路線,快速定位到 t_modified='2018-7-1’需要的結果。

實際上,B+樹提供的這個快速定位能力,來源于同一層兄弟節點的有序性。

但是,如果計算month()函數的話,你會看到傳入7的時候,在樹的第一層就不知道該怎么辦了。

也就是說,對索引字段做函數操作,可能會破壞索引值的有序性,因此優化器就決定放棄走樹搜索功能。

需要注意的是,優化器并不是要放棄使用這個索引。

在這個例子里,放棄了樹搜索功能,優化器可以選擇遍歷主鍵索引,也可以選擇遍歷索引t_modified,優化器對比索引大小后發現,索引t_modified更小,遍歷這個索引比遍歷主鍵索引來得更快。因此最終還是會選擇索引t_modified。

接下來,我們使用explain命令,查看一下這條SQL語句的執行結果。

                                                                           圖2 explain 結果

key="t_modified"表示的是,使用了t_modified這個索引;我在測試表數據中插入了10萬行數據,rows=100335,說明這條語句掃描了整個索引的所有值;Extra字段的Using index,表示的是使用了覆蓋索引。

也就是說,由于在t_modified字段加了month()函數操作,導致了全索引掃描。為了能夠用上索引的快速定位能力,我們就要把SQL語句改成基于字段本身的范圍查詢。按照下面這個寫法,優化器就能按照我們預期的,用上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');

當然,如果你的系統上線時間更早,或者后面又插入了之后年份的數據的話,你就需要再把其他年份補齊。

到這里我給你說明了,由于加了month()函數操作,MySQL無法再使用索引快速定位功能,而只能使用全索引掃描。

不過優化器在個問題上確實有“偷懶”行為,即使是對于不改變有序性的函數,也不會考慮使用索引。比如,對于select * from tradelog where id + 1 = 10000這個SQL語句,這個加1操作并不會改變有序性,但是MySQL優化器還是不能用id索引快速定位到9999這一行。所以,需要你在寫SQL語句的時候,手動改寫成 where id = 10000 -1才可以。

案例二:隱式類型轉換

接下來我再跟你說一說,另一個經常讓程序員掉坑里的例子。

我們一起看一下這條SQL語句:

mysql> select * from tradelog where tradeid=110717;

交易編號tradeid這個字段上,本來就有索引,但是explain的結果卻顯示,這條語句需要走全表掃描。你可能也發現了,tradeid的字段類型是varchar(32),而輸入的參數卻是整型,所以需要做類型轉換。

那么,現在這里就有兩個問題:

  1. 數據類型轉換的規則是什么?

  2. 為什么有數據類型轉換,就需要走全索引掃描?

先來看第一個問題,你可能會說,數據庫里面類型這么多,這種數據類型轉換規則更多,我記不住,應該怎么辦呢?

這里有一個簡單的方法,看 select “10” > 9的結果:

  1. 如果規則是“將字符串轉成數字”,那么就是做數字比較,結果應該是1;

  2. 如果規則是“將數字轉成字符串”,那么就是做字符串比較,結果應該是0。

驗證結果如圖3所示。

MySQL中為什么簡單的一行查詢也會慢

圖3 MySQL中字符串和數字轉換的效果示意圖

從圖中可知,select “10” > 9返回的是1,所以你就能確認MySQL里的轉換規則了:在MySQL中,字符串和數字做比較的話,是將字符串轉換成數字。

這時,你再看這個全表掃描的語句:

mysql> select * from tradelog where tradeid=110717;

就知道對于優化器來說,這個語句相當于:

mysql> select * from tradelog where  CAST(tradid AS signed int) = 110717;

也就是說,這條語句觸發了我們上面說到的規則:對索引字段做函數操作,優化器會放棄走樹搜索功能。

現在,我留給你一個小問題,id的類型是int,如果執行下面這個語句,是否會導致全表掃描呢?

select * from tradelog where id="83126";

你可以先自己分析一下,再到數據庫里面去驗證確認。

接下來,我們再來看一個稍微復雜點的例子。

案例三:隱式字符編碼轉換

假設系統里還有另外一個表trade_detail,用于記錄交易的操作細節。為了便于量化分析和復現,我往交易日志表tradelog和交易詳情表trade_detail這兩個表里插入一些數據。

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*/

MySQL中為什么簡單的一行查詢也會慢

                                                                             圖5 語句Q1的執行過程

圖中:

  • 第1步,是根據id在tradelog表里找到L2這一行;

  • 第2步,是從L2中取出tradeid字段的值;

  • 第3步,是根據tradeid值到trade_detail表中查找條件匹配的行。explain的結果里面第二行的key=NULL表示的就是,這個過程是通過遍歷主鍵索引的方式,一個一個地判斷tradeid的值是否匹配。

進行到這里,你會發現第3步不符合我們的預期。因為表trade_detail里tradeid字段上是有索引的,我們本來是希望通過使用tradeid索引能夠快速定位到等值的行。但,這里并沒有。

如果你去問DBA同學,他們可能會告訴你,因為這兩個表的字符集不同,一個是utf8,一個是utf8mb4,所以做表連接查詢的時候用不上關聯字段的索引。這個回答,也是通常你搜索這個問題時會得到的答案。

但是你應該再追問一下,為什么字符集不同就用不上索引呢?

我們說問題是出在執行步驟的第3步,如果單獨把這一步改成SQL語句的話,那就是:

mysql> select * from trade_detail where tradeid=$L2.tradeid.value;

其中,$L2.tradeid.value的字符集是utf8mb4。

參照前面的兩個例子,你肯定就想到了,字符集utf8mb4是utf8的超集,所以當這兩個類型的字符串在做比較的時候,MySQL內部的操作是,先把utf8字符串轉成utf8mb4字符集,再做比較。

這個設定很好理解,utf8mb4是utf8的超集。類似地,在程序設計語言里面,做自動類型轉換的時候,為了避免數據在轉換過程中由于截斷導致數據錯誤,也都是“按數據長度增加的方向”進行轉換的。

因此, 在執行上面這個語句的時候,需要將被驅動數據表里的字段一個個地轉換成utf8mb4,再跟L2做比較。

也就是說,實際上這個語句等同于下面這個寫法:

select * from trade_detail  where CONVERT(traideid USING utf8mb4)=$L2.tradeid.value;

CONVERT()函數,在這里的意思是把輸入的字符串轉成utf8mb4字符集。

這就再次觸發了我們上面說到的原則:對索引字段做函數操作,優化器會放棄走樹搜索功能。

到這里,你終于明確了,字符集不同只是條件之一,連接過程中要求在被驅動表的索引字段上加函數操作,是直接導致對被驅動表做全表掃描的原因。

作為對比驗證,我給你提另外一個需求,“查找trade_detail表里id=4的操作,對應的操作者是誰”,再來看下這個語句和它的執行計劃。

mysql>select l.operator from tradelog l , trade_detail d where d.tradeid=l.tradeid and d.id=4;

MySQL中為什么簡單的一行查詢也會慢

圖7 SQL語句優化后的explain結果

這里,我主動把 l.tradeid轉成utf8,就避免了被驅動表上的字符編碼轉換,從explain結果可以看到,這次索引走對了。

小結

今天我給你舉了三個例子,其實是在說同一件事兒,即:對索引字段做函數操作,可能會破壞索引值的有序性,因此優化器就決定放棄走樹搜索功能。

第二個例子是隱式類型轉換,第三個例子是隱式字符編碼轉換,它們都跟第一個例子一樣,因為要求在索引字段上做函數操作而導致了全索引掃描。

MySQL的優化器確實有“偷懶”的嫌疑,即使簡單地把where id+1=1000改寫成where id=1000-1就能夠用上索引快速查找,也不會主動做這個語句重寫。

因此,每次你的業務代碼升級時,把可能出現的、新的SQL語句explain一下,是一個很好的習慣。

執行一行也慢為什么?

為了便于描述,我還是構造一個表,基于這個表來說明今天的問題。這個表有兩個字段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();

接下來,我會用幾個不同的場景來舉例,有些是前面的文章中我們已經介紹過的知識點,你看看能不能一眼看穿,來檢驗一下吧。

第一類:查詢長時間不返回

如圖1所示,在表t執行下面的SQL語句:

mysql> select * from t where id=1;

查詢結果長時間不返回。

MySQL中為什么簡單的一行查詢也會慢

                                                        圖2 Waiting for table metadata lock狀態示意圖

出現這個狀態表示的是,現在有一個線程正在表t上請求或者持有MDL寫鎖,把select語句堵住了。

在MySQL 5.7版本下復現這個場景,也很容易。如圖3所示,我給出了簡單的復現步驟。
MySQL中為什么簡單的一行查詢也會慢

圖4 查獲加表鎖的線程id

等flush

接下來,我給你舉另外一種查詢被堵住的情況。

我在表t上,執行下面的SQL語句:

mysql> select * from information_schema.processlist where id=1;

這里,我先賣個關子。

你可以看一下圖5。我查出來這個線程的狀態是Waiting for table flush,你可以設想一下這是什么原因。

圖5 Waiting for table flush狀態示意圖

這個狀態表示的是,現在有一個線程正要對表t做flush操作。MySQL里面對表做flush操作的用法,一般有以下兩個:

flush tables t with read lock;

flush tables with read lock;

這兩個flush語句,如果指定表t的話,代表的是只關閉表t;如果沒有指定具體的表名,則表示關閉MySQL里所有打開的表。

但是正常這兩個語句執行起來都很快,除非它們也被別的線程堵住了。

所以,出現Waiting for table flush狀態的可能情況是:有一個flush tables命令被別的語句堵住了,然后它又堵住了我們的select語句。

現在,我們一起來復現一下這種情況,復現步驟如圖6所示:

MySQL中為什么簡單的一行查詢也會慢

                                                           圖6 Waiting for table flush的復現步驟

在session A中,我故意每行都調用一次sleep(1),這樣這個語句默認要執行10萬秒,在這期間表t一直是被session A“打開”著。然后,session B的flush tables t命令再要去關閉表t,就需要等session A的查詢結束。這樣,session C要再次查詢的話,就會被flush 命令堵住了。

圖7是這個復現步驟的show processlist結果。這個例子的排查也很簡單,你看到這個show processlist的結果,肯定就知道應該怎么做了。

MySQL中為什么簡單的一行查詢也會慢

                                                                        圖 8 行鎖復現

MySQL中為什么簡單的一行查詢也會慢

                                                                圖10 通過sys.innodb_lock_waits 查行鎖

可以看到,這個信息很全,4號線程是造成堵塞的罪魁禍首。而干掉這個罪魁禍首的方式,就是KILL QUERY 4或KILL 4。

不過,這里不應該顯示“KILL QUERY 4”。這個命令表示停止4號線程當前正在執行的語句,而這個方法其實是沒有用的。因為占有行鎖的是update語句,這個語句已經是之前執行完成了的,現在執行KILL QUERY,無法讓這個事務去掉id=1上的行鎖。

實際上,KILL 4才有效,也就是說直接斷開這個連接。這里隱含的一個邏輯就是,連接被斷開的時候,會自動回滾這個連接里面正在執行的線程,也就釋放了id=1上的行鎖。

第二類:查詢慢

經過了重重封“鎖”,我們再來看看一些查詢慢的例子。

先來看一條你一定知道原因的SQL語句:

mysql> select * from t where c=50000 limit 1;

由于字段c上沒有索引,這個語句只能走id主鍵順序掃描,因此需要掃描5萬行。

作為確認,你可以看一下慢查詢日志。注意,這里為了把所有語句記錄到slow log里,我在連接后先執行了 set long_query_time=0,將慢查詢日志的時間閾值設置為0。

MySQL中為什么簡單的一行查詢也會慢

圖12 掃描一行卻執行得很慢

是不是有點奇怪呢,這些時間都花在哪里了?

如果我把這個slow log的截圖再往下拉一點,你可以看到下一個語句,select * from t where id=1 lock in share mode,執行時掃描行數也是1行,執行時間是0.2毫秒。

MySQL中為什么簡單的一行查詢也會慢

圖14 兩個語句的輸出結果

第一個語句的查詢結果里c=1,帶lock in share mode的語句返回的是c=1000001。看到這里應該有更多的同學知道原因了。如果你還是沒有頭緒的話,也別著急。我先跟你說明一下復現步驟,再分析原因。

MySQL中為什么簡單的一行查詢也會慢

圖16 id=1的數據狀態

session B更新完100萬次,生成了100萬個回滾日志(undo log)。

帶lock in share mode的SQL語句,是當前讀,因此會直接讀到1000001這個結果,所以速度很快;而select * from t where id=1這個語句,是一致性讀,因此需要從1000001開始,依次執行undo log,執行了100萬次以后,才將1這個結果返回。

注意,undo log里記錄的其實是“把2改成1”,“把3改成2”這樣的操作邏輯,畫成減1的目的是方便你看圖。

小結

今天我給你舉了在一個簡單的表上,執行“查一行”,可能會出現的被鎖住和執行慢的例子。這其中涉及到了表鎖、行鎖和一致性讀的概念。

在實際使用中,碰到的場景會更復雜。但大同小異,你可以按照我在文章中介紹的定位方法,來定位并解決問題。

最后,我給你留一個問題吧。

我們在舉例加鎖讀的時候,用的是這個語句,select * from t where id=1 lock in share mode。由于id上有索引,所以可以直接定位到id=1這一行,因此讀鎖也是只加在了這一行上。

但如果是下面的SQL語句,

begin;
select * from t where c=5 for update;
commit;

這個語句序列是怎么加鎖的呢?加的鎖又是什么時候釋放呢?

上期問題時間

表結構如下:

mysql> CREATE TABLE `table_a` (
  `id` int(11) NOT NULL,
  `b` varchar(10) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `b` (`b`)
) ENGINE=InnoDB;

假設現在表里面,有100萬行數據,其中有10萬行數據的b的值是’1234567890’, 假設現在執行語句是這么寫的:

mysql> select * from table_a where b='1234567890abcd';

這時候,MySQL會怎么執行呢?

最理想的情況是,MySQL看到字段b定義的是varchar(10),那肯定返回空呀。可惜,MySQL并沒有這么做。

那要不,就是把’1234567890abcd’拿到索引里面去做匹配,肯定也沒能夠快速判斷出索引樹b上并沒有這個值,也很快就能返回空結果。

但實際上,MySQL也不是這么做的。

這條SQL語句的執行很慢,流程是這樣的:

  1. 在傳給引擎執行的時候,做了字符截斷。因為引擎里面這個行只定義了長度是10,所以只截了前10個字節,就是’1234567890’進去做匹配;

  2. 這樣滿足條件的數據有10萬行;

  3. 因為是select *, 所以要做10萬次回表;

  4. 但是每次回表以后查出整行,到server層一判斷,b的值都不是’1234567890abcd’;

  5. 返回結果是空。

感謝各位的閱讀,以上就是“MySQL中為什么簡單的一行查詢也會慢”的內容了,經過本文的學習后,相信大家對MySQL中為什么簡單的一行查詢也會慢這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

章丘市| 昆山市| 蚌埠市| 松桃| 河曲县| 云安县| 海宁市| 珲春市| 称多县| 盱眙县| 同心县| 鹤岗市| 礼泉县| 临朐县| 会宁县| 娄底市| 望江县| 田林县| 贵南县| 德令哈市| 滨海县| 隆林| 靖江市| 福安市| 遵义市| 丰宁| 安图县| 浮山县| 电白县| 鹿邑县| 扶绥县| 林州市| 兴业县| 辉南县| 海晏县| 永城市| 泽州县| 三穗县| 宿州市| 咸阳市| 澎湖县|