1、等值連接:顯性連接和隱性連接
在《MySQL必知必會》中對於等值連接有提到兩種方式,第一種是直接在WHERE子句中規定如何關聯即可,那么第二種則是使用INNER JOIN關鍵字。如下例兩種方式是“等同”的。
//WHERE方式
SELECT
vend_name,
prod_name,
prod_price,
quantity
FROM
vendors,
products,
orderitems
WHERE
vendors.vend_id = products.vend_id
AND
orderitems.prod_id = products.prod_id;
14
1
//WHERE方式
2
SELECT
3
vend_name,
4
prod_name,
5
prod_price,
6
quantity
7
FROM
8
vendors,
9
products,
10
orderitems
11
WHERE
12
vendors.vend_id = products.vend_id
13
AND
14
orderitems.prod_id = products.prod_id;
//INNER JOIN方式
SELECT
vend_name,
prod_name,
prod_price,
quantity
FROM
(vendors INNER JOIN products ON vendors.vend_id = prodcuts.vend_id)
INNER JOIN orderitems ON orderitems.prod_id = products.prod_id;
9
1
//INNER JOIN方式
2
SELECT
3
vend_name,
4
prod_name,
5
prod_price,
6
quantity
7
FROM
8
(vendors INNER JOIN products ON vendors.vend_id = prodcuts.vend_id)
9
INNER JOIN orderitems ON orderitems.prod_id = products.prod_id;
其中,WHERE方式我們稱之為
隱性連接,而INNER JOIN方式我們稱之為
顯性連接。這兩者是有區別的,而上面我們說的“等同”,是指兩者在結果集上是等同的,實際上在執行過程上卻是不同的。
之前我們提到過SQL語句的執行過程,實際上都會產生笛卡兒積,都會有一個虛擬表,用來暫時保存執行結果,以作為下一步的輸入。另外,ON過濾的執行順序是在WHERE之前的,所以這就導致兩者的執行過程區別在於:
- 隱性連接,在FROM過程中對所有表進行笛卡兒積,最終通過WHERE條件過濾
- 顯性連接,在每一次表連接時通過ON過濾,篩選后的結果集再和下一個表做笛卡兒積,以此循環
這么久了,我們終於要說到SQL性能的主題上來了。那么以上,這兩種執行方式會導致什么問題呢?假如有三張表做等值連接,每張表都有1000行數據,那么:
- 隱性連接,做所有表的笛卡兒積,共1000*1000*1000=1億 行數據,再通過WHERE過濾,也就是說,三張表連接最終掃描的數據量高達1億
- 顯性連接,先做頭兩張表的笛卡兒積1000*1000=100萬 行數據,通過ON條件篩選后的結果集(可能不到1000行)再和第三張表1000行數據做笛卡兒積
也就是說,顯性連接最終做笛卡兒積的數量,根據之前表間ON后的結果,可能會遠遠小於隱性連接所要掃描的數量,所以
同樣是等值連接,顯性連接的效率更高。
2、EXPLAIN
2.1 驅動表
有的人可能會疑惑,不對啊,你這么說來,顯性連接和隱性連接的差距不是一點半點,為什么我測試出來,兩者的執行效率卻幾乎是等同的呢?
這是因為數據庫引擎搗的鬼,這里以MySQL舉例,在MySQL中,表間關聯的算法是Nest Loop Join,即JOIN是通過嵌套循環來實現的。而你所寫SQL的連表順序(非OUTER類型)並不是實際執行的連表順序,因為數據庫會針對表情況進行自動優化,以小的結果集來驅動大的結果集,我們也常說以小表驅動大表。
也就是說,假如你有三張表,你寫下SQL的JOIN順序是A JOIN B ON ... JOIN C ON ...,其中表A有1000條數據,表B有100條數據,表C只有10條數據,實際上在執行的時候,很可能是先掃描數量最少的表C,然后是表B,最后是表A,中途遇到符合ON條件過濾的則執行篩選。為什么?
數據庫不傻,我們說過表連接時通過嵌套循環來實現的,從第一個表中取出第一條,和第二個表中所有記錄進行匹配,再取出第二條,和第二個表中所有記錄進行匹配,以此循環。這里的第一個表,我們就稱之為
驅動表。
如果驅動表越大,意味着外層循環次數就越多,那么被驅動表的訪問次數自然也就越多(如驅動表和被驅動表數據分別為10條和100條,那么被驅動表訪問次數為10次;如果分別是100條和10條,被驅動表訪問次數則為100次),而每次訪問被驅動表,即使需要的邏輯 IO 很少,循環次數多了,總量也不可能小,而且每次循環都不能避免消耗CPU,所以 CPU 運算量也會跟着增加。最終,這就意味着SQL性能的消耗,表現在查詢時間變長。
就像你去超市買東西,總共都是買1000件東西,我讓你買100件就付款一次,共付款10次;或者買10件就付款一次,共付款100次,哪個更累人?

