MySQL實戰45講學習筆記:第十八講

1、引子

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

我今天挑選了三個這樣的案例和你分享。但願再遇到類似的問題時,你能夠作到觸類旁通、快速解決問題。程序員

2、案例一:條件字段函數操做

假設你如今維護了一個交易系統,其中交易記錄表 tradelog 包含交易流水號(tradeid)、交易員 id(operator)、交易時間(t_modified)等字段。爲了便於描
述,咱們先忽略其餘字段。這個表的建表語句以下:sql

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 字段上有索引,因而你就很放心地在生產庫中執行了這條語句,但卻發現執行了特別久,才返回告終果。bash

一、若是對字段作了函數計算,就用不上索引了,這是 MySQL 的規定。

若是你問 DBA 同事爲何會出現這樣的狀況,他大概會告訴你:若是對字段作了函數計算,就用不上索引了,這是 MySQL 的規定。ide

二、爲何條件是 wheret_modified='2018-7-1’的時候能夠用上索引,而改爲 where month(t_modified)=7的時候就不行了?

如今你已經學過了 InnoDB 的索引結構了,能夠再追問一句爲何?爲何條件是 wheret_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
才能夠。

3、案例二:隱式類型轉換

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

咱們一塊兒看一下這條 SQL 語句:

mysql> select * from tradelog where tradeid=110717;

一、數據類型轉換的規則是什麼?

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

那麼,如今這裏就有兩個問題:

  • 1. 數據類型轉換的規則是什麼?
  • 2. 爲何有數據類型轉換,就須要走全索引掃描?

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

這裏有一個簡單的方法,看 select 「10」 > 9 的結果:

1. 若是規則是「將字符串轉成數字」,那麼就是作數字比較,結果應該是 1;
2. 若是規則是「將數字轉成字符串」,那麼就是作字符串比較,結果應該是 0。

驗證結果如圖 3 所示。

實際測試代碼以下:

mysql> mysql> select "10" > 9;
+----------+
| "10" > 9 |
+----------+
|        1 |
+----------+
1 row in set (0.00 sec)

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

從圖中可知,select 「10」 > 9 返回的是 1,因此你就能確認 MySQL 裏的轉換規則了:在 MySQL 中,字符串和數字作比較的話,是將字符串轉換成數字。

二、爲何有數據類型轉換,就須要走全索引掃描?

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

mysql> select * from tradelog where tradeid=110717;

實際測試代碼與截圖以下:

 

 

 

mysql> explain select * from tradelog where tradeid=110717;
+----+-------------+----------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table    | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra       |
+----+-------------+----------+------------+------+---------------+------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | tradelog | NULL       | ALL  | tradeid       | NULL | NULL    | NULL |    3 |    33.33 | Using where |
+----+-------------+----------+------------+------+---------------+------+---------+------+------+----------+-------------+
1 row in set, 3 warnings (0.00 sec)

就知道對於優化器來講,這個語句至關於:

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

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

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

select * from tradelog where id="83126";

你能夠先本身分析一下,再到數據庫裏面去驗證確認。接下來,咱們再來看一個稍微複雜點的例子。

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

假設系統裏還有另一個表 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 結果

實際測試代碼以下:

mysql> explain select d.* from  tradelog l, trade_detail d where d.tradeid=l.tradeid and l.id=2;
+----+-------------+-------+------------+-------+-----------------+---------+---------+-------+------+----------+-------------+
| id | select_type | table | partitions | type  | possible_keys   | key     | key_len | ref   | rows | filtered | Extra       |
+----+-------------+-------+------------+-------+-----------------+---------+---------+-------+------+----------+-------------+
|  1 | SIMPLE      | l     | NULL       | const | PRIMARY,tradeid | PRIMARY | 4       | const |    1 |   100.00 | NULL        |
|  1 | SIMPLE      | d     | NULL       | ALL   | NULL            | NULL    | NULL    | NULL  |   11 |   100.00 | Using where |
+----+-------------+-------+------------+-------+-----------------+---------+---------+-------+------+----------+-------------+
2 rows in set, 1 warning (0.01 sec)

咱們一塊兒來看下這個結果:

  • 1. 第一行顯示優化器會先在交易記錄表 tradelog 上查到 id=2 的行,這個步驟用上了主鍵索引,rows=1 表示只掃描一行;
  • 2. 第二行 key=NULL,表示沒有用上交易詳情表 trade_detail 上的 tradeid 索引,進行了全表掃描。

在這個執行計劃裏,是從 tradelog 表中取 tradeid 字段,再去 trade_detail 表裏查詢匹配字段。所以,咱們把 tradelog 稱爲驅動表,把 trade_detail 稱爲被驅動表,把 tradeid
稱爲關聯字段。

一、語句 Q1 的執行過程

接下來,咱們看下這個 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 結果

實際測試代碼以下:

mysql> explain select l.operator from tradelog l , trade_detail d where d.tradeid=l.tradeid and d.id=4;
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
| id | select_type | table | partitions | type  | possible_keys | key     | key_len | ref   | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
|  1 | SIMPLE      | d     | NULL       | const | PRIMARY       | PRIMARY | 4       | const |    1 |   100.00 | NULL  |
|  1 | SIMPLE      | l     | NULL       | ref   | tradeid       | tradeid | 131     | const |    1 |   100.00 | NULL  |
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
2 rows in set, 1 warning (0.00 sec)

這個語句裏 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 結果

實際測試代碼以下:

mysql> explain select d.* from tradelog l , trade_detail d where d.tradeid=CONVERT(l.tradeid USING utf8) and l.id=2; 
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
| id | select_type | table | partitions | type  | possible_keys | key     | key_len | ref   | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
|  1 | SIMPLE      | l     | NULL       | const | PRIMARY       | PRIMARY | 4       | const |    1 |   100.00 | NULL  |
|  1 | SIMPLE      | d     | NULL       | ref   | tradeid       | tradeid | 99      | const |    4 |   100.00 | NULL  |
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
2 rows in set, 1 warning (0.01 sec)

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

