MySQL45講讀書筆記 37講什麼時候會使用內部臨時表

一 序

  本文屬於極客時間MySQL45講讀書筆記系列。本篇接着上篇老師接着介紹MySQL內部臨時表的使用。

今天這篇文章,我就先給你舉兩個需要用到內部臨時表的例子,來看看內部臨時表是怎麼工作的。然後,我們再來分析,什麼情況下會使用內部臨時表。

union 執行流程

爲了便於量化分析,我用下面的表t1來舉例。

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();

然後,我們執行下面這條語句:

(select 1000 as f) union (select id from t1 order by id desc limit 2);

這條語句用到了union,它的語義是,取這兩個子查詢結果的並集。並集的意思就是這兩個集合加起來,重複的行只保留一行。

下圖是這個語句的explain結果。

圖1 union語句explain 結果

可以看到:

  • 第二行的key=PRIMARY,說明第二個子句用到了索引id。
  • 第三行的Extra字段,表示在對子查詢的結果集做union的時候,使用了臨時表(Using temporary)。

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

  1. 創建一個內存臨時表,這個臨時表只有一個整型字段f,並且f是主鍵字段。

  2. 執行第一個子查詢,得到1000這個值,並存入臨時表中。

  3. 執行第二個子查詢:

    • 拿到第一行id=1000,試圖插入臨時表中。但由於1000這個值已經存在於臨時表了,違反了唯一性約束,所以插入失敗,然後繼續執行;
    • 取到第二行id=999,插入臨時表成功。
  4. 從臨時表中按行取出數據,返回結果,並刪除臨時表,結果中包含兩行數據分別是1000和999。

這個過程的流程圖如下所示:

圖 2 union 執行流程

可以看到,這裏的內存臨時表起到了暫存數據的作用,而且計算過程還用上了臨時表主鍵id的唯一性約束,實現了union的語義。

順便提一下,如果把上面這個語句中的union改成union all的話,就沒有了“去重”的語義。這樣執行的時候,就依次執行子查詢,得到的結果直接作爲結果集的一部分,發給客戶端。因此也就不需要臨時表了。

圖3 union all的explain結果

可以看到,第二行的Extra字段顯示的是Using index,表示只使用了覆蓋索引,沒有用臨時表了。

MySQL官網介紹使用union而沒有

These conditions qualify a UNION for evaluation without a temporary table:

  • The union is UNION ALL, not UNION or UNION DISTINCT.

  • There is no global ORDER BY clause.

  • The union is not the top-level query block of an {INSERT | REPLACE} ... SELECT ... statement.

group by 執行流程

另外一個常見的使用臨時表的例子是group by,我們來看一下這個語句:

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

這個語句的邏輯是把表t1裏的數據,按照 id%10 進行分組統計,並按照m的結果排序後輸出。它的explain結果如下:

圖4 group by 的explain結果

在Extra字段裏面,我們可以看到三個信息:

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

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

  1. 創建內存臨時表,表裏有兩個字段m和c,主鍵是m;

  2. 掃描表t1的索引a,依次取出葉子節點上的id值,計算id%10的結果,記爲x;

    • 如果臨時表中沒有主鍵爲x的行,就插入一個記錄(x,1);
    • 如果表中有主鍵爲x的行,就將x這一行的c值加1;
  3. 遍歷完成後,再根據字段m做排序,得到結果集返回給客戶端。

這個流程的執行圖如下:

圖5 group by執行流程

圖中最後一步,對內存臨時表的排序。

圖6 內存臨時表排序流程

其中,臨時表的排序過程就是圖6中虛線框內的過程。

接下來,我們再看一下這條語句的執行結果:

接下來,我們再看一下這條語句的執行結果:

圖 7 group by執行結果

如果你的需求並不需要對結果進行排序,那你可以在SQL語句末尾增加order by null,也就是改成:

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

這樣就跳過了最後排序的階段,直接從臨時表中取數據返回。返回的結果如圖8所示。

 

