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

1、引子

這是咱們專欄的最後一篇答疑文章,今天咱們來講說一些好問題。算法

在我看來,可以幫咱們擴展一個邏輯的邊界的問題,就是好問題。由於經過解決這樣的問題,可以加深咱們對這個邏輯的理解,或者幫咱們關聯到另一個知識點,進而能夠幫助
咱們創建起本身的知識網絡。數組

在工做中會問好問題,是一個很重要的能力。bash

通過這段時間的學習,從評論區的問題我能夠感受出來,緊跟課程學習的同窗,對 SQL 語句執行性能的感受愈來愈好了,提出的問題也愈來愈細緻和精準了。網絡

接下來,咱們就一塊兒看看同窗們在評論區提到的這些好問題。在和你一塊兒分析這些問題的時候,我會指出它們具體是在哪篇文章出現的。同時,在回答這些問題的過程當中,我會假session

設你已經掌握了這篇文章涉及的知識。固然,若是你印象模糊了,也能夠跳回文章再複習一次。函數

2、join 的寫法

一、兩個 join 的查詢結果

在第 35 篇文章《join 語句怎麼優化?》中,我在介紹 join 執行順序的時候,用的都是straight_join。@郭健 同窗在文後提出了兩個問題:oop

1. 若是用 left join 的話,左邊的表必定是驅動表嗎?
2. 若是兩個表的 join 包含多個條件的等值匹配,是都要寫到 on 裏面呢,仍是隻把一個條件寫到 on 裏面,其餘條件寫到 where 部分?性能

爲了同時回答這兩個問題,我來構造兩個表 a 和 b:學習

create table a(f1 int, f2 int, index(f1))engine=innodb;
create table b(f1 int, f2 int)engine=innodb;
insert into a values(1,1),(2,2),(3,3),(4,4),(5,5),(6,6);
insert into b values(3,3),(4,4),(5,5),(6,6),(7,7),(8,8);

表 a 和 b 都有兩個字段 f1 和 f2,不一樣的是表 a 的字段 f1 上有索引。而後,我往兩個表中都插入了 6 條記錄,其中在表 a 和 b 中同時存在的數據有 4 行。
@郭健 同窗提到的第二個問題,其實就是下面這兩種寫法的區別:優化

我把這兩條語句分別記爲 Q1 和 Q2。

首先,須要說明的是,這兩個 left join 語句的語義邏輯並不相同。咱們先來看一下它們的執行結果。

圖 1 兩個 join 的查詢結果

能夠看到:

語句 Q1 返回的數據集是 6 行,表 a 中即便沒有知足匹配條件的記錄,查詢結果中也會返回一行,並將表 b 的各個字段值填成 NULL。
語句 Q2 返回的是 4 行。從邏輯上能夠這麼理解,最後的兩行,因爲表 b 中沒有匹配的字段,結果集裏面 b.f2 的值是空,不知足 where 部分的條件判斷,所以不能做爲結果集的一部分。

接下來,咱們看看實際執行這兩條語句時,MySQL 是怎麼作的。

咱們先一塊兒看看語句 Q1 的 explain 結果:

圖 2 Q1 的 explain 結果

能夠看到,這個結果符合咱們的預期:驅動表是表 a,被驅動表是表 b;

因爲表 b 的 f1 字段上沒有索引,因此使用的是 Block Nexted Loop Join(簡稱BNL) 算法。

二、left join -BNL 算法

看到 BNL 算法,你就應該知道這條語句的執行流程實際上是這樣的:

1. 把表 a 的內容讀入 join_buffer 中。由於是 select * ,因此字段 f1 和 f2 都被放入join_buffer 了。
2. 順序掃描表 b,對於每一行數據,判斷 join 條件(也就是 a.f1=b.f1 and a.f2=b.f2) 是否知足,知足條件的記錄, 做爲結果集的一行返回。若是語句中有 where 子句,須要先判斷 where 部分知足條件後,再返回。
3. 表 b 掃描完成後,對於沒有被匹配的表 a 的行(在這個例子中就是 (1,1)、(2,2) 這兩行),把剩餘字段補上 NULL,再放入結果集中。

對應的流程圖以下:

圖 3 left join -BNL 算法

能夠看到,這條語句確實是以表 a 爲驅動表,並且從執行效果看,也和使用 straight_join是同樣的。

