1. 一段SQL
话不多说,直接看SQL
select * from 表A a -- force index(idx_doc_state)
left join (
select doc_id, group_concat(name) names from 表B group by doc_id
) bb bb.doc_id = a.doc_Id
where
-- a.urgency = '无'
-- a.urgency = '加急'
a.urgency = '特急'
and a.doc_state in ('02','03','04')
-- 还有其他表A的条件,这里省略
order by a.create_time desc
limit 0,10
SQL- 1
这是一段我工作中遇到的SQL,有部分省略但整体不影响。写这篇博客主要是个给自己做个笔记,同时也想分享一下自己的经历,希望得到各位大佬的批评指点😁。2. 表中的索引
表A主键是doc_id
,表B的doc_id
是一个单列非唯一索引,表A另外还有两个索引分别是idx_create_time(create_time)
, idx_doc_state(doc_state)
3. 存在的问题
奇怪的现象: 当条件urgency='无'
时查询时间需要6s,当条件urgency='加急'
时查询时间需要6s,当条件urgency='特急'
时却只需要16ms。
实在想不通,仅仅是改变了字段的值查询效率会差这么多。
4. 查看执行计划
改变条件字段urgency
的值是不会影响执行计划的,explain
看一下:
图1:
该执行计划中可以看出,type
是index
,可能使用的索引是idx_doc_state
,真实使用的索引是idx_create_time
,然后在Extra
中包含Backward index scan
。
开始也没搞懂,type=index
是扫描整棵索引树(idx_create_time
索引树)的意思。Backward index scan
意思是使用倒序索引扫描,应该是使用了order by a.create_time desc
的原因,执行引擎会倒序扫描idx_create_time
索引树。
还是没有思路该如何优化。于是,我决定开始删减查询条件看能否发现些什么。当前urgency='加急'
,第一步我先删除了limit 0,10
条件,运气挺好,查询时间立刻从6s变成的了31ms。但是本来感觉有limit
限制查询的数据会更少,查询效率应该会更好呀😂。再次查看执行计划,看看有什么变化。
图2:
果然有变化,type
变成了range
级别,索引使用的是idx_doc_state
然后Extra
没有了Backward index scan
,但是多了Using filesort
和 Using index condition
意思是分别使用了文件排序和索引条件操作。
虽然,删除limit
条件后查询效率确实提高了,但limit
也是分页必不可少的,但同时使用的索引也发生了变化,从idx_create_time
变成了idx_doc_state
,所以有可能是索引变化影响的查询效率。于是我还原了limit
条件,然后在SQL语句“select * from 表A a
”后面加上了一段“force index(idx_doc_state)
”表示强制A表使用索引idx_doc_state
,执行SQL发现依然是31ms
,没错的确使用idx_doc_state
索引,大大提高查询效率。
5. 疑问
到这里我先留下几个疑问:
urgency='加急'
或者'无'
时,为什么使用idx_doc_state
的查询效率会更高呢?urgency='加急'
或者'无'
是,为什么使用idx_create_time
的查询效率会非常的慢?urgency='特急'
时,无论是使用idx_doc_state
还是idx_create_time
查询效率都很高?- 默认情况下为什么MySQL执行引擎会选择
idx_create_time
索引进行查询?
6. 表中的数据分布
现在我们先看看这两张表的数据的基本情况和分布:
表A有数据1023980
条,表B有数据1531
条;
表A的数据分布:
其中urgency
的不同值包含的数据条数分别是:
‘无’->1024882条 ‘加急’->6条 ‘特急’->92条
;
其中doc_status
不同值包含的数据条数分别是
'01'->1023680条 '02'->1243条 '03'->0条 '04'->57条
;
表A满足不同条件的数据分布:
当urgency='无'
,共有11
条数据满足SQL所有条件;
当urgency='加急'
,共有2
条数据满足SQL所有条件;
当urgency='特急'
,共有35
条数据满足SQL所有条件。
还有一点,这是测试环境的数据表,测试人员在表A中创建了大量的测试数据,所有100多万条urgency='无' and doc_state='01'
的数据,这部分数据的create_time
基本集中在一天。
通过下列SQL来查看一下数据的分布情况:
select
urgency,
doc_state,
left(create_time,10) ctime,
count(1) cnt
from 表A
group by urgency, doc_state, left(create_time,10)
order by ctime desc
SQL- 2
图3:
大致情况如图3,其中省略了部分行,不影响效果展示。以上就是该表的数据分布情况。
7. 问题分析
问题1:
接下来我们先带着以上问题来分析一下,首先是第1个问题,当SQL使用的索引是idx_doc_state
时,无论urgency
的取值是'无'
、'加急'
还是'特急'
SQL的执行效率基本都在31ms
左右。原因很简,因为表总共包含1023980
条数据,其中就有1023680
条数据是doc_state='01'
,而我们要查询的数据仅在'02'
、'03'
和'04'
范围内的数据,这部分数据总共才1300
条,而这部分数据足够少,所以通过idx_doc_state
索引我们可以很快的找出这部分数据。
问题2:
然后是第2问题,默认情况下优化器选择的是idx_create_time
索引进行查询,由图1的执行计划可以看出,type=index
和Extra=Backward index scan
执行器会按顺序倒序扫描idx_create_time
索引树来查找数据,先扫描到时间最大的一条数据获取主键ID,然后回表判断其他条件是否满足。如果满足,则成功获取到A表的一条数据;如果不满足,就继续扫描第二条数据,由于limit 0,10
的限制,需要获取到前10条满足条件的数据,便会停止扫描查找,如果不足10条,便会一直扫描直到扫描完所有的数据为止。
现在我们来观察图3,可以看到大部分数据集中在第6行,条件是urgency='无' and doc_state='01'
,显然这部分数据1010102
条都不满足我们的SQL条件,并且这些数据时间都集中在2021-07-02
这一天。我们就以第6行数据为界观察,可以看到所有加急
的数据都在第6行之上,所以扫描少量数据都可以找到全部加急
的数据,但是由于limit 0,10
的限制条件,需要扫描到10条数据或者扫描完整张表才会停止扫描,因为加急
数据总共还不足10条,所以最终会完整的扫描整张表才能获取到数据,这个查询效率肯定会很低。
顺便一提: 图3中第6行包含的数据被扫描了,就差不多相当于扫描了整张表了效率会很低。因为这边部分数据量和整张表数据总数差不多了,而且这部分数据都是不满足条件的数据。
以上数据分布中所说,当urgency='加急'
,共有2
条数据满足条件,也就是说这两条数据正好在图3中的第3行中包含了,如果修改SQL limit 0,2
,索引依然是idx_create_time
是否马上就可以扫描到满足条件的两条数据然后返回呢?经验证,正是如此,这里就不展示了。当然urgency='无'
时也是同理,以为图3第6行之前的时间范围内的数据中没有找够10条满足条件的数据,所以会继续向下扫描的图3第6行这部分的数据然后判断筛选,所以效率会很低。 而urgency='特急'
时,以上数据分布中说满足SQL条件的数据有35
条,这中间大部份数据都在图3第6行2021-07-02
以上时间范围内,所以在扫描图3第6行包含的数据之前,就能过获取到10条满足条件的数据,并终止扫描。因为扫描的数据量少所以查询的效率也高(扩展思考,见最后补充中的第2个问题)。
问题3:
第3个问题通过上面的解释应该清楚了,使用idx_doc_state
索引时是因为要查询的a.doc_state in ('02','03','04')
范围内的数据量少,通过索引可以很快查到。而使用idx_create_time
索引时,因为order by a.create_time desc
倒序扫描索引的原因,而满足其他条件及limit 0,10
条件的数据都在图3第6行时间2021-07-02
之前的时间范围内,所以也只需要扫描少量的数据,就能查到满足条件的数据,然后停止扫描返回数据。
问题4:
最后是第4个问题,为什么MySQL会选择使用idx_create_time
索引来查询数据。这个考虑到MySQL查询成本的计算.。具体这么计算这里就不介绍了,可以通过在 explain
后面加format=json
来查看,例如:
explain format=json
select * from 表A a -- force index(idx_doc_state)
left join (
select doc_id, group_concat(name) names from 表B group by doc_id
) bb bb.doc_id = a.doc_Id
where
-- a.urgency = '无'
-- a.urgency = '加急'
a.urgency = '特急'
and a.doc_state in ('02','03','04')
-- 还有其他表A的条件,这里省略
order by a.create_time desc
limit 0,10
SQL- 3
上面的SQL会返回json
格式的执行计划的详细信息,里面有个query_cost
的字段显示当前SQL的查询成本值,值越小通常认为效率越高,通过开启SQL中注释的部分force index(idx_doc_state)
就可以查看到使用索引idx_doc_state
的执行成本,因为使用idx_create_time
索引的执行成本比使用idx_doc_state
的执行成本低,索引MySQL选择了使用idx_create_time
索引来查询数据.。但是,成本值低也不一定查询效率就会高,就像本例中的SQL,成本值只是一个常规估计值,而本例是人为添加了测试数据,大部分数据集中在一点,分布极其不均匀,已经不常规了😁,所以具体情况具体分析吧。
8. 补充问题
-
为什么表A的
possible_keys
中只有idx_doc_state
,而使用的索引却是idx_create_time
;同时为什么刚开始优化SQL时,去掉limit 0,10
执行计划使用的索引会变成idx_doc_state
?
答: 这源于MySQL对limit和order by子句的优化,详情请看官方文档https://dev.mysql.com/doc/refman/8.0/en/limit-optimization.html。
检点来说就是MySQL优化器会对带有ORDER BY或GROUP BY和LIMIT子句的查询进行优化,在默认情况下尝试选择有序索引以提高查询效率。 -
如果
urgency='特急'
情况,上面数据分布可以看出满足条件的数据有35
条,如果最后5条数据包含在图3第6行以下时间范围内,那么limit 30,10
会很慢吗?
答: 是的,如果当前选择的是MySQL选择的是idx_create_time
确实会很慢。但是点条件为limit 30,10
时,MySQL选择使用idx_doc_state
索引进行查询,因为它的查询成本更低。