圖8 group + order by null 的結果(內存臨時表)

由於表t1中的id值是從1開始的,因此返回的結果集中第一行是id=1;掃描到id=10的時候才插入m=0這一行,因此結果集裏最後一行纔是m=0。

這個例子裏由於臨時表只有10行,內存可以放得下,因此全程只使用了內存臨時表。但是,內存臨時表的大小是有限制的,參數tmp_table_size就是控制這個內存大小的,默認是16M。

如果我執行下面這個語句序列:

set tmp_table_size=1024;
select id%100 as m, count(*) as c from t1 group by m order by null limit 10;

把內存臨時表的大小限制爲最大1024字節,並把語句改成id % 100,這樣返回結果裏有100行數據。但是,這時的內存臨時表大小不夠存下這100行數據,也就是說,執行過程中會發現內存臨時表大小到達了上限(1024字節)。

那麼,這時候就會把內存臨時錶轉成磁盤臨時表,磁盤臨時表默認使用的引擎是InnoDB。 這時,返回的結果如圖9所示。

 

圖9 group + order by null 的結果(磁盤臨時表)

如果這個表t1的數據量很大,很可能這個查詢需要的磁盤臨時表就會佔用大量的磁盤空間。

group by 優化方法 --索引

可以看到,不論是使用內存臨時表還是磁盤臨時表,group by邏輯都需要構造一個帶唯一索引的表,執行代價都是比較高的。如果表的數據量比較大,上面這個group by語句執行起來就會很慢,我們有什麼優化的方法呢?

要解決group by語句的優化問題,你可以先想一下這個問題:執行group by語句爲什麼需要臨時表?

group by的語義邏輯,是統計不同的值出現的個數。但是,由於每一行的id%100的結果是無序的,所以我們就需要有一個臨時表,來記錄並統計結果。

那麼,如果掃描過程中可以保證出現的數據是有序的,是不是就簡單了呢?

假設,現在有一個類似圖10的這麼一個數據結構,我們來看看group by可以怎麼做。

圖10 group by算法優化-有序輸入

可以看到,如果可以確保輸入的數據是有序的,那麼計算group by的時候,就只需要從左到右,順序掃描,依次累加。也就是下面這個過程:

  • 當碰到第一個1的時候,已經知道累積了X個0,結果集裏的第一行就是(0,X);
  • 當碰到第一個2的時候,已經知道累積了Y個1,結果集裏的第二行就是(1,Y);

按照這個邏輯執行的話,掃描到整個輸入的數據結束,就可以拿到group by的結果,不需要臨時表,也不需要再額外排序。

你一定想到了,InnoDB的索引,就可以滿足這個輸入有序的條件。

在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);

這樣,索引z上的數據就是類似圖10這樣有序的了。上面的group by語句就可以改成:

select z, count(*) as c from t1 group by z;

優化後的group by語句的explain結果,如下圖所示:

圖11 group by 優化的explain結果

從Extra字段可以看到,這個語句的執行不再需要臨時表,也不需要排序了。

group by優化方法 --直接排序

所以,如果可以通過加索引來完成group by邏輯就再好不過了。但是,如果碰上不適合創建索引的場景,我們還是要老老實實做排序的。那麼,這時候的group by要怎麼優化呢?

如果我們明明知道,一個group by語句中需要放到臨時表上的數據量特別大,卻還是要按照“先放到內存臨時表,插入一部分數據後,發現內存臨時表不夠用了再轉成磁盤臨時表”,看上去就有點兒傻。

在group by語句中加入SQL_BIG_RESULT這個提示(hint),就可以告訴優化器:這個語句涉及的數據量很大,請直接用磁盤臨時表。MySQL的優化器一看,磁盤臨時表是B+樹存儲,存儲效率不如數組來得高。所以,既然你告訴我數據量很大,那從磁盤空間考慮,還是直接用數組來存吧。

因此,下面這個語句

select SQL_BIG_RESULT id%100 as m, count(*) as c from t1 group by m;

