MySql優化- 執行計划解讀與優化(二)


待閱

https://mp.weixin.qq.com/s/IN2mzyOXdVWE0NQJr1egcA

說明

解讀執行計划l對於我們日常工作中慢sql的分析和調優有很大幫助,同時在解讀的過程中也能知道如何規避慢sql

建議需要了解join匹配原理的知識:https://www.cnblogs.com/LQBlog/p/10711743.html

查看sql優化器優化后的sql

EXPLAIN EXTENDED select * from( select * from( select cn.`id` from `cpn_coupon` cn
where cn.`create_timestamp` >='2019-12-27'))tab2

SHOW WARNINGS 

 

mysql執行計划表結構

各個字段詳解

測試表結構說明

sl_sales_bill_copy1 訂單抬頭表

sl_sales_bill_copy1訂單行項目表

order_status 訂單狀態變動表

id

執行順序 值越大的優先執行 如果相同則根據從上到下的順序來確定 如果為null則最后執行

#查出訂單狀態存在1010的訂單的所有行項目信息
EXPLAIN select * from sl_sales_bill_copy1 lb join sl_sales_bill_head_copy1 lh on lh.SALES_BILL_NO = lb.SALES_BILL_NO where lb.SALES_BILL_NO in(select ls.SALES_BILL_NO from order_status ls where ls.status_code=1010)

先執行id為2查詢 將查詢結果保存為臨時表,再執行id為1的 從上到下,先將lb為驅動表關聯查詢lh得出結果 subquery2臨時表 然后作為臨時表去非驅動表ls查詢數據

select_type

用途:查詢的類型,主要是用於區分普通查詢、聯合查詢、子查詢等復雜的查詢

1、SIMPLE:簡單的select查詢,查詢中不包含子查詢或者union 
2、PRIMARY:查詢中包含任何復雜的子部分,最外層查詢則被標記為primary 
3、SUBQUERY:被驅動的SELECT子查詢(子查詢位於FROM子句 where)
4、DEPENDENT SUBQUERY 子查詢依賴外部查詢,慎用
5、DERIVED:在from列表中包含的子查詢被標記為derived(衍生),mysql或遞歸執行這些子查詢,把結果放在臨時表里
6、MATERIALIZED 物化子查詢,子查詢來自視圖
7、UNION:在 UNION 查詢語句中的第二個和緊隨其后的 SELECT8、UNION RESULT:組中union結果的臨時表 9、DEPENDENT SUBQUERY 子查詢依賴外部查詢(慎用)
10、DEPENDENT UNION 子查詢中包含union

SIMPLE

explain select * from cpn_coupon where id=1

SUBQUERY

先通過子查詢得出id 再根據id值去查詢

EXPLAIN select * from cpn_coupon c where c.id =(select max(cn.coupon_id) from `cpn_coupon_code` cn
where cn.`create_timestamp` >='2019-12-27')

 

DEPENDENT SUBQUERY

慎用子查詢依賴外部查詢,會先根據外部查詢得出一個臨時表 再根據臨時表 去觸發子查詢  因為p表沒有查詢條件 每條數據都會觸發一次o表查詢  現在數據 566條  相當於觸發了566 t2 select 數據量大就更誇張(就算t2走了索引頁會導致性能問題) 建議改成select join group by  可以理解為p表查詢結果為臨時表。在for循環遍歷每一條數據 查詢o表

explain select * from prm_page_promotion p
where exists(select p.id from prm_page_promotion o where p.id=o.parent_id)

DERIVED

子查詢位於form處 先通過create_timestamp查詢結果到臨時表  再根據臨時表id查詢

EXPLAIN select * from( select cn.`id` from `cpn_coupon` cn
where cn.`create_timestamp` >='2019-12-27')tab2 where tab2.id=1

MATERIALIZED

--創建視圖
create view v1 as select cn.coupon_id from `cpn_coupon_code` cn
where cn.`create_timestamp` >='2019-12-27'
--使用視圖為子查詢
EXPLAIN select * from cpn_coupon c where c.id in(select v1.coupon_id from v1)

 

UNION/UNOIN_RESULT

cp2為union表 可以看出是先執行cpu2再執行cp2 primary為最外層,然后匯聚成UNION RESULT 結果