所以,現在我們已經明白了,原來數據庫在執行我們的SQL的時候,是會對執行順序進行優化調整的。另外,要注意的是,這里的驅動表,並不是說數據量小的就是驅動表,我們剛才也提過,如果僅僅以表的大小來作為驅動表的判斷依據,假若小表過濾后所剩下的結果集比大表多很多,結果就會在嵌套循環中帶來更多的循環次數,這種情況小表驅動大表反而是低效率了。
所以,驅動表是由結果集的數據量來決定的:
- 指定了連接條件時,滿足查詢條件的記錄行數少的表為驅動表
- 未指定連接條件時,行數少的表為驅動表
所以,准確地說,要想效率高,是要以小結果集驅動大的結果集。
2.2 EXPLAIN
那么,如何知道SQL優化后是如何執行SQL查詢順序的呢?這就要使用到MySQL中的關鍵字
EXPLAIN了。命令主要作用是輸出MySQL的優化器對SQL的執行計划,即MySQL會解釋如何處理輸入的SQL(是否使用索引,使用哪個索引,多表以什么順序及什么關聯字段做JOIN)
我們說想要SQL執行效率高,就要以小結果集驅動大結果集,而EXPLAIN的提示就可以幫助我們確認SQL執行時優化器是否會以合理的順序來JOIN多張表。
EXPLAIN的使用很簡單,直接加在SELECT之前即可,它不會真正去執行SQL,只是做分析處理。如下:
EXPLAIN
SELECT *
FROM
(SELECT * from t_rank AS r JOIN csic_delegation_dict AS dele ON r.commonCode_Delegation = dele.DELEGATION_CODE) tmp1
JOIN csic_event AS eve ON tmp1.commonCode_Event = eve.EVENT
5
1
EXPLAIN
2
SELECT *
3
FROM
4
(SELECT * from t_rank AS r JOIN csic_delegation_dict AS dele ON r.commonCode_Delegation = dele.DELEGATION_CODE) tmp1
5
JOIN csic_event AS eve ON tmp1.commonCode_Event = eve.EVENT
EXPLAIN命令會為SQL中出現的每張表返回一行信息來說明數據庫優化器將會如何操作這張表,返回的信息以表呈現,共有10個字段,如下示例:
+----+-------------+------------+--------+-------------------+---------+---------+------+------+-----------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+------------+--------+-------------------+---------+---------+------+------+-----------+
| 1 | PRIMARY | eve | ALL | NULL | NULL | NULL | NULL | 441 | |
| 1 | PRIMARY | <derived2> | ALL | NULL | NULL | NULL | NULL | 504 |Using where|
| 2 | DERIVED | dele | ALL | NULL | NULL | NULL | NULL | 41 | |
| 2 | DERIVED | r | ALL | NULL | NULL | NULL | NULL | 539 |Using where|
+----+-------------+------------+--------+-------------------+---------+---------+------+------+-----------+
8
1
+----+-------------+------------+--------+-------------------+---------+---------+------+------+-----------+
2
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
3
+----+-------------+------------+--------+-------------------+---------+---------+------+------+-----------+
4
| 1 | PRIMARY | eve | ALL | NULL | NULL | NULL | NULL | 441 | |
5
| 1 | PRIMARY | <derived2> | ALL | NULL | NULL | NULL | NULL | 504 |Using where|
6
| 2 | DERIVED | dele | ALL | NULL | NULL | NULL | NULL | 41 | |
7
| 2 | DERIVED | r | ALL | NULL | NULL | NULL | NULL | 539 |Using where|
8
+----+-------------+------------+--------+-------------------+---------+---------+------+------+-----------+
下面對這些字段做個簡單的說明(更多詳情可以參考
官方文檔):
2.2.1 id
SELECT語句的標識字段,若SQL中只有1個SELECT語句,則該值為1,否則依次遞增;若SQL是UNION的結果,則該值為NULL。
值得一提的是,官方文檔中並沒有提到在多個SELECT語句時,即id有多個不同值時,哪個先執行,哪個后執行。那么如何去認識這個順序呢?結合網友和一些簡單測試的判斷看來,大概是這樣的:
- id值較大的,執行優先級較高,且從上到下執行,且id值最大的組中,第一行為驅動表,如上圖的table dele
- id值相同時,認為是一組,執行順序從上到下
當然,這可能多少有不嚴謹的地方,只能以后在使用過程中再根據實際場景去做進一步的判別了。
先留個坑吧。
2.2.2 select_type
該字段用於說明SELECT語句的類型:
該字段的值 | 含義 |
SIMPLE | 簡單的SELECT,不適用UNION或子查詢等 |
PRIMARY | 查詢中包含任何復雜的子部分,最外層的SELECT標記為PRIMARY |
UNION | UNION中的第二個或后面的SELECT語句 |
DEPENDENT UNION | UNION中的第二個或后面的SELECT語句,取決於外面的查詢 |
UNION RESULT | UNION的結果 |
SUBQUERY | 子查詢中的第一個SELECT |
DEPENDENT SUBQUERY | 子查詢中的第一個SELECT,取決於外面的查詢 |
DERIVED | 派生表的SELECT,FROM子句的子查詢 |
UNCACHEABLE SUBQUERY | 一個子查詢的結果不能被緩存,必須重新評估外鏈接的第一行 |
2.2.3 table
用於表示數據集來自哪張表,其值一般是表名,但:
- 當數據集市UNION的結果時,其值可能是<UNION M,N>,這里的M或N是id字段的值
- 當數據集來自派生表的SELECT,則顯示的是derived*,這里的*是id字段的值,如:
mysql> EXPLAIN SELECT * FROM (SELECT * FROM ( SELECT * FROM t1 WHERE id=2602) a) b;
+----+-------------+------------+--------+-------------------+---------+---------+------+------+-------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+------------+--------+-------------------+---------+---------+------+------+-------+
| 1 | PRIMARY | <derived2> | system | NULL | NULL | NULL | NULL | 1 | |
| 2 | DERIVED | <derived3> | system | NULL | NULL | NULL | NULL | 1 | |
| 3 | DERIVED | t1 | const | PRIMARY,idx_t1_id | PRIMARY | 4 | | 1 | |
+----+-------------+------------+--------+-------------------+---------+---------+------+------+-------+
8
1
mysql> EXPLAIN SELECT * FROM (SELECT * FROM ( SELECT * FROM t1 WHERE id=2602) a) b;
2
+----+-------------+------------+--------+-------------------+---------+---------+------+------+-------+
3
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
4
+----+-------------+------------+--------+-------------------+---------+---------+------+------+-------+
5
| 1 | PRIMARY | <derived2> | system | NULL | NULL | NULL | NULL | 1 | |
6
| 2 | DERIVED | <derived3> | system | NULL | NULL | NULL | NULL | 1 | |
7
| 3 | DERIVED | t1 | const | PRIMARY,idx_t1_id | PRIMARY | 4 | | 1 | |
8
+----+-------------+------------+--------+-------------------+---------+---------+------+------+-------+
2.2.4 type
該字段表示MySQL在表中找到所需行的方式,又稱“訪問類型”,常見的有:
該字段的值 | 含義 |
ALL | 遍歷全表 |
index | 與ALL的區別在於只遍歷索引樹 |
range | 表示只操作單表,且符合查詢條件的記錄不止1條 |
ref | 表明本步執行計划操作的數據集中關聯字段是索引字段,但不止1條記錄符合上步執行計划操作的數據集的關聯條件 |
eq_ref | 表明本步執行計划操作的數據集中關聯字段是索引字段,且只有1條記錄符合上步執行計划操作的數據集的關聯條件 |
const | 表明上述"table"字段代表的數據集中,最多只有1行記錄命中本步執行計划的查詢條件 |
system | system只是const值的一個特例,它表示本步執行計划要操作的數據集中只有1行記錄 |
2.2.5 possible_keys
該字段的值是可能被MySQL用作索引的字段,若值為NULL,則沒有字段會被用作索引,因此查詢效率不會高,這種情況下,需要優化數據表的索引結構。
2.2.6 key
該字段的值是MySQL真正用到的索引。
2.2.7 key_len
該字段的值表明上述key字段的length,當MySQL將某聯合索引字段作為SQL執行時用到的索引時,key_len字段可以暗示MySQL真正在什么程度上(多長的最左前綴匹配字段)使用了該聯合索引。若key字段的值為NULL,則key_len字段值也為NULL。
2.2.8 ref
該字段的值表明數據表中的哪列或哪個constants會被用於與key字段指定的索引做比較。
2.2.9 rows
該字段的值表明MySQL執行該步計划對應的查詢時掃描的行數,該值是估算值,不完全准確。這個值對於SQL優化非常具有參考意義,通常情況下,
該值越小查詢效率越高。
2.2.10 Extra
該字段的值包含了MySQL執行query時的其它額外信息。常見如下(查詢效率由高到低):
該字段的值 |
含義 |
Using index | 表示使用索引,如果只有 Using index,說明他沒有查詢到數據表,只用索引表就完成了這個查詢,這個叫覆蓋索引。如果同時出現Using where,代表使用索引來查找讀取記錄, 也是可以用到索引的,但是需要查詢到數據表。 |
Using where | 表示條件查詢,如果不讀取表的所有數據,或不是僅僅通過索引就可以獲取所有需要的數據,則會出現 Using where。如果type列是ALL或index,而沒有出現該信息,則你有可能在執行錯誤的查詢:返回所有數據。 |
Using filesort | 不是“使用文件索引”的含義!filesort是MySQL所實現的一種排序策略,通常在使用到排序語句ORDER BY的時候,會出現該信息。 |
Using temporary | 表示為了得到結果,使用了臨時表,這通常是出現在多表聯合查詢,結果排序的場合。 |
3、STRAIGHT_JOIN
由上我們已經知道,EXPLAIN的提示可以幫助我們意識到哪些字段應該建索引,也可以幫我們確認SQL執行時,數據庫優化器是否會以合理的順序來JOIN多張表。
但有時候並不是說數據庫優化器做的執行順序就是最優的,而按照我們的FROM表的順序執行才是最優的,那么問題來了:
如果想讓優化器以FROM語句列出的表順序做JOIN,怎么辦?
這里就要用到 STRAIGHT_JOIN 關鍵字了,將其加在SELECT關鍵字之后,用來告訴優化器按照FROM列出的表順序進行JOIN。
舉個例子,我有某個SQL如果按照優化器的執行順序執行,則:

SQL查詢時間為9s:

使用STRAIGHT_JOIN關鍵字,要求按照FROM表順序進行執行,則:
SQL查詢時間為0.2s:
但是仍然需要引起注意的是,這種方式因為執行順序被固化了,那么隨着時間的推移,數據庫中的數據分布隨着業務開展而發生變化,很可能導致原本運行順暢的SQL逐漸變得糟糕。


另外,測試時為了保證結果的正確性,應避免查詢緩存,需要每次執行前手動清除:
RESET QUERY CACHE;
1
RESET QUERY CACHE;
4、參考鏈接
附件列表