網絡上有大量的資料說起將 IN 改爲 JOIN 或者 exist,而後修改完成以後確實變快了,但是爲何會變快呢?IN、EXIST、JOIN 在 MySQL 中的實現邏輯如何理解呢?本文也是比較粗淺的作一些介紹,知道了 MySQL 的大概執行邏輯,也方便理解。本文絕大多數內容來自:高性能MySQL第三版(O'Reilly.High.Performance.MySQL.3rd.Edition.M),還有一部分來自於網絡,還有的來自於本身的理解,如下的內容有引用的都會作標準,若有雷同,純屬巧合。html
例若有以下的 IN 查詢:java
SELECT * FROM tbl1 WHERE col3 IN ( SELECT col3 FROM tbl2 )
若是子查詢 select id from t2
數據量比較大的狀況下,則會很慢,從網絡找找答案,就知道每每是建議修改成:mysql
SELECT * FROM tbl1 WHERE EXISTS ( SELECT 1 FROM tbl2 WHERE tbl1.col3 = tbl2.col3 )
或者改爲 INNER JOIN 形式:sql
SELECT * FROM tbl1 INNER JOIN tbl2 ON tbl1.col3 = tbl2.col3
確實這兩種優化是可行的。不過整體來講更推薦 INNER JOIN,下面章節也會說起。網絡
一下內容摘抄自 高性能MySQL第三版(O'Reilly.High.Performance.MySQL.3rd.Edition.M),文章目錄:Query Performance Optimization-->Query Execution Basics-->The Query optimizer Process-->MySQL's join execution strategyapp
簡單的 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
ui
簡單的例子:rest
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 差很少。
沒可以找到僞代碼,我的以爲應該執行邏輯和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。
從官網與知名書籍中找到了以下的信息。從官網文檔:C.4 Restrictions on Subqueries,有以下的文字:
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第三版(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 查詢:
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 查詢:
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 查詢:
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