explain select * from cpn_coupon where id=578032073359495168
union all
select * from cpn_coupon where id=580952931153494016

DEPENDENT UNION

explain select * from cpn_coupon cp where cp.id  in(select cp1.id from cpn_coupon  cp1 where cp1.id=578032073359495168
union all
select cp2.id from cpn_coupon cp2 where cp2.id=580952931153494016)

cp2 cp1 查詢結果為unioReuslt再通過cp表子查詢union結果集

table 

table 列表示 EXPLAIN 的單獨行的唯一標識符。這個值可能是表名、表的別名或者一個未查詢產生臨時表的標識符,如派生表、子查詢或集合。

當 FROM 子句中有子查詢時,如果優化器采用的物化方式,table 列是 <derivenN> 格式,表示當前查詢依賴 id=N 的查詢,於是先執行 id=N 的查詢。

當使用 UNION 查詢時,UNION RESULT 的 table 列的值為 <UNION1,2>,1和2表示參與 UNION 的 SELECT 的行 id。

具體可參考 上面:select_type unino和 DERIVED的執行計划

type

這一列表示關聯類型或訪問類型,即MySQL決定如何查找表中的行,查找數據行記錄的大概范圍。依次從最優到最差分別為:

system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL

一般來說,好的sql查詢至少達到range級別,最好能達到ref

system

const的特例平時無法重現 可以忽略 

const

表示通過掃描索引 掃描一行就找到了數據 const用於比較primary key 或者 unique索引。因為只需匹配一行數據,所有很快。如果將主鍵置於where列表中,mysql就能將該查詢轉換為一個const 

 

eq_ref

表連接時 被驅動表關聯字段查詢為主鍵或者唯一索引查找時

lb.id為主鍵索引

如果where條件中查詢非驅動表為非唯一索引或者主鍵索引則會降為ref

ref

被驅動表關聯字段查詢為非唯一索索引和主鍵索引的普通索引時

lh.SALES_BILL_NO為普通索引

range

表示范圍查詢 常見於between 和>, >=,<, <= 前提是字段有建立btree索引

 count未建立索引執行計划

count建立索引后

index

與ALL類似 只是index全表掃描掃描的是索引頁而不是數據行

因為我們id做了索引 所以只需要去索引頁里面取出所有id數據就好了

ALL

全表掃描

possible_keys

指出MySQL可能使用哪個索引在表中找到行,查詢涉及到的字段上若存在索引,則該索引將被列出,但不一定被查詢使用

key

實際使用的索引,如果為NULL,則沒有使用索引,如果出現possible_keys有值但是 key為null 可能是在數據少量情況下 mysql優化器認為全表掃描比走索引快,所以放棄使用索引

如果想強制 MySQL使用或忽視 possible_keys 列中的索引,在查詢中使用 force indexignore index

key_len

表示索引中使用的字節數,查詢中使用的索引的長度(最大可能長度),並非實際使用長度,理論上長度越短越好。key_len是根據表定義計算而得的,不是通過表內檢索出的

1.字符串類型key_length計算規則

各個字符集長度 utf8mb4=4,utf8=3,gbk=2,latin1=1

key_len=(表字符集長度) * 列長度 + 1(null) + 2(變長列)

char(n):n字節長度

varchar(n):2字節存儲字符串長度,如果是utf-8,則長度 3n + 2

注意:該索引列可以存儲NULL值,則key_len比不可以存儲NULL值時多1個字節。

比如:varchar(50),字符集為utf8 則實際占用的key_len長度是 3 * 50 + 2 = 152,如果該列允許存儲NULL,則key_len長度是153。

2.數值類型key_length計算規則

tinyint:1字節 smallint:2字節 int:4字節 bigint:8字節

3.時間類型

date:3字節 timestamp:4字節 datetime:8字節

 

索引最大長度是768字節,當字符串過長時,MySQL 會做一個類似左前綴索引的處理,將前半部分的字符提取出來做索引。

舉例1:

id為主鍵索引 為bigint 索引key_length=8

 

explain select cp1.id from cpn_coupon  cp1 where cp1.id=578032073359495168

 

 

 

舉例2:

coupon_name為varchar(64) 按照計算公式key_len=(表字符集長度) * 列長度 + 1(null) + 2(變長列)  4*64+1+2=259

