一次对group by时间致使的慢查询的优化

前言:

最近在测试环境中点击一个图表展现页面时,半天才获得后台响应的数据进行页面渲染展现,后台的响应很慢,这样极大的下降了用户的体验;sql

发现这个问题后立刻进行了排查 ,经过排查发现是由一个查询很慢的 group by 语句致使的;数据库

本文主线:微信

①、简单描述下排查步骤;函数

②、对 group by 查询慢进行优化;工具

简单描述下排查步骤:

排查主要分为了两个步骤:性能

  • 后台接口的监控,看看哪一个方法调用时耗时多
  • 数据库开启慢查询日志,记录执行很慢的SQL

推荐使用阿里开源的Java线上诊断工具 Arthas ,使用其 trace 命令统计方法调用链路上各个方法节点的耗时;学习

Arthas 工具的具体使用方法可参考: 线上服务响应时间太长的排查心路测试

经过使用Arthas工具统计到一个进行数据库的 group by查询 方法耗时很严重;优化

为了进一步肯定是这个查询SQL 很耗时,将MySql 的慢查询日志开启了,而后再次调用后台这个接口,发现慢查询日志中确实存在了这个SQL语句;日志

SQL语句以下:

SELECT
	date_format(createts, '%Y') AS YEAR
FROM
	t_test_log
GROUP BY
	date_format(createts, '%Y')
ORDER BY
	createts DESC

这个SQL语句是用来统计表中全部数据被建立时的年份;

下面就来聊聊这个SQL为何会比较慢,而后进行了怎样的优化;

对 group by 查询慢进行优化:

在优化group by查询的时候,通常会想到下面这两个名词,经过下面这两种索引扫描能够高效快速的完成group by操做:

  • 松散索引扫描(Loose Index Scan)
  • 紧凑索引扫描(Tight Index Scan)

group by操做在没有合适的索引可用时,一般先扫描整个表提取数据并建立一个临时表,而后按照group by指定的列进行排序;在这个临时表里面,对于每个group 分组的数据行来讲是连续在一块儿的。

完成排序以后,就能够获得全部的groups 分组,并能够执行汇集函数(aggregate function)。

能够看到,在没有使用索引的时候,须要建立临时表和排序;那在执行计划的 Extra 额外信息中一般就会看到这些信息 Using temporary; Using filesort 出现 。

一、首先查看下SQL的执行计划:

获得这个慢查询的SQL后,立刻使用 explain 关键字分析其执行计划:

经过查看执行计划发现,这个SQL语句走的是 全表扫描 ,而且经过扫描了大概 99974 行记录后才获得最终的结果集,而且执行过程当中使用到了临时表和文件辅助排序;

二、SQL执行计划内容简述:

查看执行计划时,主要看上图中花圈的那三项数据便可:

  • type:访问类型,这是sql查询优化中一个很重要的指标,结果值从好到坏依次是:

  • Rows:数据行,根据表统计信息及索引选用状况,大体估算出找到所需的记录所须要读取的行数;

  • Extra:额外信息,SQL执行时十分重要的额外信息,简单说几个常会出现的值:

    • Using filesort : 未利用到索引的默认排序,须要使用文件辅助进行排序,出现其说明SQL性能很差;
    • Using temporary:使用临时表保存中间结果,常见于 group by ,出现其说明SQL性能很差;
    • Using index: 说明能够直接在索引树上就能获得最终的值,避免了回表,出现其说明SQL性能很好;
    • Using index for group-by:表示使用了 松散索引扫描 ,出现其说明SQL性能很好;由于松散索引扫描只须要读取不多量的数据就能够完成group by操做,因此执行效率很是高;
    • select tables optimized away: 在没有group by子句的状况下,基于索引优化 MIN/MAX 聚合函数操做,没必要等到执行阶段在进行计算,查询执行计划生成的阶段便可完成优化,出现其说明SQL性能达到最优,每每配合 type访问类型的system 出现;

三、创建索引后再查看执行计划:

上面经过查看执行计划得知,由于没有建立相应的索引,因此走的是全表扫描,性能最差;而后对 createts 字段建立索引;再查看其执行计划:

经过查看建立索引后的执行计划发现,这次查询走的 索引全扫描 ,这次虽然从全表扫描优化到了索引全扫描,可是仍是须要经过扫描了大概 99974 行记录后才获得最终的结果集,性能并无提高太多;

而且发现 Extra 信息中仍是存在 Using temporary; Using filesort ,说明没有使用到 松散索引扫描或紧凑索引扫描

而后再次分析下SQL语句:

SELECT
	date_format(createts, '%Y') AS YEAR
FROM
	t_test_log
