深入理解MySQL之SQL調優

前言

結合MySQL中B+樹的索引結構和Explain關鍵子給出的查詢計劃,我們就可以有針對性的對一些慢SQL進行優化,這篇博客就來總結一下優化思路。
先聲明一下表結構:

CREATE TABLE `s1` (
	`id` INT NOT NULL AUTO_INCREMENT,
	`key1` VARCHAR(100),
	`key2` VARCHAR(100),
	`key3` VARCHAR(100),
	`key_part1` VARCHAR(100),
	`key_part2` VARCHAR(100),
	`key_part3` VARCHAR(100),
	`common_field` VARCHAR(100),
	PRIMARY KEY (`id`),
	KEY idx_key1 (`key1`),
	UNIQUE KEY idx_key2 (`key2`),
	KEY idx_key3 (`key3`),
	KEY idx_key_part(`key_part1`, `key_part2`, `key_part3`)
) Engine=InnoDB CHARSET=utf8;
CREATE TABLE `s2` (
	`id` INT NOT NULL AUTO_INCREMENT,
	`key1` VARCHAR(100),
	`key2` VARCHAR(100),
	`key3` VARCHAR(100),
	`key_part1` VARCHAR(100),
	`key_part2` VARCHAR(100),
	`key_part3` VARCHAR(100),
	`common_field` VARCHAR(100),
	PRIMARY KEY (`id`),
	KEY idx_key1 (`key1`),
	UNIQUE KEY idx_key2 (`key2`),
	KEY idx_key3 (`key3`),
	KEY idx_key_part(`key_part1`, `key_part2`, `key_part3`)
) Engine=InnoDB CHARSET=utf8;

兩張表除了表名外結構一樣,除了id主鍵索引外,還爲key1idx_key3建了普通索引,爲key2建了唯一索引,爲key_part1key_part2key_part3建了聯合索引。s1表中有1000條數據,s2表中有10000條數據。

一般查詢優化

常見的 type(訪問類型)從最優到最差分別爲:system > const > eq_ref > ref > ref_or_null > range > index > ALL,一般來說,得保證查詢達到 range 級別,最好達到 ref,如果沒有達到就需要考慮優化了。

避免全字段查詢

儘量在查詢的時候,指定查詢需要的字段,減少SELECT *的使用,在可能使用索引覆蓋的場景下,不僅可以節省MySQL查詢優化器對於是否回表的判斷開銷,還可以減少MySQL回表的次數,掃描索引樹就能獲得結果。比如:

EXPLAIN SELECT key_part1, key_part2, key_part3 FROM s1 WHERE key_part1 = "bb";

在這裏插入圖片描述

等值匹配

這種情況的優化方式一般是儘量讓搜索條件能用上索引,注意聯合索引儘量不要出現中間斷開的場景,比如:

EXPLAIN SELECT key_part1, key_part2, key_part3 FROM s1 WHERE key_part1 = "bb" AND key_part3 = "aa";

在這裏插入圖片描述
從 key 長度爲 303 可以得知只用到了聯合索引的第一個列。

模糊匹配

模糊查詢可以使用右模糊查詢,儘量不要使用左模糊查詢,比如:

EXPLAIN SELECT key_part1, key_part2, key_part3 FROM s1 WHERE key_part1 LIKE "bb%"

在這裏插入圖片描述
右模糊的查詢類型是 range 級別,再看一下左模糊:

EXPLAIN SELECT key_part1, key_part2, key_part3 FROM s1 WHERE key_part1 LIKE "%bb"

在這裏插入圖片描述
此時查詢類型就變成 index 級別了。

分頁查詢優化

一般的分頁語句:

EXPLAIN SELECT * FROM s2 LIMIT 9000, 5;

在這裏插入圖片描述
在表記錄連續不會物理刪除的情況下,即主鍵連續自增的情況下,可以這樣優化:

EXPLAIN SELECT * FROM s2 WHERE id > 9000 LIMIT 5;

在這裏插入圖片描述
顯然優化後的 SQL 走了索引,而且掃描的行數大大減少,執行效率更高。但是,優化後的 SQL 在很多場景並不適用,因爲表中可能某些記錄被刪後,主鍵空缺,導致結果不一致。
所以這種優化得滿足以下兩個條件:

  • 主鍵自增且連續
  • 結果是按照主鍵排序的

一般的分頁排序語句:

EXPLAIN SELECT * FROM s2 ORDER BY key_part1 LIMIT 9000, 5;

在這裏插入圖片描述
可以看到並沒有使用到聯合索引,這是因爲掃描整個索引樹並查找到沒索引的行(可能要遍歷多個索引樹)的成本比掃描全表的成本更高,所以優化器放棄使用索引。

優化思路是讓排序時返回的字段儘可能少,可以讓排序和分頁操作先查出主鍵,然後根據主鍵查到對應的記錄,SQL 改寫如下:

EXPLAIN SELECT * FROM s2 s INNER JOIN (SELECT id FROM s2 ORDER BY key_part1 LIMIT 9000, 5) t ON t.id = s.id;

在這裏插入圖片描述
原 SQL 使用的是 filesort 排序,而優化後的 SQL 使用的是索引排序,且分頁和排序使用到了聯合索引。

排序分組優化

MySQL支持兩種方式的排序 filesort 和 index,Using index 是指 MySQL 掃描索引本身完成排序,filesort 是使用文件排序。index 效率高,filesort 效率低。
排序分組優化方式一般是儘量讓排序和分組字段用上索引,比如:

EXPLAIN SELECT key_part1, key_part2, key_part3 FROM s1 WHERE key_part1 = "bb" ORDER BY key_part1;
EXPLAIN SELECT key_part1, key_part2, key_part3 FROM s1 WHERE key_part1 = "bb" GROUP BY key_part1 ORDER BY NULL;

order by 滿足兩種情況會使用 Using index:

  • order by 語句使用索引最左前列
  • 使用 where 子句與 order by 子句條件列組合滿足索引最左前列

比如:

EXPLAIN SELECT * FROM s1 WHERE key_part1 = "bb" ORDER BY key_part1, key_part3;

在這裏插入圖片描述
此時排序條件跳過了聯合索引的中間列,導致需要使用文件排序。

需要注意的點:

  • 如果是聯合索引,不要出現中間斷開的場景
  • 不要出現倒序正序交替的場景,要麼都是倒序,要麼都是正序
  • where 高於 having,能寫在 where 中的限定條件就不要在 having 中限定

用於排序的聯合索引中間斷開的場景,比如:

EXPLAIN SELECT key_part1, key_part2, key_part3 FROM s1 WHERE key_part1 = "bb" ORDER BY key_part1, key_part3;

在這裏插入圖片描述
可以看到只用到了聯合索引的第一個列。

出現倒序正序交替的場景,比如:

EXPLAIN SELECT key_part1, key_part2, key_part3 FROM s1 WHERE key_part1 = "bb" AND key_part2 = "aa" ORDER BY key_part1, key_part2;

在這裏插入圖片描述
此時都是正序排序,Extra 提示 Using index 掃描索引就可以完成排序,如果正序倒序交替,比如:

EXPLAIN SELECT * FROM s1 WHERE key_part1 = "bb" ORDER BY key_part1 ASC, key_part3 DESC;

在這裏插入圖片描述
此時 Extra 提示 Using filesort 需要用到文件排序。

限定條件用在 having 和 where 的比較:

EXPLAIN SELECT key_part1, COUNT(id) num FROM s1 GROUP BY key_part2 HAVING key_part1 = "aa";

在這裏插入圖片描述

EXPLAIN SELECT key_part1, COUNT(id) num FROM s1 WHERE key_part1 = "aa" GROUP BY key_part2;

在這裏插入圖片描述

關聯查詢優化

MySQL的表關聯常見有兩種算法:

  • Nested-Loop Join(NLJ) 算法
  • Block Nested-Loop Join(BNL) 算法

1、 嵌套循環連接 Nested-Loop Join(NLJ) 算法
循環從驅動表中讀取行數據,在這行數據中獲取到關聯字段,根據關聯字段在被驅動表裏取出滿足條件的行,然後取出兩張表的結果合集。

EXPLAIN SELECT * FROM s2 INNER JOIN s1 ON s1.key3 = s2.key3;

在這裏插入圖片描述
從執行計劃中可以看到這些信息:

  • 驅動表是 s1,被驅動表是 s2。可以看到優化器一般會優先選擇小表作爲驅動表。所以使用 inner join 時,排在前面的表並不一定就是驅動表。
  • 使用了 NLJ 算法。一般 join 語句中,如果執行計劃 Extra 中未出現 Using join buffer 則表示使用的算法是 NLJ。

SQL的執行過程大致如下:

  • 從表 s1 中讀取一行數據
  • 從取出的一行數據中取出關聯字段 key3,到表 s2 中查找
  • 取出表 s2 中滿足條件的行,跟 s1 中獲取到的結果合併,作爲結果返回給客戶端
  • 重複上面步驟,直到連接條件不滿足

整個過程會讀取 s1 表的所有數據(掃描1000行),然後遍歷每行數據中字段 key3 的值,根據 s1 表中 key3 的值索引掃描 s2 表中的對應行(掃描1000次 s2 表的索引,1次掃描可以認爲最終只掃描 s2 表一行完整數據,也就是總共 s2 表也掃描了1000 行)。因此整個過程掃描了 2000 行。