的執行流程就是這樣的:

  1. 初始化sort_buffer,確定放入一個整型字段,記爲m;
  2. 掃描表t1的索引a,依次取出裏面的id值, 將 id%100的值存入sort_buffer中;
  3. 掃描完成後,對sort_buffer的字段m做排序(如果sort_buffer內存不夠用,就會利用磁盤臨時文件輔助排序);
  4. 排序完成後,就得到了一個有序數組。

根據有序數組,得到數組裏面的不同值,以及每個值的出現次數。這一步的邏輯,你已經從前面的圖10中瞭解過了。

下面兩張圖分別是執行流程圖和執行explain命令得到的結果。

圖12 使用 SQL_BIG_RESULT的執行流程圖

圖13 使用 SQL_BIG_RESULT的explain 結果

從Extra字段可以看到,這個語句的執行沒有再使用臨時表,而是直接用了排序算法。

基於上面的union、union all和group by語句的執行過程的分析,我們來回答文章開頭的問題:MySQL什麼時候會使用內部臨時表?

  1. 如果語句執行過程可以一邊讀數據,一邊直接得到結果,是不需要額外內存的,否則就需要額外的內存,來保存中間結果;

  2. join_buffer是無序數組,sort_buffer是有序數組,臨時表是二維表結構;

  3. 如果執行邏輯需要用到二維表特性,就會優先考慮使用臨時表。比如我們的例子中,union需要用到唯一索引約束, group by還需要用到另外一個字段來存累積計數。

小結

通過今天這篇文章,我重點和你講了group by的幾種實現算法,從中可以總結一些使用的指導原則:

  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的結果。

 

相關參數

tmpdir: 這個參數是臨時目錄的配置,在5.6以及之前的版本,臨時表/文件默認都會放在這裏。這個參數可以配置多個目錄,這樣就可以輪流在不同的目錄上創建臨時表/文件,如果不同的目錄分別指向不同的磁盤,就可以達到分流的目的。

innodb_tmpdir: 這個參數只要是被DDL中的排序臨時文件使用的。其佔用的空間會很大,建議單獨配置。這個參數可以動態設置,也是一個Session變量。

slave_load_tmpdir: 這個參數主要是給BinLog複製中Load Data時,配置備庫存放臨時文件位置時使用。因爲數據庫Crash後還需要依賴Load數據的文件,建議不要配置重啓後會刪除數據的目錄。

internal_tmp_disk_storage_engine: 當隱式臨時表被轉換成磁盤臨時表時,使用哪種引擎,默認只有MyISAM和InnoDB。5.7及以後的版本才支持。8.0.16版本後取消的這個參數。

internal_tmp_mem_storage_engine: 隱式臨時表在內存時用的存儲引擎,可以選擇Memory或者Temptable引擎。建議選擇新的Temptable引擎。

default_tmp_storage_engine: 默認的顯式臨時表的引擎,即用戶通過SQL語句創建的臨時表的引擎。

tmp_table_size: min(tmp_table_size,max_heap_table_size)是隱式臨時表的內存大小,超過這個值會轉換成磁盤臨時表。

max_heap_table_size: 用戶創建的Memory內存表的內存限制大小。

big_tables: 內存臨時錶轉換成磁盤臨時表需要有個轉化操作,需要在不同引擎格式中轉換,這個是需要消耗的。如果我們能提前知道執行某個SQL需要用到磁盤臨時表,即內存肯定不夠用,可以設置這個參數,這樣優化器就跳過使用內存臨時表,直接使用磁盤臨時表,減少開銷。

temptable_max_ram: 這個參數是8.0後纔有的,主要是給Temptable引擎指定內存大小,超過這個後,要麼就轉換成磁盤臨時表,要麼就使用自帶的overflow機制。

temptable_use_mmap: 是否使用Temptable的overflow機制。

參考:

https://dev.mysql.com/doc/refman/5.7/en/internal-temporary-tables.html

http://mysql.taobao.org/monthly/2019/04/01/

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章