你可能會想,語句 Q2 的查詢結果裏面少了最後兩行數據,是否是就是把上面流程中的步驟 3 去掉呢?咱們仍是先看一下語句 Q2 的 expain 結果吧。

圖 4 Q2 的 explain 結果

這裏先和你說一句題外話,專欄立刻就結束了,我也和你一塊兒根據 explain 結果「腦補」了不少次一條語句的執行流程了,因此我但願你已經具有了這個能力。今天,咱們再
一塊兒分析一次 SQL 語句的 explain 結果。

能夠看到,這條語句是以表 b 爲驅動表的。而若是一條 join 語句的 Extra 字段什麼都沒寫的話,就表示使用的是 Index Nested-Loop Join(簡稱 NLJ)算法。

所以,語句 Q2 的執行流程是這樣的:順序掃描表 b,每一行用 b.f1 到表 a 中去查,匹配到記錄後判斷 a.f2=b.f2 是否知足,知足條件的話就做爲結果集的一部分返回。

三、爲何語句 Q1 和 Q2 這兩個查詢的執行流程會差距這麼大呢?

那麼,爲何語句 Q1 和 Q2 這兩個查詢的執行流程會差距這麼大呢?其實,這是由於優化器基於 Q2 這個查詢的語義作了優化。

爲了理解這個問題,我須要再和你交代一個背景知識點:在 MySQL 裏,NULL 跟任何值執行等值判斷和不等值判斷的結果,都是 NULL。這裏包括, select NULL = NULL 的結
果,也是返回 NULL。

所以,語句 Q2 裏面 where a.f2=b.f2 就表示,查詢結果裏面不會包含 b.f2 是 NULL 的行,這樣這個 left join 的語義就是「找到這兩個表裏面,f一、f2 對應相同的行。對於表 a
中存在,而表 b 中匹配不到的行,就放棄」。

這樣,這條語句雖然用的是 left join,可是語義跟 join 是一致的。

所以,優化器就把這條語句的 left join 改寫成了 join,而後由於表 a 的 f1 上有索引,就把表 b 做爲驅動表,這樣就能夠用上 NLJ 算法。在執行 explain 以後,你再執行 show
warnings,就能看到這個改寫的結果,如圖 5 所示。

圖 5 Q2 的改寫結果

這個例子說明,即便咱們在 SQL 語句中寫成 left join,執行過程仍是有可能不是從左到右鏈接的。也就是說,使用 left join 時,左邊的表不必定是驅動表。

這樣看來,若是須要 left join 的語義,就不能把被驅動表的字段放在 where 條件裏面作等值判斷或不等值判斷,必須都寫在 on 裏面。那若是是 join 語句呢?

這時候,咱們再看看這兩條語句:

select * from a join b on(a.f1=b.f1) and (a.f2=b.f2); /*Q3*/
select * from a join b on(a.f1=b.f1) where (a.f2=b.f2);/*Q4*/

咱們再使用一次看 explain 和 show warnings 的方法,看看優化器是怎麼作的。

圖 6 join 語句改寫

能夠看到,這兩條語句都被改寫成:

select * from a join b where (a.f1=b.f1) and (a.f2=b.f2);

執行計劃天然也是如出一轍的。

也就是說,在這種狀況下,join 將判斷條件是否所有放在 on 部分就沒有區別了。

3、Simple Nested Loop Join 的性能問題

咱們知道,join 語句使用不一樣的算法,對語句的性能影響會很大。在第 34 篇文章《到底可不可使用 join?》的評論區中,@書策稠濁 和 @朝夕心 兩位同窗提了一個很不錯的問題。

咱們在文中說到,雖然 BNL 算法和 Simple Nested Loop Join 算法都是要判斷 M*N 次(M 和 N 分別是 join 的兩個表的行數),可是 Simple Nested Loop Join 算法的每輪
判斷都要走全表掃描,所以性能上 BNL 算法執行起來會快不少。

爲了便於說明,我仍是先爲你簡單描述一下這兩個算法。

BNL 算法的執行邏輯是:

1. 首先,將驅動表的數據所有讀入內存 join_buffer 中,這裏 join_buffer 是無序數組;
2. 而後,順序遍歷被驅動表的全部行,每一行數據都跟 join_buffer 中的數據進行匹配,匹配成功則做爲結果集的一部分返回。

