MySQL實戰45講學習筆記:第三十七講

1、本節概況

今天是大年初二,在開始咱們今天的學習以前,我要先和你道一聲春節快樂!算法

在第 16和第 34篇文章中,我分別和你介紹了 sort buffer、內存臨時表和 join buffer。這三個數據結構都是用來存放語句執行過程當中的中間數據,以輔助 SQL 語句的執行的。其
中,咱們在排序的時候用到了 sort buffer,在使用 join 語句的時候用到了 join buffer。sql

而後,你可能會有這樣的疑問,MySQL 何時會使用內部臨時表呢?數組

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

2、union 執行流程

一、 union 語句 explain 結果

爲了便於量化分析,我用下面的表 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 結果。spa

圖 1 union 語句 explain 結果線程

能夠看到:3d

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

二、union 執行流程圖

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

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

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

3. 執行第二個子查詢:

  1. 拿到第一行 id=1000,試圖插入臨時表中。但因爲 1000 這個值已經存在於臨時表了,違反了惟一性約束,因此插入失敗,而後繼續執行;
  2. 取到第二行 id=999,插入臨時表成功。

4. 從臨時表中按行取出數據,返回結果,並刪除臨時表,結果中包含兩行數據分別是1000 和 999。

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

圖 2 union 執行流程

能夠看到,這裏的內存臨時表起到了暫存數據的做用,並且計算過程還用上了臨時表主鍵id 的惟一性約束,實現了 union 的語義。

三、union all 的 explain 結果

順便提一下,若是把上面這個語句中的 union 改爲 union all 的話,就沒有了「去重」的語義。這樣執行的時候,就依次執行子查詢,獲得的結果直接做爲結果集的一部分,發給
客戶端。所以也就不須要臨時表了。

圖 3 union all 的 explain 結果

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

3、group by 執行流程

一、group by 的 explain 結果

另一個常見的使用臨時表的例子是 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,表示須要排序。

二、group by 執行流程

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

1. 建立內存臨時表,表裏有兩個字段 m 和 c,主鍵是 m;
2. 掃描表 t1 的索引 a,依次取出葉子節點上的 id 值,計算 id%10 的結果,記爲 x;

若是臨時表中沒有主鍵爲 x 的行,就插入一個記錄 (x,1);
若是表中有主鍵爲 x 的行,就將 x 這一行的 c 值加 1;

3. 遍歷完成後,再根據字段 m 作排序,獲得結果集返回給客戶端。

這個流程的執行圖以下:

 

 圖 5 group by 執行流程

圖中最後一步,對內存臨時表的排序,在第 17 篇文章中已經有過介紹,我把圖貼過來,方便你回顧。

三、內存臨時表排序流程

圖 6 內存臨時表排序流程

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

四、group by 執行結果

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

圖 7 group by 執行結果


五、group + order by null 的結果(內存臨時表)

若是你的需求並不須要對結果進行排序,那你能夠在 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 字節)。

六、group + order by null 的結果(磁盤臨時表)

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

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

若是這個表 t1 的數據量很大,極可能這個查詢須要的磁盤臨時表就會佔用大量的磁盤空間。

4、group by 優化方法 -- 索引

一、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 的索引,就能夠知足這個輸入有序的條件

你必定想到了,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 結果

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

圖 11 group by 優化的 explain 結果

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

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

一、使用 SQL_BIG_RESULT 的執行流程

因此,若是能夠經過加索引來完成 group by 邏輯就再好不過了。可是,若是碰上不適合建立索引的場景,咱們仍是要老老實實作排序的。那麼,這時候的 group by 要怎麼優化呢?

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

那麼,咱們就會想了,MySQL 有沒有讓咱們直接走磁盤臨時表的方法呢?

答案是,有的。

在 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 中瞭解過了。

二、使用 SQL_BIG_RESULT 的 explain 結果

下面兩張圖分別是執行流程圖和執行 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 還須要用到另一個字段來存累積計數。

6、小結

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

1. 若是對 group by 語句的結果沒有排序要求,要在語句後面加 order by null;
2. 儘可能讓 group by 過程用上表的索引,確認方法是 explain 結果裏沒有 Usingtemporary 和 Using filesort;
3. 若是 group by 須要統計的數據量不大,儘可能只使用內存臨時表;也能夠經過適當調大tmp_table_size 參數,來避免用到磁盤臨時表;
4. 若是數據量實在太大,使用 SQL_BIG_RESULT 這個提示,來告訴優化器直接使用排序算法獲得 group by 的結果。

最後,我給你留下一個思考題吧。

文章中圖 8 和圖 9 都是 order by null,爲何圖 8 的返回結果裏面,0 是在結果集的最後一行,而圖 9 的結果裏面,0 是在結果集的第一行?

你能夠把你的分析寫在留言區裏,我會在下一篇文章和你討論這個問題。感謝你的收聽,也歡迎你把這篇文章分享給更多的朋友一塊兒閱讀。

7、上期問題時間

上期的問題是:爲何不能用 rename 修改臨時表的更名。

在實現上,執行 rename table 語句的時候,要求按照「庫名 / 表名.frm」的規則去磁盤找文件,可是臨時表在磁盤上的 frm 文件是放在 tmpdir 目錄下的,而且文件名的規則
是「#sql{進程 id}_{線程 id}_ 序列號.frm」,所以會報「找不到文件名」的錯誤。

評論區留言點贊板:

@poppy 同窗,經過執行語句的報錯現象推測了這個實現過程。

相關文章
相關標籤/搜索