一、Extra
顾名思义,Extra列是用来说明一些额外信息的,包含不适合在其他列中显示但十分重要的额外信息。可以通过这些额外信息来
更准确的理解MySQL到底将如何执行给定的查询语句
。
1.1、No tables used
当查询语句的没有
FROM
子句时将会提示该额外信息
- SQL
EXPLAIN SELECT 1;
1.2、Impossible WHERE
查询语句的
WHERE
子句永远为FALSE
时将会提示该额外信息
- SQL
EXPLAIN SELECT * FROM s1 WHERE 1 != 1;
1.3、Using where
不用读取表中所有信息,仅通过索引就可以获取所需数据,这发生在对表的全部的请求列都是同一个索引的部分的时候,表示MySQL服务器将在存储引擎检索行后再进行过滤。表明使用了 where 过滤。
1.3.1、WHERE子句中有针对该表的搜索条件时
当使用全表扫描执行对某个表的查询,并且该语句的
WHERE
子句中有针对该表的搜索条件时,在Extra
列中会提示上述额外信息
- SQL
EXPLAIN SELECT * FROM s1 WHERE common_field = 'a';
image.png
1.3.2、WHERE子句中有针对该表的搜索条件,同时有索引条件
当使用索引访问来执行对某个表的查询,并且该语句的
WHERE
子句中有除了改索引包含的列之外的其他搜索条件时,在Extra
列中也会提示上述额外信息。比如下边这个查询虽然使用idx_key1
索引执行查询,但是搜索条件中除了包含key1
的搜索条件key1='a'
,还包含common_field
的搜索条件,所以Extra
列会显示Using where
- SQL
EXPLAIN SELECT * FROM s1 WHERE key1 = 'a' AND common_field = 'a';
image.png
1.4、No matching min/max row
当查询列表处有
MIN
或者MAX
聚集函数,但是并没有符合WHERE
子句中的搜索条件的记录时,将会提示该额外信息
- SQL
EXPLAIN SELECT MIN(key1) FROM s1 WHERE key1 = 'abcdefg';
image.png
1.5、Using index
当我们的查询列表以及搜索条件中
只包含属于某个索引的列
,也就是在可以使用索引覆盖
的情况下,在Extra
列将会提示该额外信息
- SQL
EXPLAIN SELECT key1 FROM s1 WHERE key1 = 'a';
image.png
1.5.1、二级索引直接使用 索引覆盖
- SQL
EXPLAIN SELECT id, key1 FROM s1 WHERE key1 = 'a';
image.png
- 分析
由于
key1
是索引idx_key1
的列,是一个二级索引,所以当查询 id, key1 时,由于二级索引中已经包含有id,key1,索引不需要回表
,这也就是索引覆盖
1.5.2、二级索引无法使用 索引覆盖
- SQL
EXPLAIN SELECT id, key1, key3 FROM s1 WHERE key1 = 'a';
image.png
1.6、Using index condition
有些搜索条件中虽然出现了索引列,但却不能使用到索引
- SQL
DESC
SELECT *
FROM s1
WHERE key1 > 'z'
AND key1 LIKE '%b';
image.png
-
key1 LIKE '%b'
无法使用索引
DESC
SELECT *
FROM s1
WHERE key1 LIKE '%b';
image.png
-
key1 > 'z'
使用索引
DESC
SELECT *
FROM s1
WHERE key1 > 'z';
image.png
分析
-
1、其中的
key1 > 'z'
可以使用到索引,但是key1 LIKE '%b'
却无法使用到索引,在以前版本的MySQL中是按照下面步骤来执行这个查询的- 1.1、先根据
key1 > 'z'
这个条件,从二级索引idx_key1
中获取到对应的二级索引记录 - 1.2、根据上一步骤得到的
二级索引
记录中的主键值进行回表
,找到完整的用户记录再检测该记录是否符合key1 LIKE '%b'
这个条件,将符合条件的记录加入到最后的结果集
- 1.1、先根据
-
2、但是虽然
key1 LIKE '%b'
不能组成范围区间参与range
访问方法的执行,但这个条件毕竟只涉及到了key1
列,所以MySQL把上边的步骤改进了一下- 2.1、先根据
key1 > 'z'
这个条件,定位到二级索引idx_key1
中对应的二级索引
记录 - 2.2、对于指定的
二级索引
记录,先不着急回表
,而是先检测一下该记录是否满足key1 LIKE '%b'
这个条件,如果这个条件不满足,则该二级索引
记录压根儿就没必要回表
- 2.3、对于满足
key1 LIKE '%b'
这个条件的二级索引记录执行回表
操作
- 2.1、先根据
-
3、
回表
操作其实是一个随机IO
,比较耗时,所以上述修改虽然只改进了一点点,但是可以省去好多回表
操作的成本。MySQL把他们的这个改进称之为索引条件下推
。如果在查询语句的执行过程中将要使用索引条件下推
这个特性,在Extra列中将会显示Using index condition
1.7、Using join buffer (Block Nested Loop)
在连接查询执行过程中,当
被驱动表
不能有效的利用索引加快访问速度,MySQL一般会为其分配一块名为join buffer
的内存块来加速查询速度,也就是我们所讲的基于块的嵌套循环算法
- SQL
EXPLAIN SELECT * FROM s1 INNER JOIN s2 ON s1.common_field = s2.common_field;
image.png
- 可以在对
被驱动表s1
的执行计划的 Extra 列显示了两个提示- 1、
Using join buffer (hash join)
:这是因为表s1的访问不能有效利用索引,只好退而求其次,使用join buffer
来减少对 s1 表的访问次数,从而提高性能 - 2、
Using where
:可以看到查询语句中有一个s1.common_field = s2.common_field
条件,因为 s2是驱动表,s1是被驱动表,所以在访问s1表时,s2.common_field
的值已经确定下来了,所以实际上查询s1表的条件就是s1.common_field = 一个常数
,所以提示了Using where
额外信息
- 1、
1.8、Not exists
当使用作(外)连接时,如果
WHERE
子句中包含要求被驱动表
的某个列等于NULL
值的搜索条件,而且那个列又是不允许存储NULL
值的,那么在该表的执行计划的Extra 列就会提示Not exists
额外信息
- SQL
EXPLAIN SELECT * FROM s1 LEFT JOIN s2 ON s1.key1 = s2.key1 WHERE s2.id IS NULL;
image.png
- 分析
上述查询中 s1 表时驱动表,s2表是被驱动表,
s2.id
列是不允许存储NULL
值的,而WHERE
子句中又包含s2.id IS NULL
的搜索条件,这意味着必定是驱动表
的记录在被驱动表
中找不到匹配ON
子句条件的记录才会把该驱动表
的记录加入到最终的结果集,所以对于某条驱动表
中的记录来说,如果能在被驱动表
中找到1条符合ON
子句条件的记录,那么该驱动表的记录就不会被加入到最终的结果集,也就是说我们没有必要到被驱动表中找到全部符合ON子句条件的记录,这样可以稍微节省一点性能
1.9、Using intersect(...) 、 Using union(...) 和 Using sort_union(...)
1、如果执行计划的
Extra
列出现了Using intersect(...)
提示,说明准备使用Intersect
索引合并的方式执行查询,括号中的. . .
表示需要进行索引合并的索引名称;
2、如果出现了Using union(...)
提示,说明准备使用Union
索引合并的方式执行查询
3、出现了Using sort_union(...)
提示,说明准备使用Sort-Union
索引合并的方式执行查询
- SQL
EXPLAIN SELECT * FROM s1 WHERE key1 = 'a' OR key3 = 'a';
image.png
- 其中
Extra
列就显示了Using union(idx_key1,idx_key3)
,表明MySQL即将使用idx_key1
和idx_key3
这两个索引进行Union
索引合并的方式执行查询
1.10、Zero limit
当
LIMIT
子句的参数为 0 时,表示压根不打算从表中读出任何记录,将会提示该额外信息
- SQL
EXPLAIN SELECT * FROM s1 LIMIT 0;
image.png
1.11、Using filesort
有一些情况下对结果集中的记录进行排序时可以使用到索引的
- SQL
EXPLAIN SELECT * FROM s1 ORDER BY key1 LIMIT 10;
image.png
- 分析
这个查询语句可以利用
idx_key1
索引直接取出key1
列的 10 条记录,然后再进行回表操作
就好了。但是很多情况下排序操作无法使用到索引,只能在内存中(记录较少的时候)或者磁盘中(记录较多的时候)进行排序,MySQL把这种在内存中或者磁盘上进行排序的方式统称为文件排序
。如果某个查询需要使用文件排序的方式执行查询,就会在执行计划的Extra
列中显示Using filesort
提示
- SQL
EXPLAIN SELECT * FROM s1 ORDER BY common_field LIMIT 10;
image.png
- 如果查询中需要使用
filesort
的方式进行排序的记录非常多,那么这个过程是很耗费性能的,最好想办法将使用文件排序的执行方式改为使用索引进行排序
1.12、Using temporary
在许多查询的执行过程中,MySQL可能会借助
临时表
来完成一些功能,比如去重
、排序
之类的,比如我们在执行许多包含DISTINCT
、GROUP BY
、UNION
等子句的查询过程中,如果不能有效利用索引来完成查询,MySQL很有可能寻求通过建立内部的临时表
来执行查询。如果查询中使用到了内部的临时表
,在执行计划的Extra
列将会显示Using temporary
提示
- SQL
EXPLAIN SELECT DISTINCT common_field FROM s1;
image.png
- GROUP BY
EXPLAIN SELECT common_field, count(*) AS amount FROM s1
GROUP BY common_field;
image.png
- 使用
索引
来替代掉使用临时表
执行计划中出现
Using temporary
并不是一个好的征兆,因为建立与维护临时表要付出很大成本的,所以最好能使用索引
来替代掉使用临时表
- SQL
EXPLAIN
SELECT key1, COUNT(*) AS amount
FROM s1
GROUP BY key1;
image.png
1.13、小结
- EXPLAIN不考虑各种 Cache
- EXPLAIN不能显示MySQL在执行查询时所作的优化工作
- EXPLAIN不会告诉你关于
触发器
、存储过程
的信息或用户自定义函数对查询的影响情况 - 部分统计信息是估算的,并非精确值
二、EXPLAIN四种输出格式
EXPLAIN可以输出四种格式:
传统格式
,JSON格式
,TREE格式
以及可视化输出
。用户可以根据需要选择适用于自己的格式
2.1、JSON格式
JSON格式:在EXPLAIN单词和真正的查询语句中间加上 FORMAT=JSON 。
- 语法
EXPLAIN FORMAT=JSON SELECT ....
- EXPLAIN 的 Column 与 JSON 的对应关系 Column 与 JSON 的对应关系.png
-
举例
EXPLAIN FORMAT = JSON
SELECT *
FROM s1
INNER JOIN s2 s2 ON s1.key1 = s2.key2
WHERE s1.common_field = 'a';
- JSON结果
{
"query_block":{
"select_id":1,
"cost_info":{
"query_cost":"2102.20"
},
"nested_loop":[
{
"table":{
"table_name":"s1",
"access_type":"ALL",
"possible_keys":[
"idx_key1"
],
"rows_examined_per_scan":9895,
"rows_produced_per_join":989,
"filtered":"10.00",
"cost_info":{
"read_cost":"914.80",
"eval_cost":"98.95",
"prefix_cost":"1013.75",
"data_read_per_join":"2M"
},
"used_columns":[
"id",
"key1",
"key2",
"key3",
"key_part1",
"key_part2",
"key_part3",
"common_field"
],
"attached_condition":"((`testbigdata`.`s1`.`common_field` = 'a') and (`testbigdata`.`s1`.`key1` is not null))"
}
},
{
"table":{
"table_name":"s2",
"access_type":"eq_ref",
"possible_keys":[
"idx_key2"
],
"key":"idx_key2",
"used_key_parts":[
"key2"
],
"key_length":"5",
"ref":[
"testbigdata.s1.key1"
],
"rows_examined_per_scan":1,
"rows_produced_per_join":989,
"filtered":"100.00",
"index_condition":"(cast(`testbigdata`.`s1`.`key1` as double) = cast(`testbigdata`.`s2`.`key2` as double))",
"cost_info":{
"read_cost":"989.50",
"eval_cost":"98.95",
"prefix_cost":"2102.20",
"data_read_per_join":"1M"
},
"used_columns":[
"id",
"key1",
"key2",
"key3",
"key_part1",
"key_part2",
"key_part3",
"common_field"
]
}
}
]
}
}
-
read_cost
是由下边这两部分组成的-
IO
成本 - 检测
rows x (1 - filter)
条记录的CPU
成本
-
rows和filter都是我们前边介绍执行计划的输出列,在JSON格式的执行计划中,rows相当于rows_examined_per_scan,filtered名称不变。
-
eval_cost
是这样计算的
检测
rows × filter
条记录的成本
-
prefix_cost
就是单独查询s1
表的成本,也就是:
read_cost + eval_cost
-
data_read_per_join
表示在此次查询中需要读取的数据量 -
对于 s2 表的 "cost_info" 部分是这样的
"cost_info":{
"read_cost":"989.50",
"eval_cost":"98.95",
"prefix_cost":"2102.20",
"data_read_per_join":"1M"
}
由于
s2
表是被驱动表
,所以可能被读取多次
,这里的read_cost
和eval_cost
是访问多次s2
表后累加起来的值,大家主要关注里边儿的prefix_cost
的值代表的是整个连接查询预计的成本,也就是单次查询s1
表和多次查询s2
表后的成本的和
2.2、TREE格式
TREE格式是8.0.16版本之后引入的新格式,主要根据查询的
各个部分之间的关系
和各部分的执行顺序
来描述如何查询
- SQL
EXPLAIN FORMAT =tree SELECT * FROM s1 INNER JOIN s2 ON s1.key1 = s2.key2 WHERE s1.common_field = 'a';
三、 SHOW WARNINGS的使用
需要在终端中执行,客户端可能无法输出信息
在我们使用
EXPLAIN
语句查看了某个查询的执行计划后,紧接着还可以使用SHOW WARNINGS
语句查看与这个查询的执行计划有关的一些扩展信息
- SQL
EXPLAIN
SELECT s1.key1, s2.key1
FROM s1
LEFT JOIN s2 ON s1.key1 = s2.key1
WHERE s2.common_field IS NOT NULL;
image.png
mysql> SHOW WARNINGS\G;
*************************** 1. row ***************************
Level: Warning
Code: 1739
Message: Cannot use ref access on index 'idx_key1' due to type or collation conversion on field 'key1'
*************************** 2. row ***************************
Level: Note
Code: 1003
Message: /* select#1 */ select `testbigdata`.`s1`.`key1` AS `key1`,`testbigdata`.`s2`.`key1` AS `key1` from `testbigdata`.`s1` join `testbigdata`.`s2` where ((`testbigdata`.`s2`.`common_field` is not null) and (`testbigdata`.`s1`.`key1` = `testbigdata`.`s2`.`key1`))
2 rows in set (0.00 sec)
- 简化
SELECT s1.`key1` AS `key1`, s2.`key1` AS `key1`
FROM s1 s1
JOIN s2 s2
WHERE ((s2.`common_field` IS NOT NULL) AND (s1.`key1` = s2.`key1`));
SHOW WARNINGS
展示出来的信息有三个字段,分别是Level
、Code
、Message
。最常见的就是Code为1003
的信息,当Code值为1003
时,Message
字段展示的信息类似于查询优化器将我们的查询语句重写后的语句。比如我们上边的查询本来是一个左(外)连接查询,但是有一个s2.common_field IS NOT NULL
的条件,这就会导致查询优化器把左(外)连接查询优化为内连接查询,从SHOW WARNINGS
的Message
字段也可以看出来原本的LEFT JOIN
已经变成了JOIN
注意!!!
:Message
字段展示的信息类似于查询优化器将我们的查询语句重写后的语句
,并不是等价于标准的查询语句,在很多情况下并不能直接拿到Message
中的SQL运行,它只能作为帮助我们理解MySQL将如何执行查询语句的一个参考依据而已
三、分析优化器执行计划:trace
OPTIMIZER_TRACE
是MySQL5.6引入的一项跟踪功能,它可以跟踪优化器做出的各种决策(如访问表的方法、各种开销计算、各种转换等),并将跟踪结果记录到INFORMATION_SCHEMA.OPTIMIZER_TRACE
表中。次功能默认是关闭。开启 trace,并设置格式为 JSON,同时设置 trace 最大能够使用的内存大小,避免解析过程中因为默认内存过小而不能够完整展示
- 开启 trace
SET OPTIMIZER_TRACE = 'enabled=on',END_MARKERS_IN_JSON = on;
SET OPTIMIZER_TRACE_MAX_MEM_SIZE = 1000000;
- 开启后,可分析如下语句
SELECT、INSERT、REPLACE、UPDATE、DELETE、EXPLAIN、SET、DECLARE、CASE、IF、RETURN、CALL
- 测试
SELECT *
FROM student
WHERE id < 10;
- 查询 information_schema.optimizer_trace 就可以知道MySQL是如何执行SQL的
*************************** 1. row ***************************
# 第一部分:查询语句
QUERY: select * from student where id < 10
# 第二部分:QUERY字段对应语句的跟踪信息
TRACE: {
"steps": [
{
"join_preparation": { # 预备工作
"select#": 1,
"steps": [
{
"expanded_query": "/* select#1 */ select `student`.`id` AS `id`,`student`.`stuno` AS `stuno`,`student`.`name` AS `name`,`student`.`age` AS `age`,`student`.`classid` AS `classid` from `student` where (`student`.`id` < 10)"
}
] /* steps */
} /* join_preparation */
},
{
"join_optimization": { # 进行优化
"select#": 1,
"steps": [
{
"condition_processing": { # 条件处理
"condition": "WHERE",
"original_condition": "(`student`.`id` < 10)",
"steps": [
{
"transformation": "equality_propagation",
"resulting_condition": "(`student`.`id` < 10)"
},
{
"transformation": "constant_propagation",
"resulting_condition": "(`student`.`id` < 10)"
},
{
"transformation": "trivial_condition_removal",
"resulting_condition": "(`student`.`id` < 10)"
}
] /* steps */
} /* condition_processing */
},
{
"substitute_generated_columns": { # 替换生成的列
} /* substitute_generated_columns */
},
{
"table_dependencies": [ # 表的依赖关系
{
"table": "`student`",
"row_may_be_null": false,
"map_bit": 0,
"depends_on_map_bits": [
] /* depends_on_map_bits */
}
] /* table_dependencies */
},
{
"ref_optimizer_key_uses": [ # 使用键
] /* ref_optimizer_key_uses */
},
{
"rows_estimation": [ # 行判断
{
"table": "`student`",
"range_analysis": {
"table_scan": {
"rows": 7727979,
"cost": 794484
} /* table_scan */, # 扫描表
"potential_range_indexes": [ # 潜在的范围索引
{
"index": "PRIMARY",
"usable": true,
"key_parts": [
"id"
] /* key_parts */
}
] /* potential_range_indexes */,
"setup_range_conditions": [ # 设置范围条件
] /* setup_range_conditions */,
"group_index_range": {
"chosen": false,
"cause": "not_group_by_or_distinct"
} /* group_index_range */,
"skip_scan_range": {
"potential_skip_scan_indexes": [
{
"index": "PRIMARY",
"usable": false,
"cause": "query_references_nonkey_column"
}
] /* potential_skip_scan_indexes */
} /* skip_scan_range */,
"analyzing_range_alternatives": { # 分析范围选项
"range_scan_alternatives": [
{
"index": "PRIMARY",
"ranges": [
"id < 10"
] /* ranges */,
"index_dives_for_eq_ranges": true,
"rowid_ordered": true,
"using_mrr": false,
"index_only": false,
"in_memory": 4.61915e-05,
"rows": 9,
"cost": 1.91987,
"chosen": true
}
] /* range_scan_alternatives */,
"analyzing_roworder_intersect": {
"usable": false,
"cause": "too_few_roworder_scans"
} /* analyzing_roworder_intersect */
} /* analyzing_range_alternatives */,
"chosen_range_access_summary": { # 选择范围访问摘要
"range_access_plan": {
"type": "range_scan",
"index": "PRIMARY",
"rows": 9,
"ranges": [
"id < 10"
] /* ranges */
} /* range_access_plan */,
"rows_for_plan": 9,
"cost_for_plan": 1.91987,
"chosen": true
} /* chosen_range_access_summary */
} /* range_analysis */
}
] /* rows_estimation */
},
{
"considered_execution_plans": [ # 考虑执行计划
{
"plan_prefix": [
] /* plan_prefix */,
"table": "`student`",
"best_access_path": { # 最佳访问路径
"considered_access_paths": [
{
"rows_to_scan": 9,
"access_type": "range",
"range_details": {
"used_index": "PRIMARY"
} /* range_details */,
"resulting_rows": 9,
"cost": 2.81987,
"chosen": true
}
] /* considered_access_paths */
} /* best_access_path */,
"condition_filtering_pct": 100, # 行过滤百分比
"rows_for_plan": 9,
"cost_for_plan": 2.81987,
"chosen": true
}
] /* considered_execution_plans */
},
{
"attaching_conditions_to_tables": { # 将条件附加到表上
"original_condition": "(`student`.`id` < 10)",
"attached_conditions_computation": [
] /* attached_conditions_computation */,
"attached_conditions_summary": [ # /附加条件概要
{
"table": "`student`",
"attached": "(`student`.`id` < 10)"
}
] /* attached_conditions_summary */
} /* attaching_conditions_to_tables */
},
{
"finalizing_table_conditions": [
{
"table": "`student`",
"original_table_condition": "(`student`.`id` < 10)",
"final_table_condition ": "(`student`.`id` < 10)"
}
] /* finalizing_table_conditions */
},
{
"refine_plan": [ # 精简计划
{
"table": "`student`"
}
] /* refine_plan */
}
] /* steps */
} /* join_optimization */
},
{
"join_execution": { # 执行
"select#": 1,
"steps": [
] /* steps */
} /* join_execution */
}
] /* steps */
}
#第三部分:跟踪信息过长时,被截断的跟踪信息的字节数。
MISSING_BYTES_BEYOND_MAX_MEM_SIZE: 0 # 丢失的超出最大容量的字节
# 第四部分:执行跟踪语句的用户是否有查看对象的权限。
# 当不具有权限时,该列信息为1且TRACE字段为空,
# 一般在 调用带有SQL SECURITY DEFINER的视图或者是存储过程的情况下,会出现此问题
INSUFFICIENT_PRIVILEGES: 0 # 缺失权限
1 row in set (0.00 sec)
网友评论