沒內鬼,來點乾貨!SQL優化和診斷
阿新 • • 發佈:2020-07-21
# SQL優化與診斷
## Explain診斷
Explain各引數的含義如下:
| **列名** | **說明** |
| ------------ | ------------------------------------------------------------ |
| id | 執行編號,標識select所屬的行。如果在語句中沒有子查詢或關聯查詢,只有唯一的select,每行都將顯示1.否則,內層的select語句一般會順序編號,對應於其在原始語句中的位置 |
| select_type | 顯示本行是簡單或複雜select,如果查詢有任何複雜的子查詢,則最外層標記為PRIMARY(DERIVED、UNION、UNION RESUIT) |
| table | 訪問引用哪個表(引用某個查詢,如“derived3”) |
| type | 資料訪問/讀取操作型別(All、index、range、ref、eq_ref、const/system、NULL) |
| possible_key | 揭示哪一些索引可能有利於高效的查詢 |
| key | 顯示mysql實際決定採用哪個索引來優化查詢 |
| key_len | 顯示mysql在索引裡使用的位元組數 |
| ref | 顯示了之前的表在key列記錄的索引中查詢值所用的列或常量 |
| rows | 為了找到所需要的行而需要讀取的行數,估算值 |
| Extra | 額外資訊,如using index、filesort等 |
### select_type 常見型別及其含義
- **SIMPLE**:不包含子查詢或者 UNION 操作的查詢
- **PRIMARY**:查詢中如果包含任何子查詢,那麼最外層的查詢則被標記為 PRIMARY
- **SUBQUERY**:子查詢中第一個 SELECT
- **DEPENDENT SUBQUERY**:子查詢中的第一個 SELECT,取決於外部查詢
- **UNION**:UNION 操作的第二個或者之後的查詢
- **DEPENDENT UNION**:UNION 操作的第二個或者之後的查詢,取決於外部查詢
- **UNION RESULT**:UNION 產生的結果集
- **DERIVED**:出現在 FROM 字句中的子查詢
### type常見型別及其含義
- **system**:這是 const 型別的一個特例,只會出現在待查詢的表只有一行資料的情況下
- ```consts```:常出現在主鍵或唯一索引與常量值進行比較的場景下,此時查詢效能是最優的
- **eq_ref**:當連線使用的是完整的索引並且是 PRIMARY KEY 或 UNIQUE NOT NULL INDEX 時使用它
- ```ref```:當連線使用的是字首索引或連線條件不是 PRIMARY KEY 或 UNIQUE INDEX 時則使用它
- **ref_or_null**:類似於 ref 型別的查詢,但是附加了對 NULL 值列的查詢
- **index_merge**:該聯接型別表示使用了索引進行合併優化
- ```range```:使用索引進行範圍掃描,常見於 between、> 、< 這樣的查詢條件
- ```index```:索引連線型別與 ALL 相同,只是掃描的是索引樹,通常出現在索引是該查詢的覆蓋索引的情況
- **ALL**:全表掃描,效率最差的查詢方式
阿里編碼規範要求:```至少要達到 range 級別,要求是 ref 級別,如果可以是 consts 最好```
### key列
實際在查詢中是否使用到索引的標誌欄位
### Extra列
Extra 列主要用於顯示額外的資訊,常見資訊及其含義如下:
- Using where :MySQL 伺服器會在儲存引擎檢索行後再進行過濾
- Using filesort:通常出現在 GROUP BY 或 ORDER BY 語句中,且排序或分組沒有基於索引,此時需要使用檔案在記憶體中進行排序,因為使用索引排序的效能好於使用檔案排序,所以出現這種情況可以考慮通過新增索引進行優化
- Using index:使用了覆蓋索引進行查詢,此時不需要訪問表,從索引中就可以獲取到所需的全部資料
- Using index condition:查詢使用了索引,但是需要回表查詢資料
- Using temporary:表示需要使用臨時表來處理查詢,常出現在 GROUP BY 或 ORDER BY 語句中
### 如何檢視Mysql優化器優化之後的SQL
```mysql
# 僅在伺服器環境下或通過Navicat進入命令列介面
explain extended SELECT * FROM `student` where `name` = 1 and `age` = 1;
# 再執行
show warnings;
# 結果如下:
/* select#1 */ select `mytest`.`student`.`age` AS `age`,`mytest`.`student`.`name` AS `name`,`mytest`.`student`.`year` AS `year` from `mytest`.`student` where ((`mytest`.`student`.`age` = 1) and (`mytest`.`student`.`name` = 1))
```
為什麼要做這個事呢?我們知道Mysql有一個最左匹配原則,那麼如果我的索引建的是age,name,那我以name,age這樣的順序去查詢能否使用到索引呢?實際上是可以的,就是因為Mysql查詢優化器可以幫助我們自動對SQL的執行順序等進行優化,以選取代價最低的方式進行查詢(注意是代價最低,不是時間最短)
## SQL優化
### 超大分頁場景解決方案
如表中資料需要進行深度分頁,如何提高效率?在阿里出品的Java程式設計規範中寫道:
```利用延遲關聯或者子查詢優化超多分頁場景```
說明:MySQL 並不是跳過 offset 行,而是取 offset+N 行,然後返回放棄前 offset 行,返回 N 行,那當 offset 特別大的時候,效率就非常的低下,要麼控制返回的總頁數,要麼對超過特定閾值的頁數進行 SQL 改寫
```mysql
# 反例(耗時129.570s)
select * from task_result LIMIT 20000000, 10;
# 正例(耗時5.114s)
SELECT a.* FROM task_result a, (select id from task_result LIMIT 20000000, 10) b where a.id = b.id;
# 說明
task_result表為生產環境的一個表,總資料量為3400萬,id為主鍵,偏移量達到2000萬
```
### 獲取一條資料時的Limit 1
如果資料表的情況已知,某個業務需要獲取符合某個Where條件下的一條資料,注意使用Limit
說明:在很多情況下我們已知資料僅存在一條,此時我們應該告知資料庫只用查一條,否則將會轉化為全表掃描
```mysql
# 反例(耗時2424.612s)
select * from task_result where unique_key = 'ebbf420b65d95573db7669f21fa3be3e_861414030800727_48';
# 正例(耗時1.036s)
select * from task_result where unique_key = 'ebbf420b65d95573db7669f21fa3be3e_861414030800727_48' LIMIT 1;
# 說明
task_result表為生產環境的一個表,總資料量為3400萬,where條件非索引欄位,資料所在行為第19486條記錄
```
### 批量插入
```mysql
# 反例
INSERT into person(name,age) values('A',24)
INSERT into person(name,age) values('B',24)
INSERT into person(name,age) values('C',24)
# 正例
INSERT into person(name,age) values('A',24),('B',24),('C',24);
# 說明
比較常規,就不多做說明了
```
### like語句的優化
like語句一般業務要求都是 ```'%關鍵字%'```這種形式,但是依然要思考能否考慮使用右模糊的方式去替代產品的要求,其中阿里的編碼規範提到:
```頁面搜尋嚴禁左模糊或者全模糊,如果需要請走搜尋引擎來解決```
```mysql
# 反例(耗時78.843s)
EXPLAIN select * from task_result where taskid LIKE '%tt600e6b601677b5cbfe516a013b8e46%' LIMIT 1;
# 正例(耗時0.986s)
select * from task_result where taskid LIKE 'tt600e6b601677b5cbfe516a013b8e46%' LIMIT 1
##########################################################################
# 對正例的Explain
1 SIMPLE task_result range adapt_id adapt_id 98 99 100.00 Using index condition
# 對反例的Explain
1 SIMPLE task_result ALL 33628554 11.11 Using where
# 說明
task_result表為生產環境的一個表,總資料量為3400萬,taskid是一個普通索引列,可見%%這種匹配方式完全無法使用索引,從而進行全表掃描導致效率極低,而正例通過索引查詢資料只需要掃描99條資料即可
```
### 避免SQL中對where欄位進行函式轉換或表示式計算
```mysql
# 反例
select * from task_result where id + 1 = 15551;
# 正例
select * from task_result where id = 15550;
##########################################################################
# 對正例的Explain
1 SIMPLE task_result const PRIMARY PRIMARY 8 const 1 100.00
# 對反例的Explain
1 SIMPLE task_result ALL 33631512 100.00 Using where
# 說明
其實在知道了有SQL優化器之後,我個人感覺這種普通的表示式轉換應該可以提前進行處理再進行查詢,這樣一來就可以用到索引了,但是問題又來了,如果mysql優化器可以提前計算出結果,那麼寫sql語句的人也一定可以提前計算出結果,所以矛盾點在這個地方,導致5.7版本以前的此種情況都無法使用索引吧,未來可能會對其進行優化
```
### 使用 ISNULL()來判斷是否為 NULL 值
說明:NULL 與任何值的直接比較都為 NULL
```mysql
# 1) NULL<>NULL 的返回結果是 NULL,而不是 false。
# 2) NULL=NULL 的返回結果是 NULL,而不是 true。
# 3) NULL<>1 的返回結果是 NULL,而不是 true。
```
### 多表查詢
我所在的公司基本禁止了多表查詢,那如果必須使用到的話,我們可以一起參考一下阿里的編碼規範
Eg:超過三個表禁止 join。需要 join 的欄位,資料型別必須絕對一致;多表關聯查詢時,保證被關聯的欄位需要有索引
### 明明有索引為什麼還走全表掃描
之前回答一些面試問題的時候,對某一個點的理解出現了偏差,即我認為只要查詢的列有索引則一定會使用索引去Push資料
然而實際上不僅僅是這樣,真正應該是:```針對查詢的資料行佔總資料量過多時會轉化成全表查詢```
那麼這個過多指代的是多少呢?
我的測試結果是50%,但個人認為MySQL優化器不會完全糾結於行數區分是否全表,而是有很多其他因素綜合考慮發現全表掃描的效率更高等等,所以充分認識到該問題即可
### count(*) 還是 count(id)
阿里的Java編碼規範中有以下內容:
```【強制】不要使用 count(列名) 或 count(常量) 來替代 count(*)```
count(*) 是 SQL92 定義的標準統計行數的語法,跟資料庫無關,跟 NULL 和非 NULL 無關。
說明:count(*)會統計值為 NULL 的行,而 count(列名)不會統計此列為 NULL 值的行
### 欄位型別不同導致索引失效
阿里的Java編碼規範中有以下內容:
```【推薦】防止因欄位型別不同造成的隱式轉換,導致索引失效```
實際上資料庫在查詢的時候會作一層隱式的轉換,比如 varchar 型別欄位通過 數字去查詢
```mysql
# 正例
EXPLAIN SELECT * FROM `user_coll` where pid = '1';
type:ref
ref:const
rows:1
Extra:Using index condition
# 反例
EXPLAIN SELECT * FROM `user_coll` where pid = 1;
type:index
ref:NULL
rows:3(總記錄數)
Extra:Using where; Using index
# 說明
pid欄位有相應索引,且格式為varchar
```
## 關於
感謝以下博文及其作者:
[乾貨!SQL效能優化,書寫高質量SQL語句](https://juejin.im/post/5e0f5eec5188253a9d4a436f#heading-4 )
[乾貨!SQL效能優化,書寫高質量SQL語句(二)](https://juejin.im/post/5e1eb8ebf265da3e4244e094)
[MySQL官方文件](https://dev.mysql.com/doc/refman/5.6/en/optimize-overview.html)
## Tips
自建資料表進行測試
```mysql
CREATE TABLE `student` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
`name` varchar(255) NOT NULL,
`class` varchar(255) DEFAULT NULL,
`page` bigint(20) DEFAULT NULL,
`status` tinyint(3) unsigned NOT NULL COMMENT '狀態:0 正常,1 凍結,2 刪除',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4
```
插入資料
```mysql
DELIMITER ;;
CREATE PROCEDURE insertData()
BEGIN
declare i int;
set i = 1 ;
WHILE (i < 1000000) DO
INSERT INTO student(`name`,class,`page`,`status`)
VALUES(CONCAT('class_', i),
CONCAT('class_', i),
i, (SELECT FLOOR(RAND() * 2)));
set i = i + 1;
END WHILE;
commit;
END;;
CALL insertData();
```
![](https://user-gold-cdn.xitu.io/2020/7/13/17348beb1b8e6d6f?w=1024&h=310&f=jpeg&s