日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > 数据库 >内容正文

数据库

18 | 为什么这些SQL语句逻辑相同,性能却差异巨大?

發布時間:2025/3/21 数据库 33 豆豆
生活随笔 收集整理的這篇文章主要介紹了 18 | 为什么这些SQL语句逻辑相同,性能却差异巨大? 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

在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()函數對應的值。

圖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),而輸入的參數卻是整型,所以需要做類型轉換。

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

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

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

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

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

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

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

  • 驗證結果如圖3所示。

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

    圖4 語句Q1的explain 結果

    我們一起來看下這個結果:

  • 第一行顯示優化器會先在交易記錄表tradelog上查到id=2的行,這個步驟用上了主鍵索引,rows=1表示只掃描一行;

  • 第二行key=NULL,表示沒有用上交易詳情表trade_detail上的tradeid索引,進行了全表掃描。

  • 在這個執行計劃里,是從tradelog表中取tradeid字段,再去trade_detail表里查詢匹配字段。因此,我們把tradelog稱為驅動表,把trade_detail稱為被驅動表,把tradeid稱為關聯字段。

    接下來,我們看下這個explain結果表示的執行流程:

    圖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;

    圖6 explain 結果

    這個語句里trade_detail 表成了驅動表,但是explain結果的第二行顯示,這次的查詢操作用上了被驅動表tradelog里的索引(tradeid),掃描行數是1。

    這也是兩個tradeid字段的join操作,為什么這次能用上被驅動表的tradeid索引呢?我們來分析一下。

    假設驅動表trade_detail里id=4的行記為R4,那么在連接的時候(圖5的第3步),被驅動表tradelog上執行的就是類似這樣的SQL 語句:

    select operator from tradelog where traideid =$R4.tradeid.value;

    這時候$R4.tradeid.value的字符集是utf8, 按照字符集轉換規則,要轉成utf8mb4,所以這個過程就被改寫成:

    select operator from tradelog where traideid =CONVERT($R4.tradeid.value USING utf8mb4);

    你看,這里的CONVERT函數是加在輸入參數上的,這樣就可以用上被驅動表的traideid索引。

    理解了原理以后,就可以用來指導操作了。如果要優化語句

    select d.* from tradelog l, trade_detail d where d.tradeid=l.tradeid and l.id=2;

    的執行過程,有兩種做法:

    • 比較常見的優化方法是,把trade_detail表上的tradeid字段的字符集也改成utf8mb4,這樣就沒有字符集轉換的問題了。
    alter table trade_detail modify tradeid varchar(32) CHARACTER SET utf8mb4 default null;
    • 如果能夠修改字段的字符集的話,是最好不過了。但如果數據量比較大, 或者業務上暫時不能做這個DDL的話,那就只能采用修改SQL語句的方法了。
    mysql> select d.* from tradelog l , trade_detail d where d.tradeid=CONVERT(l.tradeid USING utf8) and l.id=2;

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

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

    小結

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

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

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

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

    最后,又到了思考題時間。

    今天我留給你的課后問題是,你遇到過別的、類似今天我們提到的性能問題嗎?你認為原因是什么,又是怎么解決的呢?

    你可以把你經歷和分析寫在留言區里,我會在下一篇文章的末尾選取有趣的評論跟大家一起分享和分析。感謝你的收聽,也歡迎你把這篇文章分享給更多的朋友一起閱讀。

    上期問題時間

    我在上篇文章的最后,留給你的問題是:我們文章中最后的一個方案是,通過三次limit Y,1 來得到需要的數據,你覺得有沒有進一步的優化方法。

    這里我給出一種方法,取Y1、Y2和Y3里面最大的一個數,記為M,最小的一個數記為N,然后執行下面這條SQL語句:

    mysql> select * from t limit N, M-N+1;

    再加上取整個表總行數的C行,這個方案的掃描行數總共只需要C+M+1行。

    當然也可以先取回id值,在應用中確定了三個id值以后,再執行三次where id=X的語句也是可以的。@倪大人 同學在評論區就提到了這個方法。

    ?

    轉載于:https://www.cnblogs.com/a-phper/p/10313922.html

    總結

    以上是生活随笔為你收集整理的18 | 为什么这些SQL语句逻辑相同,性能却差异巨大?的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。

    主站蜘蛛池模板: 视频一区二区三区在线观看 | 欧美v亚洲| www.欧美日韩| 国产精品手机视频 | 99久久久无码国产精品6 | 亚洲欧美视频 | 羞羞色视频 | 另类小说一区二区 | 男人天堂视频网站 | 日韩在线激情 | 国产一区视频免费观看 | 免费日韩一区二区 | 中文字幕在线日本 | 91色网站 | 好色成人网 | 久久免费视频网 | 国产黑丝在线 | 色视频在线观看免费 | 成人午夜在线观看视频 | av大片网址| 一区二区三区日韩欧美 | 亚洲综合二区 | 男女叼嘿视频 | 亚洲精品电影 | 欧美日韩激情视频 | 国产色在线 | 131美女爱做视频 | 无码一区二区三区视频 | 亚洲视频在线观看一区二区 | 国产春色| 国产精品久久一区 | 日韩av福利| 超碰蜜臀 | 国产精华7777777 | 国产一区二区三区免费视频 | 97视频在线 | 黄免费在线观看 | 女人性高潮视频 | 成人录像 | 久久香蕉综合 | 在线观看日本一区二区 | h视频网站在线观看 | 无码国模国产在线观看 | 侵犯女教师一区二区三区 | 日本色一区 | 裸体喂奶一级裸片 | 91美女在线观看 | 福利一二三区 | 欧美乱妇日本无乱码特黄大片 | 最新日韩在线视频 | 一级性毛片 | 欧美一区二区三区不卡视频 | 成人淫片 | 香蕉久久av一区二区三区 | 在线综合视频 | 国产一级久久久 | 国产精品18久久久久久vr下载 | xxxx18日本| 91精品人妻一区二区三区蜜桃欧美 | 99热热 | 激情超碰 | 51人人看 | 黄色污网站在线观看 | 亚洲一区二区久久久 | 一炮成瘾1v1高h | heyzo北岛玲在线播放 | 在线免费观看日韩av | 91激情视频在线观看 | 激烈娇喘叫1v1高h糙汉 | 人人妻人人玩人人澡人人爽 | 综合久久久久久久久久久 | 欧美欧美欧美欧美 | 五月婷婷导航 | 国产女同在线观看 | 午夜精品99 | 在线亚洲一区 | 欧美中文网| 国产欧美日韩一区二区三区 | 名校风暴在线观看免费高清完整 | av中文资源网 | 青青青草视频 | 国产视频三级 | 强辱丰满人妻hd中文字幕 | 免费在线色视频 | 亚洲精品视频91 | 韩国精品一区二区 | 亚洲第一页色 | 美女a视频| 日本天堂在线播放 | 影音先锋 日韩 | 日本一道在线 | 爱综合网 | 国产精品成人在线 | 亚洲av无码一区二区乱孑伦as | 8x国产一区二区三区精品推荐 | 黄色在线播放视频 | 国产特黄一级片 | 波多野结衣在线一区 | 影音先锋黄色网址 |