MySQL -- 內部臨時表
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); mysql> EXPLAIN (SELECT 1000 AS f) UNION (SELECT id FROM t1 ORDER BY id DESC LIMIT 2); +----+--------------+------------+------------+-------+---------------+---------+---------+------+------+----------+----------------------------------+ | id | select_type| table| partitions | type| possible_keys | key| key_len | ref| rows | filtered | Extra| +----+--------------+------------+------------+-------+---------------+---------+---------+------+------+----------+----------------------------------+ |1 | PRIMARY| NULL| NULL| NULL| NULL| NULL| NULL| NULL | NULL |NULL | No tables used| |2 | UNION| t1| NULL| index | NULL| PRIMARY | 4| NULL |2 |100.00 | Backward index scan; Using index | | NULL | UNION RESULT | <union1,2> | NULL| ALL| NULL| NULL| NULL| NULL | NULL |NULL | Using temporary| +----+--------------+------------+------------+-------+---------------+---------+---------+------+------+----------+----------------------------------+
- 第二行的
Key=PRIMARY
,說明第二個子查詢用到了索引id - 第三行的Extra欄位為
Using temporary
- 表示在對子查詢的結果做
UNION RESULT
的時候,使用了 臨時表
- 表示在對子查詢的結果做
UNION RESULT
- 建立一個 記憶體臨時表 ,這個記憶體臨時表只有一個整型欄位f,並且f為 主鍵
- 執行第一個子查詢,得到1000,並存入記憶體臨時表中
- 執行第二個子查詢
- 拿到第一行id=1000,試圖插入到記憶體臨時表,但由於1000這個值已經存在於記憶體臨時表
- 違反唯一性約束 ,插入失敗,繼續執行
- 拿到第二行id=999,插入記憶體臨時表成功
- 拿到第一行id=1000,試圖插入到記憶體臨時表,但由於1000這個值已經存在於記憶體臨時表
- 從記憶體臨時表中按行取出資料,返回結果,並 刪除記憶體臨時表 ,結果中包含id=1000和id=999兩行
- 記憶體臨時表起到了 暫存資料 的作用,還用到了記憶體臨時表主鍵id的 唯一性約束 ,實現UNION的語義
UNION ALL
UNION ALL
沒有 去重 的語義,一次執行子查詢,得到的結果直接發給客戶端, 不需要記憶體臨時表
mysql> EXPLAIN (SELECT 1000 AS f) UNION ALL (SELECT id FROM t1 ORDER BY id DESC LIMIT 2); +----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+----------------------------------+ | id | select_type | table | partitions | type| possible_keys | key| key_len | ref| rows | filtered | Extra| +----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+----------------------------------+ |1 | PRIMARY| NULL| NULL| NULL| NULL| NULL| NULL| NULL | NULL |NULL | No tables used| |2 | UNION| t1| NULL| index | NULL| PRIMARY | 4| NULL |2 |100.00 | Backward index scan; Using index | +----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+----------------------------------+
GROUP BY
記憶體充足
-- 16777216 Bytes = 16 MB mysql> SHOW VARIABLES like '%tmp_table_size%'; +----------------+----------+ | Variable_name| Value| +----------------+----------+ | tmp_table_size | 16777216 | +----------------+----------+
執行語句
-- MySQL 5.6上執行 mysql> EXPLAIN SELECT id%10 AS m, COUNT(*) AS c FROM t1 GROUP BY m; +----+-------------+-------+-------+---------------+------+---------+------+------+----------------------------------------------+ | id | select_type | table | type| possible_keys | key| key_len | ref| rows | Extra| +----+-------------+-------+-------+---------------+------+---------+------+------+----------------------------------------------+ |1 | SIMPLE| t1| index | PRIMARY,a| a| 5| NULL | 1000 | Using index; Using temporary; Using filesort | +----+-------------+-------+-------+---------------+------+---------+------+------+----------------------------------------------+ mysql> SELECT id%10 AS m, COUNT(*) AS c FROM t1 GROUP BY m; +------+-----+ | m| c| +------+-----+ |0 | 100 | |1 | 100 | |2 | 100 | |3 | 100 | |4 | 100 | |5 | 100 | |6 | 100 | |7 | 100 | |8 | 100 | |9 | 100 | +------+-----+
-
Using index
:表示使用了 覆蓋索引 ,選擇了索引a,不需要回表 -
Using temporary
:表示使用了 臨時表 -
Using filesort
:表示需要 排序
執行過程
- 建立 記憶體臨時表 ,表裡有兩個欄位m和c,m為主鍵
- 掃描t1的索引a,依次取出葉子節點上的id值,計算id%10,記為x
(x,1)
- 遍歷完成後,再根據欄位m做排序,得到結果集返回給客戶端
排序過程
ORDER BY NULL
-- 跳過最後的排序階段,直接從臨時表中取回資料 mysql> EXPLAIN SELECT id%10 AS m, COUNT(*) AS c FROM t1 GROUP BY m ORDER BY NULL; +----+-------------+-------+-------+---------------+------+---------+------+------+------------------------------+ | id | select_type | table | type| possible_keys | key| key_len | ref| rows | Extra| +----+-------------+-------+-------+---------------+------+---------+------+------+------------------------------+ |1 | SIMPLE| t1| index | PRIMARY,a| a| 5| NULL | 1000 | Using index; Using temporary | +----+-------------+-------+-------+---------------+------+---------+------+------+------------------------------+ -- t1中的資料是從1開始的 mysql> SELECT id%10 AS m, COUNT(*) AS c FROM t1 GROUP BY m ORDER BY NULL; +------+-----+ | m| c| +------+-----+ |1 | 100 | |2 | 100 | |3 | 100 | |4 | 100 | |5 | 100 | |6 | 100 | |7 | 100 | |8 | 100 | |9 | 100 | |0 | 100 | +------+-----+
記憶體不足
SET tmp_table_size=1024;
執行語句
-- 記憶體臨時表的上限為1024 Bytes,但記憶體臨時表不能完全放下100行資料,記憶體臨時表會轉成磁碟臨時表,預設採用InnoDB引擎 -- 如果t1很大,這個查詢需要的磁碟臨時表就會佔用大量的磁碟空間 mysql> SELECT id%100 AS m, count(*) AS c FROM t1 GROUP BY m ORDER BY NULL LIMIT 10; +------+----+ | m| c| +------+----+ |1 | 10 | |2 | 10 | |3 | 10 | |4 | 10 | |5 | 10 | |6 | 10 | |7 | 10 | |8 | 10 | |9 | 10 | |10 | 10 | +------+----+
優化方案
優化索引
- 不論使用記憶體臨時表還是磁碟臨時表,
GROUP BY
都需要構造一個帶 唯一索引 的表, 執行代價較高 - 需要臨時表的原因:每一行的
id%100
是無序的,因此需要臨時表,來記錄並統計結果 - 如果可以確保輸入的資料是有序的,那麼計算
GROUP BY
時,只需要 從左到右順序掃描 ,依次累加即可- 當碰到第一個1的時候,已經累積了X個0,結果集裡的第一行為
(0,X)
- 當碰到第一個2的時候,已經累積了Y個1,結果集裡的第一行為
(1,Y)
- 整個過程不需要 臨時表 ,也不需要 排序
- 當碰到第一個1的時候,已經累積了X個0,結果集裡的第一行為
-- MySQL 5.7上執行 ALTER TABLE t1 ADD COLUMN z INT GENERATED ALWAYS AS(id % 100), ADD INDEX(z); -- 使用了覆蓋索引,不需要臨時表,也不需要排序 mysql> EXPLAIN SELECT z, COUNT(*) AS c FROM t1 GROUP BY z; +----+-------------+-------+------------+-------+---------------+------+---------+------+------+----------+-------------+ | id | select_type | table | partitions | type| possible_keys | key| key_len | ref| rows | filtered | Extra| +----+-------------+-------+------------+-------+---------------+------+---------+------+------+----------+-------------+ |1 | SIMPLE| t1| NULL| index | z| z| 5| NULL | 1000 |100.00 | Using index | +----+-------------+-------+------------+-------+---------------+------+---------+------+------+----------+-------------+
直接排序
- 一個
GROUP BY
語句需要放到臨時表的資料量 特別大 ,還是按照先放在記憶體臨時表,再退化成磁碟臨時表 - 可以直接用磁碟臨時表的形式 ,在
GROUP BY
語句中SQL_BIG_RESULT
(告訴優化器涉及的資料量很大) - 磁碟臨時表原本採用B+樹儲存, 儲存效率還不如陣列 ,優化器看到
SQL_BIG_RESULT
, 會直接用陣列儲存- 即放棄使用臨時表, 直接進入排序階段
執行過程
-- 沒有再使用臨時表,而是直接使用了排序演算法 mysql> EXPLAIN SELECT SQL_BIG_RESULT id%100 AS m, COUNT(*) AS c FROM t1 GROUP BY m; +----+-------------+-------+-------+---------------+------+---------+------+------+-----------------------------+ | id | select_type | table | type| possible_keys | key| key_len | ref| rows | Extra| +----+-------------+-------+-------+---------------+------+---------+------+------+-----------------------------+ |1 | SIMPLE| t1| index | PRIMARY,a| a| 5| NULL | 1000 | Using index; Using filesort | +----+-------------+-------+-------+---------------+------+---------+------+------+-----------------------------+
- 初始化
sort_buffer
,確定放入一個整型欄位,記為m - 掃描t1的索引a,依次取出裡面的id值,將id%100的值放入
sort_buffer
- 掃描完成後,對
sort_buffer
的欄位m做排序(sort_buffer記憶體不夠時,會利用 磁碟臨時檔案 輔助排序) - 排序完成後,得到一個有序陣列,遍歷有序陣列,得到每個值出現的次數(類似上面優化索引的方式)
小結
- 用到內部臨時表的場景
- 如果語句執行過程中可以一邊讀資料,一邊得到結果,是不需要額外記憶體的
- 否則需要額外記憶體來儲存中間結果
-
join_buffer
是 無序陣列 ,sort_buffer
是 有序陣列 ,臨時表是 二維表結構 - 如果執行邏輯需要用到 二維表特性 ,就會優先考慮使用 臨時表
- 如果對
GROUP BY
語句的結果沒有明確的排序要求,加上ORDER BY NULL
(MySQL 5.6) - 儘量讓
GROUP BY
過程 用上索引 , 確認EXPLAIN結果沒有Using temporary
和Using filesort
- 如果
GROUP BY
需要統計的資料量不大,儘量使用 記憶體臨時表 (可以適當調大tmp_table_size
) - 如果資料量實在 太大 ,使用
SQL_BIG_RESULT
來告訴優化器 直接使用排序演算法 (跳過臨時表)
參考資料
《MySQL實戰45講》
轉載請註明出處:http://zhongmingmao.me/2019/03/13/mysql-internal-temporary-table/
訪問原文「MySQL -- 內部臨時表」獲取最佳閱讀體驗並參與討論