在使用 explain 解析一個 sql 時,有時咱們會發如今 extra 列上顯示 using temporary ,這表示這條語句用到了臨時表,那麼臨時表到底是什麼?它又會對 sql 的性能產生什麼影響?又會在哪些場景中出現?本文根據 <<MySQL 實戰 45 講>> 學習整理。html
其實臨時表在以前的博客就已經出現過了,在 MySQL 中的排序 一文中就說到若是 order by 的列上沒有索引,或者說沒有用到索引,那麼就須要進行額外排序(using filesort),而額外排序優先在一塊 sort_buffer 空間中進行,若是這塊空間大小小於要加載的字段總長度,那麼就會用到臨時文件輔助排序,這個臨時文件就是臨時表。臨時表的做用就是做爲中間表優化操做,好比 group by 做爲分組的中間表, order by rand() (MySQL 中的排序 中的例子)做爲中間表幫助運算等。sql
一、建表語法是 create temporary table …。數據庫
二、一個臨時表只能被建立它的 session 訪問,對其餘線程不可見,在會話結束後自動刪除。因此,圖中 session A 建立的臨時表 t,對於 session B 就是不可見的。(因此特別適合用於join 優化)數組
三、臨時表能夠與普通表同名。session
四、session A 內有同名的臨時表和普通表的時候,show create 語句,以及增刪改查語句訪問的是臨時表。併發
五、show tables 命令不顯示臨時表。函數
臨時表分爲磁盤臨時表和內存臨時表。磁盤臨時表指的是存儲在磁盤上的臨時表,由於在磁盤上,因此執行效率比較低,優勢結構能夠是有序的,實現能夠是 InnoDB(默認),MyISAM 引擎;內存臨時表就是存儲在內存中,執行效率高,經常使用的實現引擎是 Memory。性能
一、相比於 InnoDB 表,使用內存表不須要寫磁盤,往表 temp_t 的寫數據的速度更快;學習
二、索引 b 使用 hash 索引,查找的速度比 B-Tree 索引快;優化
三、臨時表數據只有 2000 行,佔用的內存有限。
一、InnoDB 表的數據老是有序存放的,而內存表的數據就是按照寫入順序存放的;關於這點能夠經過建立 b+ 索引來進行排序,優化查詢。alter table t1 add index a_btree_index using btree (id);
二、當數據文件有空洞的時候,InnoDB 表在插入新數據的時候,爲了保證數據有序性,只能在固定的位置寫入新值,而內存表找到空位就能夠插入新值;
三、數據位置發生變化的時候,InnoDB 表只須要修改主鍵索引,而內存表須要修改全部索引;
四、InnoDB 表用主鍵索引查詢時須要走一次索引查找,用普通索引查詢的時候,須要走兩次索引查找。而內存表沒有這個區別,全部索引的「地位」都是相同的。
五、InnoDB 支持變長數據類型,不一樣記錄的長度可能不一樣;內存表不支持 Blob 和 Text 字段,而且即便定義了 varchar(N),實際也看成 char(N),也就是固定長度字符串來存儲,所以內存表的每行數據長度相同。
六、內存表支持 hash 索引,而且數據存儲在內存中,因此執行比數據存儲在磁盤上的 Innodb 快。
一、鎖粒度大,只支持表級鎖,併發度低。
二、數據持久性差。由於是內存結構,因此在重啓後數據會丟失 。由此會致使備庫在硬件升級後數據就會丟失,而且若是主從庫互爲 "主備關係" ,備庫在關閉後還會將刪除數據記錄進 binlog,重啓後主機會執行備庫發送過來的 binlog ,致使主庫數據也會丟失。
雖然 Memory 引擎看起來缺點不少,可是由於其存儲在內存中,而且關機後會自動清除數據,因此其是做爲臨時表的一個絕佳選擇。
將一個大表 ht,按照字段 f,拆分紅 1024 個分表,而後分佈到 32 個數據庫實例上(水平分表)。通常狀況下,這種分庫分表系統都有一箇中間層 proxy。不過,也有一些方案會讓客戶端直接鏈接數據庫,也就是沒有 proxy 這一層。假設分區鍵是 列 f 。
一、若是隻使用分區鍵做爲查詢條件如 select v from ht where f=N,那麼直接經過分表規則找到 N 所在的表,而後去該表上查詢就能夠了。
二、若是使用其餘字段做爲條件且須要排序如 select v from ht where k >= M order by t_modified desc limit 100,那麼非但不能肯定要查詢的記錄在哪張表上,並且由於默認使用的是分區鍵排序,因此獲得的結果仍是無序的,須要額外排序。
1)在 proxy 層完成排序。優點是速度快,缺點是開發工做量比較大,若是涉及複雜的操做如 group by,甚至 join 這樣的操做,對中間層的開發能力要求比較高。而且還容易出現內存不夠、CPU 瓶頸的問題。
2)將各個分區的查詢結果(未排序)總結到一張臨時表上進行排序。
Ⅰ、在彙總庫上建立一個臨時表 temp_ht,表裏包含三個字段 v、k、t_modified;
Ⅱ、在各個分庫上執行 select v,k,t_modified from ht_x where k >= M order by t_modified desc limit 100;
Ⅲ、把分庫執行的結果插入到 temp_ht 表中;
Ⅳ、執行 select v from temp_ht order by t_modified desc limit 100;
有表t1: create table t1(id int primary key, a int, b int, index(a)); 有記錄(1,1,1) 到 (1000,1000,1000) 執行 (select 1000 as f) union (select id from t1 order by id desc limit 2);
解析這條 sql:
能夠知道:
一、左邊語句沒有進行查表操做 二、右邊語句使用了 id 索引 三、聯合時使用了臨時表
具體過程:
一、建立一個內存臨時表,這個臨時表只有一個整型字段 f,而且 f 是主鍵字段。
二、執行第一個子查詢,獲得 1000 這個值,並存入臨時表中。
三、執行第二個子查詢:
1)拿到第一行 id=1000,試圖插入臨時表中。但因爲 1000 這個值已經存在於臨時表了,違反了惟一性約束,因此插入失敗,而後繼續執行;
2)取到第二行 id=999,插入臨時表成功。
四、從臨時表中按行取出數據,返回結果,並刪除臨時表,結果中包含兩行數據分別是 1000 和 999。
舉一個在 MySQL中的排序 中提到過的例子。
select word from words order by rand() limit 3; 表數據有10000行 SQL是從10000行記錄中隨機獲取3條記錄返回。
這個執行過程由於涉及到 rand() 且數據量比較大,因此單靠 sort_buffer 排序空間不夠,因此還用到臨時表。
過程:
一、從緩衝池依次讀取記錄,每次讀取後都調用 rand() 函數生成一個 0-1 的數存入內存臨時表,W 是 word 值,R 是 rand() 生成的隨機數。到這掃描了 10000 行。
二、初始化 sort_buffer,從內存臨時表中將 rowid(這張表自動生成的) 以及 排序數據 R 存入 sort_buffer。到這由於要遍歷內存臨時表因此又掃描了 10000 行。
三、在 sort_buffer 中根據 R 排好序,而後選擇前三個記錄的 rowid 逐條去內存臨時表中查到 word 值返回。到這由於取了三個數據去內存臨時表去查找因此又掃描了 3 行。總共 20003 行。
執行:select id%10 as m, count(*) as c from t1 group by m;
首先解析 SQL:
能夠看到使用了臨時表和額外排序,接下來來解析
執行過程:
一、建立內存臨時表,表裏有兩個字段 m 和 c,主鍵是 m;
二、掃描表 t1 的索引 a,依次取出葉子節點上的 id 值,計算 id%10 的結果,記爲 x;
1)若是臨時表中沒有主鍵爲 x 的行,就插入一個記錄 (x,1);
2)若是表中有主鍵爲 x 的行,就將 x 這一行的 c 值加 1;
遍歷完成後,再根據字段 m 作排序,獲得結果集返回給客戶端。
排序的過程就按照排序規則進行,用到 sort_buffer ,可能用到臨時表。
表結構:
CREATE TABLE `t2` ( `id` int(11) NOT NULL, `a` int(11) DEFAULT NULL, `b` int(11) DEFAULT NULL, PRIMARY KEY (`id`), KEY `a` (`a`) ) ENGINE=InnoDB;
t一、t2 結構相等,t2 100萬條數據,t1 1000行數據,t1 的數據在 t2 上都有對應,相等。執行語句:select * from t1 join t2 on (t1.b=t2.b) where t2.b>=1 and t2.b<=2000;
分析:由於字段b 沒有建立索引,因此排序是屬於 BNL 排序,再加上數據量比較大,因此在比較時掃描的總行數就等於 100萬*1000,也就是10億次。
具體過程:
一、把表 t1 的全部字段取出來,存入 join_buffer 中。這個表只有 1000 行,join_buffer_size 默認值是 256k,能夠徹底存入。
二、掃描表 t2,取出每一行數據跟 join_buffer 中的數據進行對比,
1)若是不知足 t1.b=t2.b,則跳過;
2)若是知足 t1.b=t2.b, 再判斷其餘條件,也就是是否知足 t2.b 處於[1,2000]的條件,若是是,就做爲結果集的一部分返回,不然跳過。
優化:
若是篩選字段用的比較多,那麼能夠爲其建立索引,使 BNL 優化成 NLJ,可是若是這個字段使用的很少,那麼爲其建立索引反倒會由於多了沒必要要的維護成本而下降整體的性能。因此。針對於使用率不高的 BNL 篩選字段的優化,能夠建立一個臨時表,讓這個臨時表做爲一個索引表,來優化成 NLJ,同時由於臨時表在會話結束後會自動刪除,省去了維護成本。
create temporary table temp_t(id int primary key, a int, b int, index(b))engine=innodb; insert into temp_t select * from t2 where b>=1 and b<=2000; select * from t1 join temp_t on (t1.b=temp_t.b);
這樣執行過程就變成:
一、執行 insert 語句構造 temp_t 表並插入數據的過程當中,對錶 t2 作了全表掃描,這裏掃描行數是 100 萬。
二、以後的 join 語句,掃描表 t1,這裏的掃描行數是 1000;join 比較過程當中,作了 1000 次帶索引的查詢(由於t1 1000行,做爲驅動表,t2做爲被驅動表)。相比於優化前的 join 語句須要作 10 億次條件判斷來講,這個優化效果仍是很明顯的。
能夠看到在 sessionA 在已經建立了一個名爲 t1 的臨時表,而且 sessionA 未結束前,sessionB 也建立了一個名爲 t1 的臨時表,沒有發生異常。這是爲何?
首先要知道在 MySQL 啓動後每張表都會加載到內存中,因此每張表都分爲內存表和磁盤表。
一、對於磁盤表:
1)普通表的表結構和數據文件都是存儲在庫名文件夾下的,文件名就是表名。
2)結構文件存儲在臨時文件夾下,文件的後綴是 frm,前綴是 "#sql{進程 id}_{線程id}_序列號";
數據文件在 5.6 及以前是存儲在臨時文件夾下的,5.7 開始存放在專門存放臨時文件數據的臨時表空間。
二、對於內存表:
1)普通表的命名是 "庫名 + 表名"。
2)臨時表的命名則在 " 庫名 + 表名 " 的基礎上,加入了 " server_id + thread_id "。好比:
session A 的臨時表 t1,在備庫的 table_def_key 就是:庫名 +t1+「M 的 serverid」+「session A 的 thread_id」;
session B 的臨時表 t1,在備庫的 table_def_key 就是 :庫名 +t1+「M 的 serverid」+「session B 的 thread_id」。
綜上所述,由於臨時表在磁盤和內存中表的命名都取自具體的進程id、線程id、因此能夠實現不一樣的會話建立相同的表名。
若是 binlog 的格式是 row,那麼是不會記錄臨時表的各個操做的,由於臨時表就是做爲中間表來輔助各類操做的,因此在 row 格式下直接記錄的是通過臨時表得出的具體要操做的數據。
臨時表是一種很是方便的結構麼,由於其會隨着會話結束而自動刪除,因此在一些查詢效率較低但篩選字段使用不多的場景,就能夠經過建立臨時表,而後在臨時表上建立索引來提升查詢效率,同時也避免了索引的後續維護,而在其餘複雜操做中,臨時表也能夠充當中間表的做用。因此臨時表普遍出如今查詢(多表聯查)、分組、排序(排序返回的字段總長度過大)等場景中。
總結:
一、若是語句執行過程能夠一邊讀數據,一邊直接獲得結果,是不須要額外內存的,不然就須要額外的內存,來保存中間結果;
二、join_buffer 是無序數組,sort_buffer 是有序數組,內存臨時表是二維表結構,無序;磁盤臨時表默認是B+結構,能夠是數組,有序。
三、若是執行邏輯須要用到二維表特性,就會優先考慮使用臨時表。好比咱們的例子中,union 須要用到惟一索引約束, group by 還須要用到另一個字段來存累積計數。