explain select cp1.id from cpn_coupon  cp1 where cp1.coupon_name='a'

 

 

 

ref

ref 列顯示了在 key 列記錄的索引中,表查找值所用到的列或常量,常見的有:const(常量),字段名(例:user.id

rows

列是查詢優化器估計要讀取並檢測的行數,注意這個不是結果集里的行數。

如果查詢優化器使用全表掃描查詢,rows 列代表預計的需要掃碼的行數;如果查詢優化器使用索引執行查詢,rows 列代表預計掃描的索引記錄行數。

Extra

Extra 列提供了一些額外信息。這一列在 MySQL中提供的信息有幾十個,這里僅列舉一些常見的重要值如下:

Using index 

表示使用了覆蓋索引,覆蓋索引:表示索引包含了返回所有列 而不回表

 

Using where Using index 

查詢的列被索引覆蓋,並且 WHERE 篩選條件是索引列之一 id為主鍵  name為普通索引

explain select cp1.id from cpn_coupon  cp1 where cp1.coupon_name='a'

 

 

NULL

查詢的列未被索引覆蓋,並且 WHERE 篩選條件是索引的前導列,意味着用到了索引,但是部分字段未被索引覆蓋,必須通過 回表 來查詢,不是純粹地用到了索引,也不是完全沒用到索引。

id主鍵 introduction沒有索引,需要根據回表獲取到對應數據

explain select cp1.id,cp1.introduction from cpn_coupon  cp1  

Using where

Using where的作用只是提醒我們MySQL將用where子句來過濾結果集

Using index condition

查詢的列不完全被索引覆蓋 where條件為二級索引 需要根據二級索引存儲的聚簇索引id 獲得數據才能拿到introduction(回表)

id為主鍵 introduction沒有索引 coupon_name有索引

explain select cp1.id,cp1.introduction from cpn_coupon  cp1 where cp1.coupon_name='a'

 

 

 

Using temporary

表示mysql需要臨時表轉存數據 常見於 group by、DISTINCT、ORDER BY使用非索引字段  一般出現這種情況就需要考慮進行優化了,首先是想到用索引來優化。

 

表示使用了非索引字段排序

Using filesort

如果一個排序操作不能通過索引來完成,那這次排序操作就叫做filesort,這跟file沒有任何關系。filesort應該叫做sort,而它的實現,就是大家熟悉的快排(一般由於 排序列沒有建索引導致)

有時候存在Using filesort,也未必是什么大不了的:如

 select * from t_talbe order by id;只是告訴你它使用了“all rows”。

什么是回表查詢如何避免回表

首先看聚簇索引和非聚簇索引構成點擊跳轉

例子1:

id為聚簇索引 name為非聚簇索引 通過聚簇索引name查找可以找到id 無須回表查詢效率高

select id,name from user where name='shenjian'; 

Extra:Using index。

例子2

id為聚簇索引 name為非聚簇索引,sex為非索引,sex需要根據聚簇索引獲取到對應的數據行(回表操作) 如果將name和sex升級為聯合索引則無須回表

select id,name,sex from user where name='shenjian';

Extra:Using index condition

 

不要使用cont(*) 避免回表 使用 coun(索引列)

 

如何查找mysql中的慢sql

1.查看mysql是否開啟mansql記錄日志

show variables like 'slow_query_log';

2.慢sql記錄時間

 

show variables like 'long_query_time';

3.設置記錄mysql為打開狀態

set global slow_query_log='ON';OFF為關閉

設置超過一秒的sql都將記錄

set global long_query_time=1

設置記錄文件

set global slow_query_log_file='/var/lib/mysql/test_1116.log';

查看記錄文件

show variables like 'slow_query_log_file';

優化實踐 

數據表

# 重建 `staff` 表
DROP TABLE `staff`;
CREATE TABLE `staff` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(24) NOT NULL DEFAULT '' COMMENT '姓名',
  `s_name` VARCHAR(24) NOT NULL DEFAULT '' COMMENT  '花名',
  `s_no` INT(4) NOT NULL DEFAULT 0 COMMENT  '工號',
  `work_age` int(11) NOT NULL DEFAULT '0' COMMENT '工齡',
  `position` varchar(20) NOT NULL DEFAULT '' COMMENT '職位',
  `arrival_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '入職時間',
  `remark` VARCHAR(500) DEFAULT NULL COMMENT '備注', # 允許 NULL
  PRIMARY KEY (`id`), # 主鍵
  UNIQUE KEY idx_s_name (s_name), # 唯一索引
  KEY idx_s_no (s_no), # 普通索引
  KEY `idx_name_age_position` (`name`,`work_age`,`position`) USING BTREE # 聯合索引
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COMMENT='員工記錄表';
# 初始化 `staff` 表數據
INSERT INTO staff(name,s_name,s_no,work_age,position,arrival_time) VALUES('zhangsan','zs',10,2,'manager',NOW());
INSERT INTO staff(name,s_name,s_no,work_age,position,arrival_time) VALUES('lisi','ls',11,3,'dev',NOW());
INSERT INTO staff(name,s_name,s_no,work_age,position,arrival_time) VALUES('wangwu','ww',12,8,'dev',NOW());
INSERT INTO staff(name,s_name,s_no,work_age,position,arrival_time) VALUES('zhangliu','zl',110,5,'dev',NOW());
INSERT INTO staff(name,s_name,s_no,work_age,position,arrival_time) VALUES('xiaosun','xs',111,5,'dev',NOW());
INSERT INTO staff(name,s_name,s_no,work_age,position,arrival_time) VALUES('donggua','dg',200,3,'dev',NOW());

全值匹配

EXPLAIN SELECT * FROM staff WHERE name= 'zhangsan';

 

 

 

EXPLAIN SELECT * FROM staff WHERE name= 'zhangsan' AND work_age = 2;

 

 

 

EXPLAIN SELECT * FROM staff where name = 'zhangsan' AND work_age = 2 AND position = 'dev';

 

 

 

EXPLAIN SELECT * FROM staff where position = 'dev' AND name = 'zhangsan' AND work_age = 2;

這里我們將聯合索引最左name排在后面 也能正常使用索引時因為mysql優化后會幫我們排在前面,在寫的過程中 還是要根據聯合索引順序編寫,避免二次優化

 

 

最佳左前綴法則

如果索引了多列,要遵守最左前綴法則。指的是查詢從索引的最左前列開始並且不跳過索引中的列。

跳過了name(全表掃描)

EXPLAIN SELECT * FROM staff WHERE work_age = 2 AND position ='dev';

 

 

 

跳過了name和work_age(全表掃描)

EXPLAIN SELECT * FROM staff WHERE position = 'dev';

 

 

索引列上避免做計算操作

EXPLAIN SELECT * FROM staff WHERE LEFT(name, 5) = 'zhang';
EXPLAIN SELECT * FROM staff WHERE LOWER(name) = 'zhangsan';

 

 

 查詢值上函數運算是沒問題的 

EXPLAIN SELECT * FROM staff WHERE name = LEFT('zhang',5);

 

 

其實很好理解比如你通過hashMap存儲根據name快速找到對應對象,如果你要根據key做處理匹配 就只能遍歷keys 做完處理再比較

但是如果你正式查詢條件使用函數,你只是在get之前 通過函數轉了一下查詢值 不會影響索引

范圍條件右邊的列無法使用索引

EXPLAIN SELECT * FROM staff WHERE name= 'zhangsan' AND work_age > 2 AND position ='dev';

 

 

 根據上面key_length計算規則  idx_name+age的索引長度 正好為78 索引position沒有用到索引

只是從 name = 'zhangsan' AND work_age > 2 條件返回的結果集中,再過濾符合 position 字段條件的數據。

盡量使用覆蓋索引

1覆蓋索引:簡單理解,只訪問建了索引的列。減少使用 SELECT * 語句查詢列。 避免回表

未回表查詢

EXPLAIN SELECT name,work_age FROM staff WHERE name= 'zhangsan' AND work_age = 3;

 

 

 回表查詢

因為查詢 返回了非索引字段 所以需要根據聚簇索引找到對應的數據行

EXPLAIN SELECT name,work_age,s_no FROM staff WHERE name= 'zhangsan' AND work_age = 3;
EXPLAIN SELECT * FROM staff WHERE name= 'zhangsan' AND work_age = 3;

 

 

范圍條件查找能夠命中索引

例子1:

若條件中范圍列有普通索引和主鍵索引同時存在, 優先使用主鍵索引:

EXPLAIN SELECT * FROM staff WHERE staff.s_no > 10 AND staff.id > 2;

 

 

例子2: 

EXPLAIN SELECT * FROM staff  WHERE staff.name != 'zl' AND staff.s_no > 1 ;

 

 

可以看到s_no沒有走 索引  因為數據量比較小,sql優化引擎認為全表掃描比索引快,因為會回表 需要查詢2次

我們可以改為force index強制走索引

EXPLAIN SELECT * FROM staff force index(idx_s_no) WHERE staff.name != 'zl' AND staff.s_no > 1 ;

 

 

IS NOT NULL 無法使用索引

EXPLAIN select * from cpn_coupon c
where c.coupon_name is null

 

 

 

 

 

EXPLAIN select * from cpn_coupon c
where c.coupon_name is not null

 

模糊條件查詢以通配符開頭索引失效

EXPLAIN SELECT * from staff where name like '%zhang%';
EXPLAIN SELECT * from staff where name like '%zhang';

 

 

EXPLAIN SELECT * from staff where name like 'zhang%';

 

 改為覆蓋索引將走索引

EXPLAIN SELECT name,work_age FROM staff WHERE name LIKE '%zhang%';

 

避免隱式轉換

未加單引號

EXPLAIN SELECT * FROM staff WHERE name = 1

 

 

可以理解為

EXPLAIN SELECT * FROM staff WHERE CONVERT(name,int) = 1;

OR多數情況會失效 

因為是or查詢,所以格努work_age找索引 沒有滿足最左前綴匹配將全表掃描

EXPLAIN SELECT * FROM staff WHERE name='zhangsan' OR work_age = 2;

 

優化為將走索引

EXPLAIN SELECT * FROM staff WHERE name='zhangsan' OR (name='zhangsan' and work_age = 2);

 

使用覆蓋查詢將走索引

explain SELECT name,work_age FROM staff WHERE name='zhangsan' OR work_age = 2;

EXPLAIN SELECT * FROM staff WHERE name='zhangsan' OR s_name='wangwu';

 

 

 使用union改進

EXPLAIN SELECT * FROM staff WHERE name='zhangsan' 
union all
 SELECT * FROM staff WHERE s_name='wangwu';

 

id2 后面extra是因為 唯一索引如果查詢不存在的值將不會走索引 參考:https://www.cnblogs.com/huahua035/p/10573930.html

注意所以再日常使用中不要使用 先查詢判斷是否存在 再插入 直接插入 讓mysql拋出異常 並捕獲異常比如用戶注冊,但是建立了唯一索引查詢的時候就要特別注意 查詢的地方 

負向查詢條件不能使用索引

負向查詢條件包括:!=、<>、NOT IN、NOT EXISTS、NOT LIKE 等。

EXPLAIN SELECT * FROM staff WHERE id !=1 AND id != 2; 
EXPLAIN SELECT * FROM staff WHERE id NOT IN (1,2);

 

 in則可以命中

EXPLAIN SELECT * FROM staff WHERE id IN (11,12);

排序對索引的影響

ORDER BY是經常用的語句,排序也遵循最左前綴列的原則。

EXPLAIN SELECT * FROM staff ORDER BY name,work_age;

 

EXPLAIN SELECT name,work_age FROM staff ORDER BY name,work_age;

 

 

 覆蓋索引可以命中索引

索引優化總結

1.更新非常頻繁字段不宜建索引

因為字段更新台頻繁,會導致B+樹的頻繁的變更,重建索引。所以這個過程是十分消耗數據庫性能的。

2.區分度不大的字段不宜建索引

比如類似性別這類的字段,區分度不大,建立索引的意義不大。因為不能有效過濾數據,性能和全表掃描相當。另外注意一點,返回數據的比例在 30% 之外的,優化器不會選擇使用索引。

3.業務中有唯一特性的字段,建議建成唯一索引

業務中如果有唯一特性的字段,即使是多個字段的組合,也盡量都建成唯一索引。盡管唯一索引會影響插入效率,但是對於查詢的速度提升是非常明顯的。此外,還能夠提供校驗機制,如果沒有唯一索引,高並發場景下,可能還會產生臟數據。

但是要小心 查詢不存在的數據不走索引

4.多表關聯時,要確保關聯字段上必須有索引


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM