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

1、引子

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

在上一篇文章中,咱們在優化 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 引擎多線程

弄清楚了內存表和臨時表的區別之後,咱們再來看看臨時表有哪些特徵。架構

弄清楚了內存表和臨時表的區別之後,咱們再來看看臨時表有哪些特徵。學習

2、臨時表的特性

爲了便於理解,咱們來看下下面這個操做序列:優化

圖 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. 不須要擔憂數據刪除問題。若是使用普通表,在流程執行過程當中客戶端發生了異常斷開,或者數據庫發生異常重啓,還須要專門來清理中間過程當中生成的數據表。而臨時表因爲會自動回收,因此不須要這個額外的操做。

3、臨時表的應用

一、分庫分表簡圖

因爲不用擔憂線程之間的重名衝突,臨時表常常會被用在複雜查詢的優化過程當中。其中,分庫分表系統的跨庫查詢就是一個典型的使用場景。

通常分庫分表的場景,就是要把一個邏輯上的大表分散到不一樣的數據庫實例上。好比。將一個大表 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 層的進程代碼中實現排序。

第一種思路是,在 proxy 層的進程代碼中實現排序。

這種方式的優點是處理速度快,拿到分庫的數據之後,直接在內存中參與計算。不過,這個方案的缺點也比較明顯:

1. 須要的開發工做量比較大。咱們舉例的這條語句還算是比較簡單的,若是涉及到複雜的操做,好比 group by,甚至 join 這樣的操做,對中間層的開發能力要求比較高;
2. 對 proxy 端的壓力比較大,尤爲是很容易出現內存不夠用和 CPU 瓶頸的問題。

三、另外一種思路就是,把各個分庫拿到的數據,彙總到一個 MySQL 實例的一個表中,而後在這個彙總實例上作邏輯操做。

另外一種思路就是,把各個分庫拿到的數據,彙總到一個 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 相似,你能夠本身再思考一下具體的流程。

4、爲何臨時表能夠重名?

一、不一樣線程能夠建立同名的臨時表,這是怎麼作到的呢?

你可能會問,不一樣線程能夠建立同名的臨時表,這是怎麼作到的呢?

接下來,咱們就看一下這個問題。

咱們在執行

create temporary table temp_t(id int primary key)engine=innodb;

這個語句的時候,MySQL 要給這個 InnoDB 表建立一個 frm 文件保存表結構定義,還要有地方保存表數據。

這個 frm 文件放在臨時文件目錄下,文件名的後綴是.frm,前綴是「#sql{進程 id}_{線程id}_ 序列號」。你可使用 select @@tmpdir 命令,來顯示實例的臨時文件目錄。

而關於表中數據的存放方式,在不一樣的 MySQL 版本中有着不一樣的處理方式:

  • 在 5.6 以及以前的版本里,MySQL 會在臨時文件目錄下建立一個相同前綴、以.ibd 爲後綴的文件,用來存放數據文件;
  • 而從 5.7 版本開始,MySQL 引入了一個臨時文件表空間,專門用來存放臨時文件的數據。所以,咱們就不須要再建立 ibd 文件了。

從文件名的前綴規則,咱們能夠看到,其實建立一個叫做 t1 的 InnoDB 臨時表,MySQL在存儲上認爲咱們建立的表名跟普通表 t1 是不一樣的,所以同一個庫下面已經有普通表 t1
的狀況下,仍是能夠再建立一個臨時表 t1 的。

二、不一樣線程能夠建立同名的臨時表驗證過程

爲了便於後面討論,我先來舉一個例子。

圖 4 臨時表的表名

這個進程的進程號是 1234,session A 的線程 id 是 4,session B 的線程 id 是 5。因此你看到了,session A 和 session B 建立的臨時表,在磁盤上的文件不會重名。

MySQL 維護數據表,除了物理上要有文件外,內存裏面也有一套機制區別不一樣的表,每一個表都對應一個 table_def_key。

  • 一個普通表的 table_def_key 的值是由「庫名 + 表名」獲得的,因此若是你要在同一個庫下建立兩個同名的普通表,建立第二個表的過程當中就會發現 table_def_key 已經存在了。
  • 而對於臨時表,table_def_key 在「庫名 + 表名」基礎上,又加入了「server_id+thread_id」。

也就是說,session A 和 sessionB 建立的兩個臨時表 t1,它們的 table_def_key 不一樣,磁盤文件名也不一樣,所以能夠並存

在實現上,每一個線程都維護了本身的臨時錶鏈表。這樣每次 session 內操做表的時候,先遍歷鏈表,檢查是否有這個名字的臨時表,若是有就優先操做臨時表,若是沒有再操做普
通表;在 session 結束的時候,對鏈表裏的每一個臨時表,執行 「DROP TEMPORARYTABLE + 表名」操做。

這時候你會發現,binlog 中也記錄了 DROP TEMPORARY TABLE 這條命令。你必定會以爲奇怪,臨時表只在線程內本身能夠訪問,爲何須要寫到 binlog 裏面?

這,就須要說到主備複製了。

5、臨時表和主備複製

一、若是關於臨時表的操做都不記錄、備庫在執行到 insertinto t_normal 的時候,就會報錯「表 temp_t 不存在」

既然寫 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 設置爲 row 格式就行了吧?

確實是這樣。若是當前的 binlog_format=row,那麼跟臨時表有關的語句,就不會記錄到binlog 裏。也就是說,只在 binlog_format=statment/mixed 的時候,binlog 中才會記
錄臨時表的操做。

這種狀況下,建立臨時表的語句會傳到備庫執行,所以備庫的同步線程就會建立這個臨時表。主庫在線程退出的時候,會自動刪除臨時表,可是備庫同步線程是持續在運行的。所
以,這時候咱們就須要在主庫上再寫一個 DROP TEMPORARY TABLE 傳給備庫執行。

三、MySQL 在記錄 binlog 的時候drop tablet_normal改爲了標準的格式。爲何要這麼作呢 ?

以前有人問過我一個有趣的問題: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 不一樣,因此這兩個表在備庫的應用線程裏面是不會衝突的。

6、小結

今天這篇文章,我和你介紹了臨時表的用法和特性。

在實際應用中,臨時表通常用於處理比較複雜的計算邏輯。因爲臨時表是每一個線程本身可見的,因此不須要考慮多個線程執行同一個處理邏輯時,臨時表的重名問題。在線程退出
的時候,臨時表也能自動刪除,省去了收尾和異常處理的工做。

在 binlog_format='row’的時候,臨時表的操做不記錄到 binlog 中,也省去了很多麻煩,這也能夠成爲你選擇 binlog_format 時的一個考慮因素。

須要注意的是,咱們上面說到的這種臨時表,是用戶本身建立的 ,也能夠稱爲用戶臨時表。與它相對應的,就是內部臨時表,在第 17 篇文章中我已經和你介紹過。

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

下面的語句序列是建立一個臨時表,並將其更名:

圖 6 關於臨時表更名的思考題

能夠看到,咱們可使用 alter table 語法修改臨時表的表名,而不能使用 rename 語法。你知道這是什麼緣由嗎?

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

7、上期問題時間

上期的問題是,對於下面這個三個表的 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 優化了,不然回表完還要增長額外的排序過程,得不償失。

相關文章
相關標籤/搜索