Group by 優化

萌新J發表於2021-01-29

一個標準的 Group by 語句包含排序、分組、聚合函式,比如 select a,count(*) from t group by a ;  這個語句預設使用 a 進行排序。如果 a 列沒有索引,那麼就會建立臨時表來統計 a和 count(*),然後再通過 sort_buffer 按 a 進行排序。

標準的執行流程

結構:

create table t1(id int primary key, a int, b int, index(a));
delimiter ;;
create procedure idata()
begin
  declare i int;

  set i=1;
  while(i<=1000)do
    insert into t1 values(i, i, i);
    set i=i+1;
  end while;
end;;
delimiter ;
call idata();

函式就是向 t1 中插入1000條語句,從(1,1,1) 到(1000,1000,1000)。

執行   select id%10 as m, count(*) as c from t1 group by m;

解析

Using index,表示這個語句使用了覆蓋索引,選擇了索引 a,不需要回表;
Using temporary,表示使用了臨時表;
Using filesort,表示需要排序。

過程

1、建立記憶體臨時表,表裡有兩個欄位 m 和 c,主鍵是 m;
2、掃描表 t1 的索引 a,依次取出葉子節點上的 id 值,計算 id%10 的結果,記為 x;
  1)如果臨時表中沒有主鍵為 x 的行,就插入一個記錄 (x,1);
  2)如果表中有主鍵為 x 的行,就將 x 這一行的 c 值加 1;

第2 步如果發現記憶體臨時表儲存的總欄位長度到達引數 tmp_table_size 設定的大小,那麼就會將記憶體臨時表升級為磁碟臨時表,然後重新開始遍歷計算。
3、遍歷完成後,再根據欄位 m 做排序,得到結果集返回給客戶端。

最後的排序就是下圖虛線框中的操作,如果 sort_buffer 設定的大小不夠大,那麼就會使用臨時表來輔助排序。

 

優化

未優化(也就是分組列沒有索引)的 group by 的總過程可以概括為:因為資料是無序的,所以需要建立臨時表,然後一個一個判斷屬於哪個分組,最後再根據分組列進行排序。所以,優化可以有兩個思路:

去掉排序

在明確返回的資料不需要排序的情況下,可以禁止排序,也就是將上面的語句改成 select a,count(*) from t group by a order by null。

 

順序排列

如果記錄都按照排序欄位排序,那麼資料就變成了下面的結構:

這樣在實際獲取要返回的欄位或計算聚合函式時,只需要按順序依次訪問,等到列值變成下一個就知道當前組訪問結束,將之前統計的資料直接返回。這樣就避免了建立臨時表,同時排序也不需要使用 sort_buffer 進行額外排序。這樣就極大地提高了執行的效率。

實現

1、如果分組欄位適合建立索引就直接為分組欄位建立索引。

MySQL 5.7 版本支援了 generated column 機制,用來實現列資料的關聯更新。你可以用下面的方法建立一個列 z,然後在 z 列上建立一個索引(如果是 MySQL 5.6 及之前的版本,你也可以建立普通列和索引,來解決這個問題)

alter table t1 add column z int generated always as(id % 100), add index(z);

然後解析:

這時沒有用到臨時表和額外排序,所以效能提升。

 

2、如果分組欄位不適合(使用率很低),那麼可以使用 SQL_BIG_RESULT 來嘗試優化。

在 group by 語句中加入 SQL_BIG_RESULT 這個提示(hint),就可以告訴優化器:這個語句涉及的資料量很大,請直接用磁碟臨時表。MySQL 的優化器一看,磁碟臨時表是 B+ 樹儲存,儲存效率不如陣列來得高。所以,既然使用SQL_BIG_RESULT來說明資料量很大,那從磁碟空間考慮,還是直接用陣列來存吧。所以在使用 SQL_BIG_RESULT 後優化器會使用陣列結構的磁碟臨時表。

但是如果在未達到磁碟臨時表的使用條件是不會使用磁碟臨時表的,也就是在 sort_buffer 空間能夠儲存要返回和排序的總欄位長度時,就使用陣列結構的 sort_buffer ,如果總欄位超過 sort_buffer 大小,那麼就再加上陣列結構的磁碟臨時表來幫助排序。

那麼在 sort_buffer 空間足夠的情況下, sort_buffer 內部就會對資料進行排序,這樣也就起到了索引的作用,

還是以上面的例子來看,使用 SQL_BIG_RESULT

alter table t1 add column z int generated always as(id % 100), add index(z);

具體過程如下:

1、初始化 sort_buffer,確定放入一個整型欄位,記為 m;
2、掃描表 t1 的索引 a,依次取出裡面的 id 值, 將 id%10 的值存入 sort_buffer 中;
3、掃描完成後,對 sort_buffer 的欄位 m 做排序(如果 sort_buffer 記憶體不夠用,就會利用磁碟臨時檔案輔助排序);
4、排序完成後,就得到了一個有序陣列。

解析:

可以看到此時就沒有使用臨時表了,而是直接使用 sort_buffer 進行排序,這樣就省去了使用臨時錶帶來的效能消耗。

 

總結

1、如果對 group by 語句的結果沒有排序要求,要在語句後面加 order by null;那麼一般情況就不需要使用臨時表了(上面兩個優化都是在要求排序的前提下提出的優化方式)
2、儘量讓 group by 過程用上表的索引,確認方法是 explain 結果裡沒有 Using temporary 和 Using filesort;
3、如果 group by 需要統計的資料量不大,儘量只使用記憶體臨時表;也可以通過適當調大 tmp_table_size 引數,來避免用到磁碟臨時表;
4、如果資料量實在太大,使用 SQL_BIG_RESULT 這個提示,來告訴優化器直接使用排序演算法得到 group by 的結果。

相關文章