5、小結

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

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

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

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

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

今天我留給你的課後問題是,你遇到過別的、相似今天咱們提到的性能問題嗎?你認爲緣由是什麼,又是怎麼解決的呢?

你能夠把你經歷和分析寫在留言區裏,我會在下一篇文章的末尾選取有趣的評論跟你們一塊兒分享和分析。感謝你的收聽,也歡迎你把這篇文章分享給更多的朋友一塊兒閱讀。

6、上期問題時間

我在上篇文章的最後,留給你的問題是:咱們文章中最後的一個方案是,經過三次 limitY,1 來獲得須要的數據,你以爲有沒有進一步的優化方法。

這裏我給出一種方法,取 Y一、Y2 和 Y3 裏面最大的一個數,記爲 M,最小的一個數記爲
N,而後執行下面這條 SQL 語句:

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

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

固然也能夠先取回 id 值,在應用中肯定了三個 id 值之後,再執行三次 where id=X 的語句也是能夠的。@倪大人 同窗在評論區就提到了這個方法。

7、經典留言


一、冠超

很是感謝老師分享的內容,實打實地學到了。這裏提個建議,但願老師能介紹一下設計表的時候要怎麼考慮這方面的知識哈😊

做者回復:

是這樣的,其實咱們整個專欄大部分的文章,最後都是爲了說明 「怎麼設計表」、「怎麼考慮優化SQL語句」

可是由於這個不是一成不變的,不少是須要考慮現實的狀況,
因此這個專欄就是想把對應的原理說一下,這樣你們在應對不一樣場景的時候,能夠組合來考慮。

也就是說沒有一段話能夠把「怎麼設計表」講清楚(或者說硬寫出來極可能就是一些general的沒有什麼針對性做用的描述)

你能夠把你的業務背景抽象說下,咱們來具體討論吧

  

二、Eliefly

感受要使用索引就不能「破壞」索引原有的順序,這節的函數操做,隱式轉換都「破壞」了原有的順序。
* from t where city in in (「杭州」," 蘇州 ") order by name limit 100; 一樣是破壞了 (city,name) 聯合索引的遞增順序,
相似的還有使用聯合索引,一個字段DESC,一個ASC

做者回復: 

總結得很好

「順勢而查」才能用上索引😆

三、傑之7

經過這一節的學習,理解了即便邏輯相同的查詢語句,在不知道數據庫裏面的運行機制時,性能差別會很是明顯。

老師經過三個案例都是在說明同一件事,在使用索性字段作函數操做時,函數會破壞索引值的有序性,致使作全表掃描而讓性能下降。

在後面的兩個案例中,隱式類型轉換中,字符串和數字作比較是將字符串轉化成數字,因此在Select * from tradelog where tradeid=110717中,對字符串作了CAST函數操做。在隱式字符編碼轉換中,兩個表存在字符集的不一樣,Convert函數將utf8轉換成utf8mb4。

老師,我目前沒有回答您的課後思考,您的文章難度對於我這種尚未接觸過計算機專業和數據庫工做的同窗來有必定的難度,我先跟着您學習完整個專欄的內容,以後複習時有一點數據庫底子後會思考並回復每一篇的問答題。

做者回復: 👍

中間有任何問題都提出來,
平時本身找個MySQL,跟着驗證例子

四、某、人

QL邏輯相同,性能差別較大的,經過老師所講學習到的,和平時碰到的,大概有如下幾類:

一.字段發生了轉換,致使本該使用索引而沒有用到索引

1.條件字段函數操做
2.隱式類型轉換
3.隱式字符編碼轉換

(若是驅動表的字符集比被驅動表得字符集小,關聯列就能用到索引,若是更大,須要發生隱式編碼轉換,則不能用到索引,latin<gbk<utf8<utf8mb4)

二.嵌套循環,驅動表與被驅動表選擇錯誤

1.鏈接列上沒有索引,致使大表驅動小表,或者小表驅動大表(可是大表走的是全表掃描) --鏈接列上創建索引
2.鏈接列上雖然有索引,可是驅動表任然選擇錯誤。--經過straight_join強制選擇關聯表順序
3.子查詢致使先執行外表在執行子查詢,也是驅動表與被驅動表選擇錯誤。
   --能夠考慮把子查詢改寫爲內鏈接,或者改寫內聯視圖(子查詢放在from後組成一個臨時表,在於其餘表進行關聯)
4.只須要內鏈接的語句,可是寫成了左鏈接或者右鏈接。好比select * from t left join b on t.id=b.id where b.name='abc'驅動表被固定,大機率會掃描更多的行,致使效率下降.
   --根據業務狀況或sql狀況,把左鏈接或者右鏈接改寫爲內鏈接

三.索引選擇不一樣,形成性能差別較大

1.select * from t where aid= and create_name>'' order by id limit 1;
選擇走id索引或者選擇走(aid,create_time)索引,性能差別較大.結果集都有可能不一致
--這個能夠經過where條件過濾的值多少來大概判斷,該走哪一個索引

四.其它一些因素

1.好比以前學習到的是否有MDL X鎖2.innodb_buffer_pool設置得過小,innodb_io_capacity設置得過小,刷髒速度跟不上3.是不是對錶作了DML語句以後,立刻作select,致使change buffer收益不高4.是否有數據空洞5.select選取的數據是否在buffer_pool中6.硬件緣由,資源搶佔緣由多種多樣,還須要慢慢補充。

相關文章
相關標籤/搜索