為什麼 EXISTS(NOT EXIST) 與 JOIN(LEFT JOIN) 的效能會比 IN(NOT IN) 好
前言
網路上有大量的資料提及將 IN 改成 JOIN 或者 exist,然後修改完成之後確實變快了,可是為什麼會變快呢?IN、EXIST、JOIN 在 MySQL 中的實現邏輯如何理解呢?本文也是比較粗淺的做一些介紹,知道了 MySQL 的大概執行邏輯,也方便理解。本書絕大多數內容來自:高效能MySQL第三版(O'Reilly.High.Performance.MySQL.3rd.Edition.M),還有一部分來自於網路,還有的來自於自己的理解,以下的內容有引用的都會做標準,如有雷同,純屬巧合。
IN 改為 JOIN/EXIST
例如有如下的 IN 查詢:
SELECT * FROM tbl1 WHERE col3 IN ( SELECT col3 FROM tbl2 )
如果子查詢 select id from t2
資料量比較大的情況下,則會很慢,從網路找找答案,就知道往往是建議修改為:
SELECT
*
FROM
tbl1
WHERE
EXISTS ( SELECT 1 FROM tbl2 WHERE tbl1.col3 = tbl2.col3 )
或者改成 INNER JOIN 形式:
SELECT
*
FROM
tbl1
INNER JOIN tbl2 ON tbl1.col3 = tbl2.col3
確實這兩種優化是可行的。不過總體來說更推薦 INNER JOIN,下面章節也會提及。
MySQL JOIN 語法的執行邏輯
一下內容摘抄自 高效能MySQL第三版(O'Reilly.High.Performance.MySQL.3rd.Edition.M),文章目錄:Query Performance Optimization-->Query Execution Basics-->The Query optimizer Process-->MySQL's join execution strategy
INNER JOIN
簡單的 JOIN 例子:
SELECT tbl1.col1, tbl2.col2 FROM tbl1 INNER JOIN tbl2 USING ( col3 ) WHERE tbl1.col1 IN ( 5, 6 );
MySQL 執行的虛擬碼:
// WHERE tbl1.col1 IN ( 5, 6 ) 篩選出 tb11 符合條件的記錄
outer_iter = iterator over tbl1 where col1 IN(5,6)
outer_row = outer_iter.next
while outer_row
// 用 tb11 的 col3 去 tbl2 表中查詢,有索引將會非常快
inner_iter = iterator over tbl2 where col3 = outer_row.col3
inner_row = inner_iter.next
// 可能會命中多條資料
while inner_row
output [ outer_row.col1, inner_row.col2 ]
inner_row = inner_iter.next
end
outer_row = outer_iter.next
end
實際上就是兩個迴圈啦,從上面的程式碼可以大致瞭解到,為什麼等連線加了索引會很快,主要是因為加了索引,這條語句將走索引:inner_iter = iterator over tbl2 where col3 = outer_row.col3
LEFT JOIN
簡單的例子:
SELECT
tbl1.col1,
tbl2.col2
FROM
tbl1
LEFT OUTER JOIN tbl2 USING ( col3 )
WHERE
tbl1.col1 IN ( 5, 6 );
MySQL 執行的虛擬碼:
// WHERE tbl1.col1 IN ( 5, 6 ) 篩選出 tb11 符合條件的記錄
outer_iter = iterator over tbl1 where col1 IN(5,6)
outer_row = outer_iter.next
while outer_row
// 用 tb11 的 col3 去 tbl2 表中查詢,有索引將會非常快
inner_iter = iterator over tbl2 where col3 = outer_row.col3
inner_row = inner_iter.next
if inner_row
// 可能會命中多條資料
while inner_row
output [ outer_row.col1, inner_row.col2 ]
inner_row = inner_iter.next
end
else
// 沒有命中的則返回 NULL
output [ outer_row.col1, NULL ]
end
outer_row = outer_iter.next
end
和 INNER JOIN 差不多。
MySQL Exist 語法執行邏輯
沒能夠找到虛擬碼,個人覺得應該執行邏輯和JOIN是相似的。從 高效能MySQL第三版(O'Reilly.High.Performance.MySQL.3rd.Edition.M) 找到了 Exist 與 INNER JOIN 的使用場景,文章路徑:Chapter 6. Query Performance Optimization-->Limitations of the MySQL Query Optimizer-->Correlated Subqueries-->When a correlated subquery is good。
例如下面的 JOIN 語句:
SELECT DISTINCT
film.film_id
FROM
sakila.film INNER JOIN sakila.film_actor USING ( film_id );
需要對資料去重,這時候使用 EXISTS 會更合適,因為它的含義是 有一個匹配,所以平時使用的時候也得要小心,使用不當數據就被直接丟失了。改成如下的 EXISTS 語句,執行效率會更高:
SELECT
film_id
FROM
sakila.film
WHERE
EXISTS ( SELECT * FROM sakila.film_actor WHERE film.film_id = film_actor.film_id );
所以大多數時候可以使用 INNER JOIN,特別的場景使用 EXISTS。
MySQL IN 語法的執行邏輯
The reason for supporting row comparisons for
IN
but not for the others is thatIN
is implemented by rewriting it as a sequence of=
comparisons andAND
operations. This approach cannot be used forALL
,ANY
, orSOME
.
以及高效能MySQL第三版(O'Reilly.High.Performance.MySQL.3rd.Edition.M),文章目錄:Chapter 6. Query Performance Optimization-->The Query Optimization Process-->The Query optimizer-->IN() list comparisons 下有如下描述:
In many database servers, IN() is just a synonym for multiple OR clauses, because the two are logically equivalent. Not so in MySQL, which sorts the values in the IN() list and uses a fast binary search to see whether a value is in the list. This is O(log n) in the size of the list, whereas an equivalent series of OR clauses is O(n) in the size of the list (i.e., much slower for large lists).
所以呢,IN 查詢會被轉變為 OR 查詢,列子如下。
舉個栗子
有如下簡單的的 SQL:
SELECT
*
FROM
tbl1
WHERE
col3 IN (SELECT col3 FROM tbl2)
那麼經過 MySQL 會先執行 SELECT col3 FROM tbl2
,假設得到三個值 '1', '2',則會被會被處理為 OR
語句:
SELECT
*
FROM
tbl1
WHERE
col3 = '1'
OR col3 = '2'
而 OR
語句實際上又會被處理成 UNION ALL
,所以最後執行的語句類似於:
SELECT
*
FROM
tbl1
WHERE
col3 = '1'
UNION ALL
SELECT
*
FROM
tbl1
WHERE
col3 = '2'
因此,如果子查詢 SELECT col3 FROM tbl2
的資料量很大的話,MySQL 的解析可能比執行更耗費時間。(PS:多少資料量算多呢?這個我一直沒有找到答案,應該也是和MySQL的配置相關,所以才不會有一個定值,因此建議儘量使用 EXISTS 或者 JOIN)
MySQL 可能對IN查詢做的優化
書籍 高效能MySQL第三版(O'Reilly.High.Performance.MySQL.3rd.Edition.M) 有描述了 IN 查詢有可能會被MySQL內部優化為 EXISTS 查詢,文章路徑:Chapter 6. Query Performance Optimization-->Limitations of the MySQL Query Optimizer-->Correlated Subqueries。
語句一:比如一個IN查詢:
SELECT
*
FROM
sakila.film
WHERE
film_id IN ( SELECT film_id FROM sakila.film_actor WHERE actor_id = 1 );
語句二:有可能在實現的時候被分成了兩次查詢,先通過查詢得到 film_id 在通過 IN 查詢,如下所示:
SELECT GROUP_CONCAT(film_id) FROM sakila.film_actor WHERE actor_id = 1;
-- Result: 1,23,25,106,140,166,277,361,438,499,506,509,605,635,749,832,939,970,980
SELECT * FROM sakila.film
WHERE film_id
IN(1,23,25,106,140,166,277,361,438,499,506,509,605,635,749,832,939,970,980);
實際上呢,語句一MySQL會嘗試優化為 EXISTS 查詢,如下的語句,而語句二則沒辦法做更多的優化。應該是簡單的查詢可以直接優化,複雜的查詢是不能夠的,要不然平常直接寫IN語句,而不用專門改成 EXISTS 或者 INNER JOIN 語句。
SELECT
*
FROM
sakila.film
WHERE
EXISTS ( SELECT * FROM sakila.film_actor WHERE actor_id = 1 AND film_actor.film_id = film.film_id );
NOT IN 改成 NOT EXIST/LEFT JOIN
例如有如下的 NOT IN 查詢:
SELECT
*
FROM
tbl1
WHERE
col3 NOT IN ( SELECT col3 FROM tbl2 )
改成 NOT EXISTS 語法:
SELECT
*
FROM
tbl1
WHERE
NOT EXISTS ( SELECT 1 FROM tbl2 WHERE tbl1.col3 = tbl2.col3 )
改成 LEFT JOIN 語法:
SELECT
*
FROM
tbl1
LEFT JOIN tbl2 ON tbl1.col3 = tbl2.col3
WHERE
tbl2.col3 IS NULL
書籍 高效能MySQL第三版(O'Reilly.High.Performance.MySQL.3rd.Edition.M) 有描述了 NOT EXISTS 與 LEFT JOIN 的對比,文章路徑:Chapter 6. Query Performance Optimization-->Limitations of the MySQL Query Optimizer-->Correlated Subqueries-->When a correlated subquery is good。該部分對比了二者的執行計劃,實際上是相差無幾的。
NOT EXISTS 的執行計劃
NOT EXISTS 查詢:
EXPLAIN SELECT
film_id,
language_id
FROM
sakila.film
WHERE
NOT EXISTS ( SELECT * FROM sakila.film_actor WHERE film_actor.film_id = film.film_id ) \G
執行計劃輸出內容:
*************************** 1. row ***************************
id: 1
230 | Chapter 6: Query Performance Optimization
select_type: PRIMARY
table: film
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 951
Extra: Using where
*************************** 2. row ***************************
id: 2
select_type: DEPENDENT SUBQUERY
table: film_actor
type: ref
possible_keys: idx_fk_film_id
key: idx_fk_film_id
key_len: 2
ref: film.film_id
rows: 2
Extra: Using where; Using index
LEFT JOIN 執行計劃
LEFT JOIN 查詢:
EXPLAIN SELECT
film.film_id,
film.language_id
FROM
sakila.film
LEFT OUTER JOIN sakila.film_actor USING ( film_id )
WHERE
film_actor.film_id IS NULL \G
執行計劃輸出結果:
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: film
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 951
Extra:
*************************** 2. row ***************************
id: 1
select_type: SIMPLE
table: film_actor
type: ref
possible_keys: idx_fk_film_id
key: idx_fk_film_id
key_len: 2
ref: sakila.film.film_id
rows: 2
Extra: Using where; Using index; Not exists
二者相差無幾,LEFT JOIN 效能會略好一些,所以建議使用 LEFT JOIN。
歡迎轉載,但請註明本文連結,謝謝你。
2018.9.16 19:50