GROUP BY
	date_format(createts, '%Y')
ORDER BY
	createts DESC

发现SQL中对索引字段 createts 作了 date_format 函数运算,因此才致使没使用上松散索引扫描或紧凑索引扫描;而后须要重写下SQL 。

四、经过改写SQL进行优化:

改写后的SQL以下:

SELECT
	date_format(createts, '%Y') AS years
FROM
	(
		SELECT
			createts
		FROM
			t_test_log
		GROUP BY
			createts
	) t_test_log_1
GROUP BY
	date_format(createts, '%Y')
ORDER BY
	createts DESC

改写完SQL后从新执行,发现查询速度快了很是多,性能上有了质的飞跃;

而后又查看了下它的执行计划以下:

查看上面那个嵌套查询SQL语句的执行计划,子查询部分的经过扫描大概52行记录就能获得结果集,相比于一开始须要扫描 99974 行 记录才能获得结果集,这个性能快了太多了;而且子查询的 Extra 信息中出现了 Using index for group-by ,说明使用到了松散索引扫描,效率才提高了这么多;

外查询对子查询(52行记录)的结果集再次进行分组排序,此时采用的是全表(全结果集)的查询, 若是结果集很大的话,效率不会很高

因此,在使用此优化方案的SQL语句时,须要统计下子查询的结果集的大小,若是子查询结果集很大的话,就不建议使用此方案了,能够尝试使用下面的这种优化方案;

五、经过 改写SQL + 改写代码 进行优化:

上面优化方案,只需改写SQL便可,无需对代码进行修改;本优化方案既要改写SQL,还要进行代码的修改;

改写后的SQL以下: 这个SQL是查询出表中最小年份和最大年份

(
	SELECT
		date_format(createts, '%Y') AS years
	FROM
		t_test_log
	ORDER BY
		createts
	LIMIT 1
)
UNION ALL
	(
		SELECT
			date_format(createts, '%Y') AS years
		FROM
			t_test_log
		ORDER BY
			createts DESC
		LIMIT 1
	)

查看下上面这个SQL语句的执行计划:

上面这个SQL是利用索引的默认排序,直接获取排序后的第一条记录,只须要扫描一行记录(rows :1)就能获取到最终的结果集;因此此SQL的性能是很是好的 。

可是须要记住,这个SQL查询出的结果集不是最终须要的数据,须要 写代码 计算出最终的结果集:

  • 获得的最大最小年份这两个值 同样:说明表中的数据都是属于一个年份的
  • 获得的最大最小年份这两个值不同:
    • 两个值相减得一:说明年份是挨着的两个年份,能够直接将结果集返回;
    • 两个值相减大于一:说明最小年份和最大年份之间还存在年份,经过计算得出中间年份

可是注意,经过写代码计算出最终的年份,这种方式仍是存在一个问题的,那就是确实表中根本没有中间年份的数据,可是经过计算却得出了;

举例说明:假如经过SQL查询出了最小年份和最大年份是2018和2021,那么再经过代码计算出中间年份2019和2020,可是表中数据根本就不存在2019年份的数据,这是就会出现问题了;

因此这种方案也须要根据本身具体的业务场景和实际的数据状况等分析是否须要采用 。

扩展:

在经过 改写SQL + 改写代码 进行优化时,改写的SQL不止上面那一种,还有一种查询效率也比较高的改写SQL;

就是使用 min、max 聚合函数进行改写SQL,可是在使用聚合函数时,能够写出下面两种样式的SQL,到底哪一种改写SQL效率是比较高呢,留个悬念,你们能够自行去分析尝试下哟! 能够在评论区留下你的答案呀!

第一种改写SQL方式:

(
	SELECT
		min(date_format(createts, '%Y')) AS years
	FROM
		t_test_log
)
UNION ALL
  (
		SELECT
			max(date_format(createts, '%Y')) AS years
		FROM
			t_test_log
   )

第二种改写SQL方式:

(
	SELECT
		date_format(minyear, '%Y') AS years
	FROM
		(
			SELECT
				min(createts) AS minyear
			FROM
				t_test_log
		) t_test_log_1
)
UNION ALL
   (
		SELECT
			date_format(maxyear, '%Y') AS years
		FROM
			(
				SELECT
					max(createts) AS maxyear
				FROM
					t_test_log
			) t_test_log_2
   )

♡ 点赞 + 评论 + 转发 哟

若是本文对您有帮助的话,请挥动下您爱发财的小手点下赞呀,您的支持就是我不断创做的动力,谢谢啦!

您能够微信搜索【木子雷】公众号,大量Java学习干货文章,您能够来瞧一瞧哟!

相关文章
相关标签/搜索