MySQL优化经验分享记录

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:

  该执行计划中可以看出,typeindex,可能使用的索引是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 filesortUsing 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. 疑问

到这里我先留下几个疑问:

  1. urgency='加急'或者'无'时,为什么使用idx_doc_state的查询效率会更高呢?
  2. urgency='加急'或者'无'是,为什么使用idx_create_time的查询效率会非常的慢?
  3. urgency='特急'时,无论是使用idx_doc_state还是idx_create_time查询效率都很高?
  4. 默认情况下为什么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=indexExtra=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. 补充问题

  1. 为什么表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子句的查询进行优化,在默认情况下尝试选择有序索引以提高查询效率。

  2. 如果urgency='特急'情况,上面数据分布可以看出满足条件的数据有35条,如果最后5条数据包含在图3第6行以下时间范围内,那么limit 30,10会很慢吗?
    答: 是的,如果当前选择的是MySQL选择的是idx_create_time确实会很慢。但是点条件为limit 30,10时,MySQL选择使用idx_doc_state索引进行查询,因为它的查询成本更低。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值