今天是大年三十,在開始咱們今天的學習以前,我要先和你道一聲春節快樂!算法
在上一篇文章中,咱們在優化 join 查詢的時候使用到了臨時表。當時,咱們是這麼用的:sql
create temporary table temp_t like t1; alter table temp_t add index(b); 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);
你可能會有疑問,爲何要用臨時表呢?直接用普通表是否是也能夠呢?數據庫
今天咱們就從這個問題提及:臨時表有哪些特徵,爲何它適合這個場景?bash
這裏,我須要先幫你釐清一個容易誤解的問題:有的人可能會認爲,臨時表就是內存表。可是,這兩個概念但是徹底不一樣的。session
內存表,指的是使用 Memory 引擎的表,建表語法是 create table …engine=memory。這種表的數據都保存在內存裏,系統重啓的時候會被清空,可是表
結構還在。除了這兩個特性看上去比較「奇怪」外,從其餘的特徵上看,它就是一個正常的表。
而臨時表,可使用各類引擎類型 。若是是使用 InnoDB 引擎或者 MyISAM 引擎的臨時表,寫數據的時候是寫到磁盤上的。固然,臨時表也可使用 Memory 引擎多線程
弄清楚了內存表和臨時表的區別之後,咱們再來看看臨時表有哪些特徵。架構
弄清楚了內存表和臨時表的區別之後,咱們再來看看臨時表有哪些特徵。學習
爲了便於理解,咱們來看下下面這個操做序列:優化
圖 1 臨時表特性示例spa
能夠看到,臨時表在使用上有如下幾個特色:
1. 建表語法是 create temporary table …。
2. 一個臨時表只能被建立它的 session 訪問,對其餘線程不可見。因此,圖中 session A建立的臨時表 t,對於 session B 就是不可見的。
3. 臨時表能夠與普通表同名。
4. session A 內有同名的臨時表和普通表的時候,show create 語句,以及增刪改查語句訪問的是臨時表。
5. show tables 命令不顯示臨時表。
因爲臨時表只能被建立它的 session 訪問,因此在這個 session 結束的時候,會自動刪除臨時表。也正是因爲這個特性,臨時表就特別適合咱們文章開頭的 join 優化這種場景。爲
什麼呢?
緣由主要包括如下兩個方面:
1. 不一樣 session 的臨時表是能夠重名的,若是有多個 session 同時執行 join 優化,不須要擔憂表名重複致使建表失敗的問題。
2. 不須要擔憂數據刪除問題。若是使用普通表,在流程執行過程當中客戶端發生了異常斷開,或者數據庫發生異常重啓,還須要專門來清理中間過程當中生成的數據表。而臨時表因爲會自動回收,因此不須要這個額外的操做。
因爲不用擔憂線程之間的重名衝突,臨時表常常會被用在複雜查詢的優化過程當中。其中,分庫分表系統的跨庫查詢就是一個典型的使用場景。
通常分庫分表的場景,就是要把一個邏輯上的大表分散到不一樣的數據庫實例上。好比。將一個大表 ht,按照字段 f,拆分紅 1024 個分表,而後分佈到 32 個數據庫實例上。
以下圖所示:
圖 2 分庫分表簡圖
通常狀況下,這種分庫分表系統都有一箇中間層 proxy。不過,也有一些方案會讓客戶端直接鏈接數據庫,也就是沒有 proxy 這一層。
在這個架構中,分區 key 的選擇是以「減小跨庫和跨表查詢」爲依據的。若是大部分的語句都會包含 f 的等值條件,那麼就要用 f 作分區鍵。這樣,在 proxy 這一層解析完 SQL
語句之後,就能肯定將這條語句路由到哪一個分表作查詢。
好比下面這條語句:
select v from ht where f=N;
這時,咱們就能夠經過分表規則(好比,N%1024) 來確認須要的數據被放在了哪一個分表上。這種語句只須要訪問一個分表,是分庫分表方案最歡迎的語句形式了。
可是,若是這個表上還有另一個索引 k,而且查詢語句是這樣的:
select v from ht where k >= M order by t_modified desc limit 100;
這時候,因爲查詢條件裏面沒有用到分區字段 f,只能到全部的分區中去查找知足條件的全部行,而後統一作 order by 的操做。這種狀況下,有兩種比較經常使用的思路。
第一種思路是,在 proxy 層的進程代碼中實現排序。
這種方式的優點是處理速度快,拿到分庫的數據之後,直接在內存中參與計算。不過,這個方案的缺點也比較明顯:
1. 須要的開發工做量比較大。咱們舉例的這條語句還算是比較簡單的,若是涉及到複雜的操做,好比 group by,甚至 join 這樣的操做,對中間層的開發能力要求比較高;
2. 對 proxy 端的壓力比較大,尤爲是很容易出現內存不夠用和 CPU 瓶頸的問題。
另外一種思路就是,把各個分庫拿到的數據,彙總到一個 MySQL 實例的一個表中,而後在這個彙總實例上作邏輯操做。
好比上面這條語句,執行流程能夠相似這樣:
select v from ht where k >= M order by t_modified desc limit 100;
在彙總庫上建立一個臨時表 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;
獲得結果。
這個過程對應的流程圖以下所示:
圖 3 跨庫查詢流程示意圖
在實踐中,咱們每每會發現每一個分庫的計算量都不飽和,因此會直接把臨時表 temp_ht放到 32 個分庫中的某一個上。這時的查詢邏輯與圖 3 相似,你能夠本身再思考一下具體的流程。
你可能會問,不一樣線程能夠建立同名的臨時表,這是怎麼作到的呢?
接下來,咱們就看一下這個問題。
咱們在執行
create temporary table temp_t(id int primary key)engine=innodb;
這個語句的時候,MySQL 要給這個 InnoDB 表建立一個 frm 文件保存表結構定義,還要有地方保存表數據。
這個 frm 文件放在臨時文件目錄下,文件名的後綴是.frm,前綴是「#sql{進程 id}_{線程id}_ 序列號」。你可使用 select @@tmpdir 命令,來顯示實例的臨時文件目錄。
而關於表中數據的存放方式,在不一樣的 MySQL 版本中有着不一樣的處理方式:
從文件名的前綴規則,咱們能夠看到,其實建立一個叫做 t1 的 InnoDB 臨時表,MySQL在存儲上認爲咱們建立的表名跟普通表 t1 是不一樣的,所以同一個庫下面已經有普通表 t1
的狀況下,仍是能夠再建立一個臨時表 t1 的。
爲了便於後面討論,我先來舉一個例子。
圖 4 臨時表的表名
這個進程的進程號是 1234,session A 的線程 id 是 4,session B 的線程 id 是 5。因此你看到了,session A 和 session B 建立的臨時表,在磁盤上的文件不會重名。
MySQL 維護數據表,除了物理上要有文件外,內存裏面也有一套機制區別不一樣的表,每一個表都對應一個 table_def_key。
也就是說,session A 和 sessionB 建立的兩個臨時表 t1,它們的 table_def_key 不一樣,磁盤文件名也不一樣,所以能夠並存。
在實現上,每一個線程都維護了本身的臨時錶鏈表。這樣每次 session 內操做表的時候,先遍歷鏈表,檢查是否有這個名字的臨時表,若是有就優先操做臨時表,若是沒有再操做普
通表;在 session 結束的時候,對鏈表裏的每一個臨時表,執行 「DROP TEMPORARYTABLE + 表名」操做。
這時候你會發現,binlog 中也記錄了 DROP TEMPORARY TABLE 這條命令。你必定會以爲奇怪,臨時表只在線程內本身能夠訪問,爲何須要寫到 binlog 裏面?
這,就須要說到主備複製了。
既然寫 binlog,就意味着備庫須要。
你能夠設想一下,在主庫上執行下面這個語句序列:
create table t_normal(id int primary key, c int)engine=innodb;/*Q1*/ create temporary table temp_t like t_normal;/*Q2*/ insert into temp_t values(1,1);/*Q3*/ insert into t_normal select * from temp_t;/*Q4*/
若是關於臨時表的操做都不記錄,那麼在備庫就只有 create table t_normal 表和 insertinto t_normal select * from temp_t 這兩個語句的 binlog 日誌,備庫在執行到 insert
的時候,就會報錯「表 temp_t 不存在」。
你可能會說,若是把 binlog 設置爲 row 格式就行了吧?由於 binlog 是 row 格式時,在記錄 insert into t_normal 的 binlog 時,記錄的是這個操做的數據,即:write_rowevent
裏面記錄的邏輯是「插入一行數據(1,1)」。
確實是這樣。若是當前的 binlog_format=row,那麼跟臨時表有關的語句,就不會記錄到binlog 裏。也就是說,只在 binlog_format=statment/mixed 的時候,binlog 中才會記
錄臨時表的操做。
這種狀況下,建立臨時表的語句會傳到備庫執行,所以備庫的同步線程就會建立這個臨時表。主庫在線程退出的時候,會自動刪除臨時表,可是備庫同步線程是持續在運行的。所
以,這時候咱們就須要在主庫上再寫一個 DROP TEMPORARY TABLE 傳給備庫執行。
以前有人問過我一個有趣的問題:MySQL 在記錄 binlog 的時候,不管是 create table仍是 alter table 語句,都是原樣記錄,甚至於連空格都不變。可是若是執行 drop tablet_normal,
系統記錄 binlog 就會寫成:
DROP TABLE `t_normal` /* generated by server */
也就是改爲了標準的格式。爲何要這麼作呢 ?
如今你知道緣由了,那就是:drop table 命令是能夠一次刪除多個表的。好比,在上面的例子中,設置 binlog_format=row,若是主庫上執行 "drop table t_normal, temp_t"這個命令,那麼 binlog 中就只能記錄:
DROP TABLE `t_normal` /* generated by server */
由於備庫上並無表 temp_t,將這個命令重寫後再傳到備庫執行,纔不會致使備庫同步線程中止。
因此,drop table 命令記錄 binlog 的時候,就必須對語句作改寫。「/* generated byserver */」說明了這是一個被服務端改寫過的命令。
說到主備複製,還有另一個問題須要解決:主庫上不一樣的線程建立同名的臨時表是不要緊的,可是傳到備庫執行是怎麼處理的呢?
如今,我給你舉個例子,下面的序列中實例 S 是 M 的備庫。
圖 5 主備關係中的臨時表操做
主庫 M 上的兩個 session 建立了同名的臨時表 t1,這兩個 create temporary table t1語句都會被傳到備庫 S 上。
可是,備庫的應用日誌線程是共用的,也就是說要在應用線程裏面前後執行這個 create 語句兩次。(即便開了多線程複製,也可能被分配到從庫的同一個 worker 中執行)。那
麼,這會不會致使同步線程報錯 ?
顯然是不會的,不然臨時表就是一個 bug 了。也就是說,備庫線程在執行的時候,要把這兩個 t1 表當作兩個不一樣的臨時表來處理。這,又是怎麼實現的呢?
MySQL 在記錄 binlog 的時候,會把主庫執行這個語句的線程 id 寫到 binlog 中。這樣,在備庫的應用線程就可以知道執行每一個語句的主庫線程 id,並利用這個線程 id 來構
造臨時表的 table_def_key:
1. session A 的臨時表 t1,在備庫的 table_def_key 就是:庫名 +t1+「M 的serverid」+「session A 的 thread_id」;
2. session B 的臨時表 t1,在備庫的 table_def_key 就是 :庫名 +t1+「M 的serverid」+「session B 的 thread_id」。
因爲 table_def_key 不一樣,因此這兩個表在備庫的應用線程裏面是不會衝突的。
今天這篇文章,我和你介紹了臨時表的用法和特性。
在實際應用中,臨時表通常用於處理比較複雜的計算邏輯。因爲臨時表是每一個線程本身可見的,因此不須要考慮多個線程執行同一個處理邏輯時,臨時表的重名問題。在線程退出
的時候,臨時表也能自動刪除,省去了收尾和異常處理的工做。
在 binlog_format='row’的時候,臨時表的操做不記錄到 binlog 中,也省去了很多麻煩,這也能夠成爲你選擇 binlog_format 時的一個考慮因素。
須要注意的是,咱們上面說到的這種臨時表,是用戶本身建立的 ,也能夠稱爲用戶臨時表。與它相對應的,就是內部臨時表,在第 17 篇文章中我已經和你介紹過。
最後,我給你留下一個思考題吧。
下面的語句序列是建立一個臨時表,並將其更名:
圖 6 關於臨時表更名的思考題
能夠看到,咱們可使用 alter table 語法修改臨時表的表名,而不能使用 rename 語法。你知道這是什麼緣由嗎?
你能夠把你的分析寫在留言區,我會在下一篇文章的末尾和你討論這個問題。感謝你的收聽,也歡迎你把這篇文章分享給更多的朋友一塊兒閱讀。
上期的問題是,對於下面這個三個表的 join 語句,
select * from t1 join t2 on(t1.a=t2.a) join t3 on (t2.b=t3.b) where t1.c>=X and t2.c>=Y and t3.c>=Z;
若是改寫成 straight_join,要怎麼指定鏈接順序,以及怎麼給三個表建立索引。
第一原則是要儘可能使用 BKA 算法。須要注意的是,使用 BKA 算法的時候,並非「先計算兩個表 join 的結果,再跟第三個表 join」,而是直接嵌套查詢的。
具體實現是:在 t1.c>=X、t2.c>=Y、t3.c>=Z 這三個條件裏,選擇一個通過過濾之後,數據最少的那個表,做爲第一個驅動表。此時,可能會出現以下兩種狀況。
第一種狀況,若是選出來是表 t1 或者 t3,那剩下的部分就固定了。
1. 若是驅動表是 t1,則鏈接順序是 t1->t2->t3,要在被驅動表字段建立上索引,也就是t2.a 和 t3.b 上建立索引;
2. 若是驅動表是 t3,則鏈接順序是 t3->t2->t1,須要在 t2.b 和 t1.a 上建立索引。同時,咱們還須要在第一個驅動表的字段 c 上建立索引。
第二種狀況是,若是選出來的第一個驅動表是表 t2 的話,則須要評估另外兩個條件的過濾效果。
總之,總體的思路就是,儘可能讓每一次參與 join 的驅動表的數據集,越小越好,由於這樣咱們的驅動表就會越小。
@庫淘淘 作了實驗驗證;@poppy 同窗作了很不錯的分析;@dzkk 同窗在評論中介紹了 MariaDB 支持的 hash join,你們能夠了解一下;@老楊同志提了一個好問題,若是語句使用了索引 a,結果還要對 a 排序,就不用 MRR 優化了,不然回表完還要增長額外的排序過程,得不償失。