Simple Nested Loop Join 算法的執行邏輯是:順序取出驅動表中的每一行數據,到被驅動表去作全表掃描匹配,匹配成功則做爲結果集的一部分返回。

這兩位同窗的疑問是,Simple Nested Loop Join 算法,其實也是把數據讀到內存裏,而後按照匹配條件進行判斷,爲何性能差距會這麼大呢?

解釋這個問題,須要用到 MySQL 中索引結構和 Buffer Pool 的相關知識點:

1. 在對被驅動表作全表掃描的時候,若是數據沒有在 Buffer Pool 中,就須要等待這部分數據從磁盤讀入;從磁盤讀入數據到內存中,會影響正常業務的 Buffer Pool 命中率,並且這個算法自然

會對被驅動表的數據作屢次訪問,更容易將這些數據頁放到 Buffer Pool 的頭部(請參考第 35 篇文章中的相關內容);

2. 即便被驅動表數據都在內存中,每次查找「下一個記錄的操做」,都是相似指針操做。而 join_buffer 中是數組,遍歷的成本更低。

因此說,BNL 算法的性能會更好。

4、distinct 和 group by 的性能

在第 37 篇文章《何時會使用內部臨時表?》中,@老楊同志 提了一個好問題:若是隻須要去重,不須要執行聚合函數,distinct 和 group by 哪一種效率高一些呢?

我來展開一下他的問題:若是表 t 的字段 a 上沒有索引,那麼下面這兩條語句:

select a from t group by a order by null;
select distinct a from t;

的性能是否是相同的?

首先須要說明的是,這種 group by 的寫法,並非 SQL 標準的寫法。標準的 group by語句,是須要在 select 部分加一個聚合函數,好比:

select a,count(*) from t group by a order by null;

這條語句的邏輯是:按照字段 a 分組,計算每組的 a 出現的次數。在這個結果裏,因爲作的是聚合計算,相同的 a 只出現一次。

備註:這裏你能夠順便複習一下第 37 篇文章中關於 group by 的相關內容。

沒有了 count(*) 之後,也就是再也不須要執行「計算總數」的邏輯時,第一條語句的邏輯就變成是:按照字段 a 作分組,相同的 a 的值只返回一行。而這就是 distinct 的語義,因此
不須要執行聚合函數時,distinct 和 group by 這兩條語句的語義和執行流程是相同的,

所以執行性能也相同。

這兩條語句的執行流程是下面這樣的。

1. 建立一個臨時表,臨時表有一個字段 a,而且在這個字段 a 上建立一個惟一索引;
2. 遍歷表 t,依次取數據插入臨時表中:若是發現惟一鍵衝突,就跳過;不然插入成功;

3. 遍歷完成後,將臨時表做爲結果集返回給客戶端。

5、備庫自增主鍵問題

除了性能問題,你們對細節的追問也很到位。在第 39 篇文章《自增主鍵爲何不是連續的?》評論區,@帽子掉了 同窗問到:在 binlog_format=statement 時,語句 A 先獲取
id=1,而後語句 B 獲取 id=2;接着語句 B 提交,寫 binlog,而後語句 A 再寫 binlog。

這時候,若是 binlog 重放,是否是會發生語句 B 的 id 爲 1,而語句 A 的 id 爲 2 的不一致狀況呢?

首先,這個問題默認了「自增 id 的生成順序,和 binlog 的寫入順序多是不一樣的」,這個理解是正確的。

其次,這個問題限定在 statement 格式下,也是對的。由於 row 格式的 binlog 就沒有這個問題了,Write row event 裏面直接寫了每一行的全部字段的值。

而至於爲何不會發生不一致的狀況,咱們來看一下下面的這個例子。

create table t(id int auto_increment primary key);
insert into t values(null);

圖 7 insert 語句的 binlog

能夠看到,在 insert 語句以前,還有一句 SET INSERT_ID=1。這條命令的意思是,這個線程裏下一次須要用到自增值的時候,不論當前表的自增值是多少,固定用 1 這個值。

這個 SET INSERT_ID 語句是固定跟在 insert 語句以前的,好比 @帽子掉了同窗提到的場景,主庫上語句 A 的 id 是 1,語句 B 的 id 是 2,可是寫入 binlog 的順序先 B 後 A,那
麼 binlog 就變成:

