最近在优化公司CRM报表系统时发现一个有趣的问题。对学校分组聚合统计后,一旦查询的范围超过必定时长,这个SQL的执行耗时就和原来差10倍以上。线上数据差很少几百万。下面给出一个脱敏后的表结构和存储过程用来模拟。模拟的截图均出自个人虚拟机(2核2G内存)。html
建表SQLnode
CREATE TABLE `dt_school` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增主键',
`tea_reg` int(11) NOT NULL DEFAULT '0' COMMENT '老师注册数',
`stu_reg` int(11) NOT NULL DEFAULT '0' COMMENT '学生注册数',
`school_id` int(11) NOT NULL COMMENT '学校id',
`time` int(11) NOT NULL DEFAULT '0' COMMENT '更新时间(具体到天)',
PRIMARY KEY (`id`),
KEY `key_school_id` (`school_id`),
KEY `index_time_school` (`time`,`school_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='数据统计-学校统计表'
复制代码
初始化数据mysql
delimiter //
CREATE PROCEDURE proc_init_dt_school()
BEGIN
DECLARE d INT;
DECLARE sid INT ;
SET d = 20160101;
WHILE d < 20190501 DO
SET sid = 1;
WHILE sid < 1501 DO
insert into dt_school(tea_reg,stu_reg,school_id,time)
value(floor(rand()*100),floor(rand()*100),sid,d);
SET sid = sid + 1;
END WHILE;
SET d = DATE_FORMAT(date_add(d,INTERVAL 1 day),'%Y%m%d');
END WHILE;
END //
delimiter ;
call proc_init_dt_school() ;
复制代码
线上的SQL大概是这样的linux
select sum(tea_reg),sum(stu_reg) from dt_school
where time between 20160101 and 20160411 group by school_id\G;
复制代码
执行结果: sql
多查一天数据库
select sum(tea_reg),sum(stu_reg) from dt_school where time between 20160101 and 20160412 group by school_id\G;
复制代码
执行结果 性能优化
一脸懵逼.jpg , 好吧。对比一下执行计划 : bash
发现一旦查询的时间范围超过了这个节点,MySQL 就会选择school_id这个索引,致使查询缓慢。既然是这样我使用force index 强制使用index_time_schoolapp
select sum(tea_reg),sum(stu_reg) from dt_school force index(index_time_school)
where time between 20160101 and 20160412 group by school_id\G;
复制代码
drop index key_school_id on dt_school
复制代码
再次执行这条查询性能
select sum(tea_reg),sum(stu_reg) from dt_school
where time between 20160101 and 20160412 group by school_id\G;
复制代码
因此, 索引就必定能加快查询吗?不合适的索引还不如不建。
咱们发现删掉了,key_school_id索引后。MySQL仍然不会选择index_time_school索引,虽然咱们可使用force index() 显式指定索引,可是这并不能从根本上解决问题。由于MySQL不使用这个索引,说明至少从MySQL优化器来看,这个索引使用与否已经没有太大的优化意义了。咱们发现一旦这个咱们把这个查询范围扩大,耗时就愈加明显
select sum(tea_reg),sum(stu_reg) from dt_school
force index(index_time_school) group by school_id\G;
复制代码
这个时候我忽然有个想法,鉴于这块业务都是对数据作求和聚合,其实咱们能够经过,将数据作月度汇总的方法来解决:
拆分完的SQL查询变为:
SELECT
sum(tea_reg) as tea_reg,
sum(stu_reg) as stu_reg,
school_id
FROM (
(SELECT
sum(tea_reg) as tea_reg,
sum(stu_reg) as stu_reg,
school_id
FROM dt_school
WHERE time BETWEEN 20180121 AND 20180131
GROUP BY school_id)
UNION ALL (SELECT
sum(tea_reg) as tea_reg,
sum(stu_reg) as stu_reg,
school_id
FROM dt_school_month
WHERE time BETWEEN 20180201 AND 20180331
GROUP BY school_id)
UNION ALL (SELECT
sum(tea_reg) as tea_reg,
sum(stu_reg) as stu_reg,
school_id
FROM dt_school
WHERE time BETWEEN 20180401 AND 20180402
GROUP BY school_id))
GROUP BY school_id;
复制代码
SQL变复杂了,可是 由于dt_school_month 中只有两行汇总数据time=20180228 and 20180331 ,而且dt_school 和 dt_school_month的查询都能走index_time_school索引 ,因此速度上去了。
刚作完这个月表方案,技术交流群里的大佬梦康就发了篇博文 一次 group by + order by 性能优化分析 也在讲遇到的类似的案例,其中提到一个关键字 SQL_BIG_RESULT 。 我两眼放光,充分发挥一个小白学徒工应有的精神。
看到个锤子,就想拿出来钉几下
select SQL_BIG_RESULT sum(tea_reg),sum(stu_reg) from dt_school
where time between 20190101 and 20190412 group by school_id\G;
复制代码
show variables like '%sort_buffer_size%';
复制代码
SET GLOBAL sort_buffer_size = 1024*1024*2;
复制代码
为啥使用SQL_BIG_RESULT能加快查询呢? 先看一下 执行计划
help SQL_BIG_RESULT
复制代码
SQL_BIG_RESULT或者 SQL_SMALL_RESULT能够与GROUP BY或DISTINCT一块儿使用 告诉优化器结果集分别有不少行或不多行。使用SQL_BIG_RESULT,MySQL在建立时直接使用基于磁盘的临时表,而且优先对带有GROUP BY元素键的临时表进行排序。使用 SQL_SMALL_RESULT,MySQL使用内存中的临时表来存储生成的表而不是使用排序。
这段话也就是,网上有些博文总结为使用SQL_BIG_RESULT先排序,后分组。不使用SQL_BIG_RESULT, 先分组后排序。
为何使用了SQL_BIG_RESULT能加快查询,这还得从group by 的原理提及 。这里我要借用一下丁奇老师专栏的图 ,顺带安利一下他的专栏MySQL45讲
group by 的本质其实也是排序
对于
select sum(tea_reg) as t ,sum(stu_reg) as s from dt_school
where time between 20190101 and 20190412 group by school_id\G;
复制代码
这个语句的执行流程是这样的: 1 . 建立内存临时表,表里有三个字段t , s , school_id 2. 扫描表dt_school主键索引,依次取出节点school_id和tea_reg, stu_reg,和time 的值。 3. 若是time的值不在获取的时间范围内,丢弃。判断内存表中是否已经有school_id值的行,若是没有插入(school_id,tea_reg,stu_reg), 若是有在对应的行加上tea_reg 和stu_reg的值 4. 若是依次往内存表插数据的时候,发现内存表已满。新起一个innodb引擎的磁盘临时表,将数据挪到磁盘临时表中。 5. 将对临时表的school_id排序,将结果集返回给客户端。
其实5这个步骤也不是必须的 。当使用索引school_id来遍历时,插入临时表的数据就默认是有序的,这就是为什么数据量一大优化器就要选择school_id索引,由于它老是认为排序是耗时的,而使用school_id是不须要排序的。 因此对于group by a 若是走的不是a 索引,在mysql5.6及如下的操做都是默认对分组后进行排序的,若是你不须要,能够尝试使用order by null 来加快这个查询。
上面这个流程,比较傻的就是第4步了,当内存临时表不够用时,咱们再把内存临时表的数据挪到磁盘临时表。 而使用SQL_BIG_RESULT的时候,优化器就会直接使用磁盘临时表
对于
select SQL_BIG_RESULT sum(tea_reg) as t ,sum(stu_reg) as s from dt_school
where time between 20190101 and 20190412 group by school_id\G;
复制代码
这个语句的执行流程是这样的:
这也就是为啥 查看SQL_BIG_RESULT的语句的执行计划,Extra选项的值,没有使用临时表,可是须要using filesort 。 固然咱们加上这个school_id索引的话,这个filesort排序也不须要了。
丁奇老师在专栏里的建议,是优先调高内存临时表的大小(temp_table_size) , 可是没有说明缘由。因而 我请教了公司的DBA同窗,他是这样说的
表数据少的时候,使用提示符让走文件排序很快,实际上一个表若是十几G,呢,文件排序就很慢很慢 sort buffer是会话变量,线上设置的1M,通常不会给很大,由于若是同时有很链接都在排序就会占不少内存。 数据库是一个总体有限的资源须要均衡分配,不能由于某条语句调整配置。
以上就是对本身工做中关于group by 的总结。因为做者见识有限,文中不免纰漏繁多。欢迎读者交流指正。
SELECT Syntax -- MySQL 5.6 Reference Manual dev.mysql.com/doc/refman/… dev.mysql.com/doc/refman/…
MySQL执行计划extra中的using index 和 using where using index 的区别 -- Linux 公社
何时会使用内部临时表 -- 丁奇
MySQL的优化 -- 老叶茶馆