本篇实践文章基于mysql8.0
Explain是mysql提供的针对查询语句模拟优化的工具,可以针对输出的结果进行有效分析。
mysql8.0Explian官网地址
https://dev.mysql.com/doc/refman/8.0/en/explain-output.html
建几张表备用
CREATE TABLE `test_idx` (
`id` int NOT NULL,
`a` char(4) NOT NULL,
`b` varchar(10) NOT NULL,
`c` int NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_a_b_c` (`a`,`b`,`c`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
随便执行一行查询语句,发现结果集中出现多列语义。id、select_type、table、type列等等。那其中各列的含义是什么?
set session optimizer_switch='derived_merge=off'; #关闭mysql5.7以后对派生表的优化
explain select (select 1 from actor where id = 1) from (select * from film where id = 1) der;
type列是Explain中最为关键的列。
查询性能排序依次是:
system > const > eq_ref > ref > range > index > ALL
1)Null:优化分解查询语句后,不需要访问表或索引,直接能取到
2)system:经过mysql优化后,表中只有1行数据与之匹配,相当于常量
3)const:使用主键或者unique key查询表单时,因为主键或唯一索引的限制,只会拿到1条结果匹配
4)eq_ref:使用唯一索引或主键索引进行的关联查询,关联主键的结果只有1条匹配。
5)ref:通过非主键索引或唯一索引的部分前缀进行查询,可能出现多个结果
6)range:使用了索引的范围查询,此时会扫描索引的指定范围区间。(该场景如果索引范围太大,最好要分页。)
explain select * from test_idx WHERE id>1;
-----------------------------------------------------------------------------以下为不及格的使用----------------------------------------------------------------------------
7)index:扫描二级索引树下的所有叶子节点(下述场景中,film表中所有字段均被索引或主键覆盖,此时会出现覆盖索引查询)
explain select a,b,c from test_idx;
8)All:全表扫描聚集索引的叶子节点(actor表中的所有数据都要查到,包括非索引字段,此时最优的查询方案就是聚簇索引的全表扫描)
sql语句使用了索引时,计算出走的索引长度。(索引最大长达768字节,过长会走左侧前缀原则压缩索引长度)看如下案例。有如下查询语句:
#KEY `idx_a_b_c` (`a`,`b`,`c`) USING BTREE 索引条件
EXPLAIN SELECT * from test_idx where a='a' and b='text' and c=1;
此时key_len =62的结果是怎么计算的呢?char(a)=4×4 , varchar(b)=4×10+2, int(c) =4
那么他们的计算规则如果?
该列展示的是额外信息。常见的值由如下几种:
1)Using index:使用覆盖索引
该种场景比较多,如果查询时使用二级索引树可以直接查到结果,不必再回表,既是using index。
2)Using where:使用 where 语句来处理结果
3)Using index condition:查询的列不完全被索引覆盖,where条件中是一个前导列的范围;比如:使用联合索引的部分前部索引进行范围查询。
4)Using temporary:使用临时表来存储结果。例如,需要查询出test_inx表中某个字段的所用不同结果,此时如果d字段非索引,那么就需要建临时表来存储distinct的结果。这种场景可以通过优化字段为索引解决,因为索引树已经排序,去重的工作直接在索引树中完成。
5)Using filesort;
本文4.2节详细补充。
如果索引了多个列,需要遵守建表是idx索引的顺序进行查询,不能跳过前列的索引、不能在前列的索引进行范围查询。
构想索引树的顺序。
1)顺序查询
叶子节点中,索引树是按照字段顺序排序好了的。在查询过程中,也必须按照该顺序查询,否则索引树就失效了。可以你看下面这条语句(已知idx(a_b_c)是按照a、b、c的顺序组织的),我们查询的时候特意按照b=? and =?的顺序查,为什么也走了索引呢?
其实是mysql在server层执行词法分析优化时,把查询语句的顺序进行了调整。我们可以通过开启OPTIMIZER_TRACE(trace)验证这一点。
set session optimizer_trace="enabled=on",end_markers_in_json=on;
explain select a,b,c from test_idx WHERE b='zhangsan' and a='a';
SELECT * FROM information_schema.OPTIMIZER_TRACE;
{
/* group_index_range */,
"analyzing_range_alternatives": {
"range_scan_alternatives": [
{
"index": "idx_a_b_c",
"ranges": [
"a <= a <= a AND zhangsan <= b <= zhangsan"
] /* ranges */,
"index_dives_for_eq_ranges": true,
"rowid_ordered": false,
"using_mrr": false,
"index_only": true,
"rows": 1,
"cost": 0.36,
"chosen": false,
"cause": "cost"
}
}
},
}
2)其他错误场景
#无法使用索引或只能使用部分索引的场景
select a,b,c from test_idx WHERE b='zhangsan' and c=1; #跳过第一个字段后,后续b、c字段无法直接排序
select a,b,c from test_idx WHERE a='a' and c=1; #跳过第二个字段b后,c字段无法使用索引
select a,b,c from test_idx WHERE c=1; #跳过前2个字段后,c字段无法使用索引
#不要在索引列做任何函数操作,否则索引失效,转全表扫描
select a,b,c from test_idx WHERE a='a'; #正确用法
select a,b,c from test_idx WHERE left(a,3)='a'; #错误用法
EXPLAIN select * from test_idx WHERE a='a' and b='zhangsan' and c>1; #索引查询时,每个字段都可以通过索引树查询
EXPLAIN select * from test_idx WHERE a='a' and b>'zhangsan' and c=1; #查询时,只有前2个字段走索引
EXPLAIN select * from test_idx WHERE a>'a' and b='zhangsan' and c=1; #只有第一个字段走索引
select a,b,c from test_idx where a=? #此时二级索引数就能解决问题
mysql会根据计算cost耗费,选择耗费小的方式进行查询,有可能会走到ALL全表扫描
前面阿里规约已经详述
mysql5.6版本之前,首字段的where a like ‘zw%’ and b=‘teacher’ and c=28的流程如下图
5.6版本之后,索引下推后,提前过滤后续索引字段,减少大量的回表操作。
我们看到右侧模糊查询时,select * from text_idx where a=‘a’ and b like ‘zhangsan%’ ;可以当做等值查询做索引查询。
这是因为mysql本身也有这样字符串字段截取当索引的查询。
group by实际是order by的一种,其实现相同,且在mysql8.0已不再支持group by,此处只探究order by的过程。
#虽然有字段c存在,但mysql优化为将a过滤后,以b排序
EXPLAIN select * from test_idx WHERE a='a' and c=18 ORDER BY b;
EXPLAIN select * from test_idx WHERE a='a' ORDER BY b,c; #顺序排序
EXPLAIN select * from test_idx WHERE a='a' and b='dev' ORDER BY c,b; #a,b已排序完成,排序c
key_len表示只有字段a走索引(这里在表中注入了10w条数据,调整了a的长度为8个字符)
#中间跳过了b字段,c无法使用索引排序
EXPLAIN select * from test_idx WHERE a='a' ORDER BY c;
EXPLAIN select * from test_idx WHERE a='a' ORDER BY c,b; #倒置的排序
EXPLAIN select * from test_idx WHERE a='a' ORDER BY b ASC,c DESC; #不同的排序规则
EXPLAIN select * from test_idx WHERE a in('a1','a2') ORDER BY b,c; #对于排序来说,in中的结果集是范围查询
排序时,using index表示使用耳二级索引树排序,只需要将二级索引树加载到内存进行排序即可。using filesort表示使用聚簇索引进行排序,加载的是主键索引表。
我们依次来看下面的4句sql。
explain select * from test_idx LIMIT 10000,5;
explain select * from test_idx ORDER BY id LIMIT 10000,5;
explain select * from test_idx where id>10 LIMIT 10000,5;
explain select * from test_idx where id=10 LIMIT 10000,5;
1)第一句sql,直接limit10000,5。此时没有任何约束条件,走全表扫描,效率最差
2)Order By id(id是主键)。此时使用主键作为索引进行索引树扫描(此时主键没有范围区间,实际是覆盖索引的场景)
3)where id>10 LIMIT 10000,5。此时主键id有指定范围,可以使用主键索引的range查询。
4)where id=10 LIMIT 10000,5。此时主键id=10,其实limit已经失效,只需要1次回表就能查到具体值。
分析上述4条语句,实际是效率越来越高的,当业务中能提供的信息越明确,查询效率越好。
看下面语句,即使查询条件不是主键,但只要能合理的使用索引信息,查询的效率依然很高。
注意:千万不要对非索引字段作为查询条件进行排序,filesort效率非常差。
关联表查询的效率也并不推荐使用,但其中的算法需要明白。建2张表用于大小表嵌套关联使用
CREATE TABLE `t1` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`a` int(11) DEFAULT NULL,
`b` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_a` (`a`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
create table t2 like t1;
drop procedure if exists insert_t1;
delimiter ;;
create procedure insert_t1()
begin
declare i int;
set i=1;
while(i<=10000)do
insert into t1(a,b) values(i,i);
set i=i+1;
end while;
end;;
delimiter ;
call insert_t1();
drop procedure if exists insert_t2;
delimiter ;;
create procedure insert_t2()
begin
declare i int;
set i=1;
while(i<=100)do
insert into t2(a,b) values(i,i);
set i=i+1;
end while;
end;;
delimiter ;
call insert_t2();
概念:驱动表和被驱动表。
1)当关联表语句使用join或inner join时,mysql的优化器会分析关联表的数据量大小,把数量小的表作为驱动表,数据量大的作为被驱动表。
2)使用left join时,left左表作为被驱动表,右手作为被驱动表。
3)当使用right join时,右手作为驱动表,左表为被驱动表。因为left、right已经明确了表关系。
当查询的列是索引列时(t1、t2表中a字段是索引idx_a(a))
EXPLAIN SELECT * from t1 inner JOIN t2 on t1.a = t2.a;
EXPLAIN SELECT * from t1 inner JOIN t2 on t1.b = t2.b;
我们通常会认为count(*)查询所有数据,执行效率会比较低。从而计算count时,直接count(primary key)。实际他们的执行效率几乎相差无几。
EXPLAIN SELECT COUNT(1) from t1;
EXPLAIN SELECT COUNT(id) from t1;
EXPLAIN SELECT COUNT(a) from t1;
EXPLAIN SELECT COUNT(*) from t1;