2、基於塊的嵌套循環連接 Block Nested-Loop Join(BNL)算法
把驅動表的數據讀入到 join_buffer 中,然後掃描被驅動表,把被驅動表每一行讀取出來跟 join_buffer 中的數據做對比。如果被驅動表的關聯字段沒索引,MySQL會選擇 Block Nested-Loop Join 算法。

EXPLAIN SELECT * FROM s2 INNER JOIN s1 ON s1.common_field = s2.common_field;

在這裏插入圖片描述
Extra 中的 Using join buffer (Block Nested Loop)說明該關聯查詢使用的是 BNL 算法。 上面SQL的執行過程大致如下:

  • 把 s1 所有的數據放入到 join_buffer 中
  • 把表 s2 中每一行數據取出來,跟 join_buffer 中的數據做對比
  • 返回滿足連接條件的數據

整個過程對錶 s1 和 s2 都做了一次全表掃描,因此掃描的總行數爲11000行(表 s1 的數據總量 1000 + 表 s2 的數據總量10000)。並且 join_buffer 裏的數據是無序的,因此對錶 s2 中的每一行,都要做 1000 次判斷,所以內存中的判斷次數是 1000 * 10000= 1000 萬次。
可以看到這種算法的掃描行數爲兩張表的記錄總和,並且還有兩張表數據量乘積的內存比較操作,但是相比於磁盤掃描,內存比較操作要快得多。

被驅動表的關聯字段沒索引爲什麼要選擇使用 BNL 算法而不使用 NLJ 呢?
如果第二條SQL使用 NLJ 算法,由於沒有索引,所以只能遍歷掃描全表,那麼磁盤掃描表數據就達到了1000 * 10000= 1000 萬次,而用 BNL 算法的磁盤掃描行數會少很多,相比於磁盤掃描,BNL 算法的內存計算會快得多。

因此MySQL對於被驅動表的關聯字段沒有索引的關聯查詢,一般爲了減少磁盤掃描的 IO 開銷,會傾向於使用 BNL 算法。如果有索引一般選擇 NLJ 算法,有索引的情況下 NLJ 算法比 BNL 算法性能更高。

對於關聯查詢的優化:

  • 關聯字段加索引,讓MySQL做關聯操作時儘量選擇 NLJ 算法
  • 小表驅動大表,寫多表連接SQL時如果明確知道哪張表是小表可以用 straight_join 寫法固定連接驅動方式,省去MySQL優化器優化的性能開銷。

straight_join 功能同 join 類似,但能指定讓左邊的表來驅動右邊的表,能修改表優化器對於聯表查詢的執行順序。
比如可以用 NLJ 算法查詢的SQL語句,使用了 straight_join 後換成了 BNL 算法:

EXPLAIN SELECT * FROM s2 STRAIGHT_JOIN s1 ON s1.common_field = s2.key1;

在這裏插入圖片描述
強制用 s2 表作爲驅動表後,原來能用 NLJ 算法現在MySQL使用了 BNL 算法來執行查詢。使用的時候注意兩點:

  • 只適用於 inner join,並不適用於 left join,right join。(因爲left join,right join已經指定了表的執行順序)
  • 儘可能讓優化器去判斷,因爲大部分情況下MySQL優化器是比人要聰明的。使用 straight_join 一定要慎重,部分情況下人爲指定的執行順序並不一定會比優化引擎要靠譜。

總結

  • 爲表建立合適的索引,但是也不要建立太多索引,索引有開銷和維護的成本,不合適的索引反而適得其反
  • 模糊查詢可以使用右模糊查詢,儘量不要使用左模糊查詢
  • 聯合索引要遵守最左前綴法則,查詢從索引的最左列開始並且不跳過索引中的列
  • 儘量在索引列上完成排序,遵循索引建立(索引創建的順序)時的最左前綴法則
  • 不在索引列上做任何操作(計算、函數、類型轉換等),會導致索引失效而轉向全表掃描
  • 儘量使用覆蓋索引(只訪問索引包含的列的查詢),減少SELECT * 語句
  • MySQL在使用不等於(!= 或者 <>)的時候無法使用索引會導致全表掃描
  • IS NULL,IS NOT NULL 無法使用索引,有的版本可以
  • 字符串不加單引號會導致索引失效
  • 少用 OR 或 IN,MySQL內部優化器會根據檢索比例、 表大小等多種因素整體評估是否使用索引
  • 範圍查詢優化,將大的查詢範圍拆成多個小的查詢範圍
  • 關聯字段加索引,讓MySQL做關聯操作時儘量選擇 NLJ 算法
  • 小表驅動大表,寫多表連接SQL時如果明確知道哪張表是小表可以用 straight_join 寫法固定連接驅動方式,省去MySQL優化器優化的性能開銷。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章