SET INSERT_ID=2;
語句B;
SET INSERT_ID=1;
語句A;

你看,在備庫上語句 B 用到的 INSERT_ID 依然是 2,跟主庫相同。

所以,即便兩個 INSERT 語句在主備庫的執行順序不一樣,自增主鍵字段的值也不會不一致。

6、小結

今天這篇答疑文章,我選了 4 個好問題和你分享,並作了分析。在我看來,可以提出好問題,首先表示這些同窗理解了咱們文章的內容,進而又作了深刻思考。有大家在認真的閱
讀和思考,對我來講是鼓勵,也是動力。

說實話,短短的三篇答疑文章沒法所有展開同窗們在評論區留下的高質量問題,以後有的同窗還會二刷,也會有新的同窗加入,你們想到新的問題就請給我留言吧,我會繼續關注
評論區,和你在評論區交流。

老規矩,答疑文章也是要有課後思考題的。

在第 8 篇文章的評論區, @XD 同窗提到一個問題:他查看了一下 innodb_trx,發現這個事務的 trx_id 是一個很大的數(281479535353408),並且彷佛在同一個 session 中
啓動的會話獲得的 trx_id 是保持不變的。當執行任何加寫鎖的語句後,trx_id 都會變成一個很小的數字(118378)。

你能夠經過實驗驗證一下,而後分析看看,事務 id 的分配規則是什麼,以及 MySQL 爲何要這麼設計呢?

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

7、上期問題時間

上期的問題是,怎麼給分區表 t 建立自增主鍵。因爲 MySQL 要求主鍵包含全部的分區字段,因此確定是要建立聯合主鍵的。

這時候就有兩種可選:一種是 (ftime, id),另外一種是 (id, ftime)。若是從利用率上來看,應該使用 (ftime, id) 這種模式。由於用 ftime 作分區 key,說明大
多數語句都是包含 ftime 的,使用這種模式,能夠利用前綴索引的規則,減小一個索引。

這時的建表語句是:

CREATE TABLE `t` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `ftime` datetime NOT NULL,
  `c` int(11) DEFAULT NULL,
  PRIMARY KEY (`ftime`,`id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1
PARTITION BY RANGE (YEAR(ftime))
(PARTITION p_2017 VALUES LESS THAN (2017) ENGINE = MyISAM,
 PARTITION p_2018 VALUES LESS THAN (2018) ENGINE = MyISAM,
 PARTITION p_2019 VALUES LESS THAN (2019) ENGINE = MyISAM,
 PARTITION p_others VALUES LESS THAN MAXVALUE ENGINE = MyISAM);

固然,個人建議是你要儘可能使用 InnoDB 引擎。InnoDB 表要求至少有一個索引,以自增字段做爲第一個字段,因此須要加一個 id 的單獨索引。

CREATE TABLE `t` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `ftime` datetime NOT NULL,
  `c` int(11) DEFAULT NULL,
  PRIMARY KEY (`ftime`,`id`),
  KEY `id` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1
PARTITION BY RANGE (YEAR(ftime))
(PARTITION p_2017 VALUES LESS THAN (2017) ENGINE = InnoDB,
 PARTITION p_2018 VALUES LESS THAN (2018) ENGINE = InnoDB,
 PARTITION p_2019 VALUES LESS THAN (2019) ENGINE = InnoDB,
 PARTITION p_others VALUES LESS THAN MAXVALUE ENGINE = InnoDB);

固然把字段反過來,建立成:

PRIMARY KEY (`id`,`ftime`),
KEY `id` (`ftime`)

也是能夠的。

評論區留言點贊板:

@夾心麪包 、@郭江偉 同窗提到了最後一種方案。

@aliang 同窗提了一個好問題,關於 open_files_limit 和innodb_open_files 的關係,我在回覆中作了說明,你們能夠看一下。

@萬勇 提了一個好問題,實際上對於如今官方的版本,將字段加在中間仍是最後,在性能上是沒差異的。可是,我建議你們養成習慣(若是你是 DBA就幫業務開發同窗養成習慣),將字段加在最後面,由於這樣仍是比較方便操做的。這個問題,我也在評論的答覆中作了說明,你能夠看一下。

相關文章
相關標籤/搜索