最近在優化公司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的優化 -- 老葉茶館