下面這些sql都含有子查詢:
mysql> select * from t1 where a in (select a from t2); mysql> select * from (select * from t1) as t;
按返回的結果集區分子查詢
1、標量子查詢
那些只返回一個單一值的子查詢稱之為標量子查詢。比如:
select * from t1 where a in (select max(a) from t2);
2、行子查詢
返回一條記錄的子查詢,不過這條記錄需要包含多個列。比如:
select * from t1 where (a, b) = (select a, b from t2 limit 1);
3、列子查詢
返回一個列的數據的子查詢,包含多條記錄。比如:
select * from t1 where a in (select a from t2);
4、表子查詢
子查詢的結果既包含很多條記錄,又包含很多個列。比如:
select * from t1 where (a, b) in (select a,b from t2);
按與外層查詢關系來區分子查詢
1、相關子查詢
如果子查詢的執行需要依賴於外層查詢的值,我們就可以把這個子查詢稱之為相關子查詢。比如:
select * from t1 where a in (select a from t2 where t1.a = t2.a);
2、不相關子查詢
如果子查詢可以單獨運行出結果,而不依賴於外層查詢的值,我們就可以把這個子查詢稱之為不相關子查詢。前邊介紹的那些子查詢全部都可以看作不相關子查。
子查詢在MySQL中是怎么執行的
1、對於不相關標量子查詢或者行子查詢
比如:select * from t1 where a = (select a from t2 limit 1);
它的執行步驟是:
1)執行select a from t2 limit 1這個子查詢。
2)然后在將上一步子查詢得到的結果當作外層查詢的參數再執行外層查詢select * from t1 where a = …;
2、對於相關標量子查詢或者行子查詢
比如:select * from t1 where b = (select b from t2 where t1.a = t2.a limit 1);
它的執行步驟是:
1)先從外層查詢中獲取一條記錄,本例中也就是先從t1表中獲取一條記錄。
2)然后從上一步驟中獲取的那條記錄中找出子查詢中涉及到的值,本例中就是從t1表中獲取的那條記錄中找出t1.a列的值,然后執行子查詢。
3)最后根據子查詢的查詢結果來檢測外層查詢WHERE子句的條件是否成立,如果成立,就把外層查詢的那條記錄加入到結果集,否則就丟棄。
4)再次執行第一步,獲取第二條外層查詢中的記錄,依次類推。。。
3、IN子查詢優化
mysql對IN子查詢進行了優化。
比如:select * from t1 where a in (select a from t2);
對於不相關的IN子查詢來說,如果子查詢的結果集中的記錄條數很少,那么把子查詢和外層查詢分別看成兩個單獨的單表查詢效率還是蠻高的,但是如果單獨執行子查詢后的結果集太多的話,就會導致這些問題:
• 結果集太多,可能內存中都放不下
• 對於外層查詢來說,如果子查詢的結果集太多,那就意味着IN子句中的參數特別多,這會導致:
• 無法有效的使用索引,只能對外層查詢進行全表掃描。
• 在對外層查詢執行全表掃描時,由於IN子句中的參數太多,這會導致檢測一條記錄是否符合和IN子句中的參數匹配花費的時間太長
在mysql中,不直接將不相關子查詢的結果集當作外層查詢的參數,而是將該結果集寫入一個臨時表里。寫入臨時表的過程是這樣的:
1)該臨時表的列就是子查詢結果集中的列。
2)寫入臨時表的記錄會被去重。IN語句是判斷某個操作數在不在某個集合中,集合中的值重不重復對整個IN語句的結果並不影響,所以我們在將結果集寫入臨時表時對記錄進行去重可以讓臨時表變得更小。臨時表也是個表,只要為表中記錄的所有列建立主鍵或者唯一索引就可以進行去重。
3)一般情況下子查詢結果集不會特別大,所以會為它建立基於內存的使用Memory存儲引擎的臨時表,而且會為該表建立哈希索引。IN語句的本質就是判斷某個操作數在不在某個集合里,如果集合中的數據建立了哈希索引,那么這個匹配的過程就是很快的。
4)如果子查詢的結果集非常大,超過了系統變量tmp_table_size或者max_heap_table_size,臨時表會轉而使用基於磁盤的存儲引擎來保存結果集中的記錄,索引類型也對應轉變為B+樹索引。
這個將子查詢結果集中的記錄保存到臨時表的過程稱之為物化(Materialize)。那個存儲子查詢結果集的臨時表稱之為物化表。正因為物化表中的記錄都建立了索引(基於內存的物化表有哈希索引,基於磁盤的有B+樹
索引),通過索引執行IN語句判斷某個操作數在不在子查詢結果集中變得非常快,從而提升了子查詢語句的性能。
還是對於上面的那個sql:
mysql> select * from t1 where a in (select a from t2);
當我們把子查詢進行物化之后,假設子查詢物化表的名稱為materialized_table,該物化表存儲的子查詢結果集的列為m_val,那么這個查詢其實可以從下邊兩種角度來看待:
• 從表t1的角度來看待,整個查詢的意思其實是:對於t1表中的每條記錄來說,如果該記錄的a列的值在子查詢對應的物化表中,則該記錄會被加入最終的結果集。
• 從子查詢物化表的角度來看待,整個查詢的意思其實是:對於子查詢物化表的每個值來說,如果能在t1表中找到對應的a列的值與該值相等的記錄,那么就把這些記錄加入到最終的結果集。
也就是說其實上邊的查詢就相當於表t1和子查詢物化表materialized_table進行內連接:
select * from t1 inner join materialized_table on t1.a = m_val;
轉化成內連接之后,查詢優化器就可以評估不同連接順序需要的成本是多少,選取成本最低的那種查詢方式執行查詢。
雖然將子查詢進行物化之后再執行查詢會有建立臨時表的成本,但是可以將子查詢轉換為JOIN還是會更有效率一點的。那能不能不進行物化操作直接把子查詢轉換為連接呢。
我們對比下面兩個sql:
select * from t1 where a in (select a from t2); select t1.* from t1 inner join t2 on t1.a = t2.a;
這兩個sql的查詢結果其實很像,只是說對於第二個sql的結果集沒有去重,所以IN子查詢和兩表連接之間並不完全等價,但是將子查詢轉換為連接又真的可以充分發揮優化器的作用,所以MySQL提出了一個新概念半連接(semi-join),將t1表和t2表進行半連接的意思就是:對於t1表的某條記錄來說,我們只關心在t2表中是否存在與之匹配的記錄是否存在,而不關心具體有多少條記錄與之匹配,最終的結果集中只保留t1表的記錄。semi-join只是在MySQL內部采用的一種執行子查詢的方式,MySQL並沒有提供面向用戶的semi-join語法 。
那么怎么實現semi-join呢?
(1)Table pullout (子查詢中的表上拉)
當子查詢的查詢列表處只有主鍵或者唯一索引列時,可以直接把子查詢中的表上拉到外層查詢的FROM子句中,並把子查詢中的搜索條件合並到外層查詢的搜索條件中。
比如:select * from t1 where a in (select a from t2 where t2.b = 1); – a是主鍵
我們可以直接把t2表上拉到外層查詢的FROM子句中,並且把子查詢中的搜索條件合並到外層查詢的搜索條件中,上拉之后的查詢就是這樣的:
select * from t1 inner join t2 on t1.a = t2.a where t2.b = 1; -– a是主鍵
(2)DuplicateWeedout execution strategy (重復值消除)
對於這個查詢來說:
select * from t1 where a in (select e from t2 where t2.b = 1); – e只是一個普通字段
轉換為半連接查詢后,t1表中的某條記錄可能在t2表中有多條匹配的記錄,所以該條記錄可能多次被添加到最后的結果集中,為了消除重復,我們可以建立一個臨時表,比方說這個臨時表長這樣:
CREATE TABLE tmp ( id PRIMARY KEY );
這樣在執行連接查詢的過程中,每當某條t1表中的記錄要加入結果集時,就首先把這條記錄的主鍵值加入到這個臨時表里,如果添加成功,說明之前這條t1表中的記錄並沒有加入最終的結果集,現在把該記錄添加到最終的結果集;如果添加失敗,說明這條之前這條t1表中的記錄已經加入過最終的結果集,這里直接把它丟棄就好了,這種使用臨時表消除semi-join結果集中的重復值的方式稱之為DuplicateWeedout。
(3)FirstMatch execution strategy (首次匹配)
FirstMatch是一種最原始的半連接執行方式,就是我們最開始的思路,先取一條外層查詢的中的記錄,然后到子查詢的表中尋找符合匹配條件的記錄,如果能找到一條,則將該外層查詢的記錄放入最終的結果集並且停止查找更多匹配的記錄,如果找不到則把該外層查詢的記錄丟棄掉;然后再開始取下一條外層查詢中的記錄,重復上邊這個過程。
(4)LooseScan(松散索引掃描)
子查詢掃描了非唯一索引,因為是非唯一索引,所以可能有相同的值,可以利用索引去重。
對於某些使用IN語句的相關子查詢,比方這個查詢:
select * from t1 where a in (select b from t2 where t1.b = t2.b);
它可以轉換為半連接:
select * from t1 semi join t2 on t1.a = t2.a and t1.b = t2.b;
如一下幾種情況就不能轉換為semi-join:
• 外層查詢的WHERE條件中有其他搜索條件與IN子查詢組成的布爾表達式使用OR連接起來
• 使用NOT IN而不是IN的情況
• 子查詢中包含GROUP BY、HAVING或者聚集函數的情況
• 子查詢中包含UNION的情況
那么對於不能轉為semi-join查詢的子查詢,有其他方式來進行優化:
• 對於不相關子查詢來說,可以嘗試把它們物化之后再參與查詢
比如對於使用了NOT IN下面這個sql:
select * from t1 where a not in (select a from t2 where t2.a = 1);
請注意這里將子查詢物化之后不能轉為和外層查詢的表的連接,因為用的是not in只能是先掃描t1表,然后對t1表的某條記錄來說,判斷該記錄的a值在不在物化表中。
• 不管子查詢是相關的還是不相關的,都可以把IN子查詢嘗試專為EXISTS子查詢
其實對於任意一個IN子查詢來說,都可以被轉為EXISTS子查詢,通用的例子如下:
outer_expr IN (SELECT inner_expr FROM … WHERE subquery_where)
可以被轉換為:
EXISTS (SELECT inner_expr FROM … WHERE subquery_where AND outer_expr=inner_expr)
這樣轉換的好處是,轉換前本來不能用到索引,但是轉換后可能就能用到索引了,比如:
select * from t1 where a in (select a from t2 where t2.e = t1.e);
這個sql里面的子查詢時用不到索引的,轉換后變為:
select * from t1 where exists (select 1 from t2 where t2.e = t1.e and t1.a = t2.a)
轉換之后t2表就能用到a字段的索引了。
所以,如果IN子查詢不滿足轉換為semi-join的條件,又不能轉換為物化表或者轉換為物化表的成本太大,那么它就會被轉換為EXISTS查詢。
對於派生表的優化
select * from (select a, b from t1) as t;
上面這個sql,子查詢是放在from后面的,這個子查詢的結果相當於一個派生表,表的名稱是t,有a,b兩個字段。
對於派生表,有兩種執行方式:
(一)把派生表物化
我們可以將派生表的結果集寫到一個內部的臨時表中,然后就把這個物化表當作普通表一樣參與查詢。當然,在對派生表進行物化時,使用了一種稱為延遲物化的策略,也就是在查詢中真正使用到派生表時才回去嘗試物化派生表,而不是還沒開始執行查詢就把派生表物化掉。比如:
select * from (select * from t1 where a = 1) as derived1 inner join t2 on derived1.a = t2.a where t2.a =10;
如果采用物化派生表的方式來執行這個查詢的話,那么執行時首先會到t1表中找出滿足t1.a = 10的記錄,如果找不到,說明參與連接的t1表記錄就是空的,所以整個查詢的結果集就是空的,所以也就沒有必要去物化查詢中的派生表了。
(二)將派生表和外層的表合並,也就是將查詢重寫為沒有派生表的形式
比如下面這個sql:
select * from (select * from t1 where a = 1) as t;
和下面的sql是等價的:
select * from t1 where a = 1;
再看一些復雜一點的sql:
select * from (select * from t1 where a = 1) as t inner join t2 on t.a = t2.a where t2.b = 1;
我們可以將派生表與外層查詢的表合並,然后將派生表中的搜索條件放到外層查詢的搜索條件中,就像下面這樣:
select * from t1 inner join t2 on t1.a = t2.a where t1.a = 1 and t2.b = 1;
這樣通過將外層查詢和派生表合並的方式成功的消除了派生表,也就意味着我們沒必要再付出創建和訪問臨時表的成本了。可是並不是所有帶有派生表的查詢都能被成功的和外層查詢合並,當派生表中有這些語句就不可以和外層查詢合並:
聚集函數,比如MAX()、MIN()、SUM()啥的
DISTINCT
GROUP BY
HAVING
LIMIT
UNION 或者 UNION ALL
派生表對應的子查詢的SELECT子句中含有另一個子查詢
所以MySQL在執行帶有派生表的時候,優先嘗試把派生表和外層查詢合並掉,如果不行的話,再把派生表物化掉執行查詢。