一次group by 優化之旅

最近在優化公司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耗時翻n倍?

線上的SQL大概是這樣的linux

select sum(tea_reg),sum(stu_reg) from dt_school 
where time between 20160101 and 20160411 group by school_id\G;
複製代碼

執行結果: sql

image.png

多查一天數據庫

select sum(tea_reg),sum(stu_reg) from dt_school where time between 20160101 and 20160412 group by school_id\G;
複製代碼

執行結果 性能優化

image.png

一臉懵逼.jpg , 好吧。對比一下執行計劃 : bash

image.png
image.png

發現一旦查詢的時間範圍超過了這個節點,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;
複製代碼

image.png

image.png
可是對於真實的生產環境,我並不想使用force index 。因此,我決定嘗試刪掉這個索引,排除它的干擾。(固然線上生成環境,我沒有刪,由於還有其餘業務查詢須要用這個索引)

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;
複製代碼

image.png
image.png
發現刪掉key_school_id 這個索引後,MySQL一樣不會選擇index_time_school 這個索引。可是,咱們能夠看到,就算不走索引,全表掃描聚合,也只須要0.83s 遠快過使用key_school_id索引。

因此, 索引就必定能加快查詢嗎?不合適的索引還不如不建。


解決方案

咱們發現刪掉了,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;
複製代碼

image.png

這個時候我忽然有個想法,鑑於這塊業務都是對數據作求和聚合,其實咱們能夠經過,將數據作月度彙總的方法來解決:

  1. 建一張與dt_school 如出一轍的 dt_school_month
  2. 定時腳本每月定時將dt_school的數據分組聚合後插入dt_school_month
  3. 業務層進行拆分,若是對於一個時間範圍20180121-20180402 的查詢,切成20180121-20180131, 20180401-20180402(查dt_school表) ,20180201-20180331(查dt_school_month)表

拆分完的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索引 ,因此速度上去了。


SQL_BIG_RESULT

剛作完這個月表方案,技術交流羣裏的大佬夢康就發了篇博文 一次 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;
複製代碼

image.png
發現,用上SQL_BIG_RESULT後,這個執行時長也變快了。 還有個未解決的問題是線上腳本機用上SQL_BIG_RESULT比走index_time_school索引還快,線下我用虛擬機復現的話,就稍慢與走index_time_school索引。 我猜多是由於線上機用固態硬盤的緣故(若是你有調試思路,麻煩告訴我 thx)。 若是測試時發現用上SQL_BIG_RESULT仍是比較慢,你能夠執行一下語句。

show variables like '%sort_buffer_size%';
複製代碼

sort_buffer默認配置

SET GLOBAL sort_buffer_size = 1024*1024*2;
複製代碼

爲啥使用SQL_BIG_RESULT能加快查詢呢? 先看一下 執行計劃

image.png
再執行一下

help SQL_BIG_RESULT
複製代碼

image.png
image.png
僞裝成一個看得懂英語的人,翻譯一下,手冊大概是說

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, 先分組後排序。


從group by 原理提及

爲何使用了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排序,將結果集返回給客戶端。

image.png
image.png

其實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;
複製代碼

這個語句的執行流程是這樣的:

  1. 掃描表dt_school主鍵索引,依次取出節點school_id和tea_reg, stu_reg,和time 的值。
  2. 若是time的值不在獲取的時間範圍內,丟棄。不然插入sort buffer中,若是sort_buffer 不夠用,直接使用磁盤臨時文件輔助排序
  3. 對sort buffer 的數據對school_id 進行排序
  4. 掃描sort buffer 的數據,返回聚合結果

image.png

這也就是爲啥 查看SQL_BIG_RESULT的語句的執行計劃,Extra選項的值,沒有使用臨時表,可是須要using filesort 。 固然咱們加上這個school_id索引的話,這個filesort排序也不須要了。

丁奇老師在專欄裏的建議,是優先調高內存臨時表的大小(temp_table_size) , 可是沒有說明緣由。因而 我請教了公司的DBA同窗,他是這樣說的

表數據少的時候,使用提示符讓走文件排序很快,實際上一個表若是十幾G,呢,文件排序就很慢很慢 sort buffer是會話變量,線上設置的1M,通常不會給很大,由於若是同時有很鏈接都在排序就會佔不少內存。 數據庫是一個總體有限的資源須要均衡分配,不能由於某條語句調整配置。


以上就是對本身工做中關於group by 的總結。因爲做者見識有限,文中不免紕漏繁多。歡迎讀者交流指正。


參考閱讀 :

相關文章
相關標籤/搜索