Mysql深度讲解 – 子查询优化
前言
上一篇【Mysql深度講解 – Join語句】詳細說了Join的相關內容,本篇會講一下子查詢如何優化。一般來說在正常的業務情況下大多數sql語句都會有子查詢,一個表從另一個表里查詢數據,本篇會通過幾個例子列舉這樣的sql語句的優化思路。更多Mysql調優內容請點擊【Mysql優化-深度講解系列目錄】。
子查詢優化的思路
Mysql中對于子查詢的優化其實會盡量優化為Join語句執行,下面這兩個sql都含有子查詢,大體上子查詢可以分為兩種。
select * from t1 where a in (select a from t2); -- 第一種,寫在where后面作為一個條件 select * from (select * from t1) as t; -- 第二種,寫在from后買你作為一個表按照返回類結果區分
標量子查詢
那些只返回一個單一值的子查詢稱之為標量子查詢。比如:
select * from t1 where a in (select max(a) from t2);
行子查詢
返回一條記錄的子查詢,不過這條記錄可能包含多個列。比如:
select * from t1 where (a, b) = (select a, b from t2 limit 1);
列子查詢
返回一個列的數據的子查詢,包含多行記錄。比如:
select * from t1 where a in (select a from t2);
表子查詢
子查詢的結果既包含很多條記錄,又包含很多個列。比如:
select * from t1 where (a, b) in (select a,b from t2);
按照外層查詢關系區分
相關子查詢
如果子查詢的執行需要依賴于外層查詢的值或者字段,我們就可以把這個子查詢稱之為相關子查詢。比如:
select * from t1 where a in (select a from t2 where t1.a = t2.a);
不相關子查詢
如果子查詢可以單獨運行出結果,而不依賴于外層查詢的值或者字段,我們就可以把這個子查詢稱之為不相關子查詢。前邊介紹的那些子查詢全部都可以看作不相關子查詢。
子查詢的執行過程
對于相關標量子查詢或者行子查詢,比如:
select * from t1 where b = (select b from t2 where t1.a = t2.a limit 1);
它的執行步驟是
對于不相關標量子查詢或者行子查詢,比如:
select * from t1 where a = (select a from t2 limit 1);
它的執行步驟是:
IN子查詢
對于標量查詢,一般來說由于數量被限制了,Mysql執行的相對比較快。但是對于不是標量的查詢,Mysql就必須做大量的工作去計算查詢成本。比如: select * from t1 where a in (select a from t2);
對于不相關的IN子查詢來說,如果子查詢的結果集中的記錄條數很少,那么把子查詢和外層查詢分別看成兩個單 獨的單表查詢效率還是蠻高的,但是如果單獨執行子查詢后的結果集太多的話,就會導致這些問題:
物化表
在mysql中,不直接將不相關子查詢的結果集當作外層查詢的參數,而是將該結果集寫入一個臨時表里。這個將子查詢結果集中的記錄保存到臨時表的過程稱之為物化Materialize。那個存儲子查詢結果集的臨時表稱之為物化表。正因為物化表中的記錄都建立了索引(基于內存的物化表有哈希索引,基于磁盤的有B+樹索引),通過索引執行IN語句判斷某個操作數在不在子查詢結果集中變得非常快,從而提升了子查詢語句的性能。寫入臨時表的過程是這樣的:
分析SQL
還是對于上面的那個sql:select * from t1 where a in (select a from t2);
當我們把子查詢進行物化之后,假設子查詢物化表的名稱為temp_table,該物化表存儲的子查詢結果集的列為temp_a,再此過程中還包含給列temp_a去重的步驟,那么這個查詢其實可以從下邊兩種角度來看待:
- 從表t1的角度來看待,整個查詢的意思其實是:對于t1表中的每條記錄來說,如果該記錄的a列的 值在子查詢對應的物化表中,則該記錄會被加入最終的結果集。
- 從子查詢物化表的角度來看待,整個查詢的意思其實是:對于子查詢物化表的每個值來說,如果能 在t1表中找到對應的a列的值與該值相等的記錄,那么就把這些記錄加入到最終的結果集。
也就是說其實上邊的查詢就相當于表t1和子查詢物化表temp_table進行內連接:
select * from t1 inner join temp_table on t1.a = temp_table. temp_a;
轉化成內連接之后,查詢優化器就可以評估不同連接順序需要的成本是多少,選取成本最低的那種查詢方式執行查詢。
半連接(semi-join)
雖然將子查詢進行物化之后再執行查詢會提高一些效率,但是建立臨時表也是需要成本的,如果可以將子查詢直接轉換為join豈不是更有效率。那可否繞過物化操作直接把子查詢轉換為連接呢,對此我們對比下這兩個sql:
select * from t1 where a in (select a from t2); select t1.* from t1 inner join t2 on t1.a = t2.a;如果說a是兩個表的主鍵,或者僅僅是t2表的主鍵,那么就不需要對某個列進行去重,建立物化表就很得不償失。因為理論上來說兩個sql的結果很像,只不過第二個sql沒有做去重,所以IN子查詢和兩表連接之間并不完全等價。但是為了充分發揮查詢優化器的作用,Mysql會對第二個sql進行半連接(semi-join)。
將t1表和t2表進行半連接的意思就是:對于t1表的某條記錄來說,我們只關心在t2表中是否存在 與之匹配的記錄是否存在,而不關心具體有多少條記錄與之匹配,最終的結果集中只保留t1表的記錄。要注意的是semi-join 只屬于MySQL內部采用的一種執行子查詢的方式,MySQL并沒有提供面向用戶的semi-join語法 。所以我們可以理解為Mysql為了優化,做了這樣一個事情:
select * from t1 where a in (select a from t2); -- 原sql select t1.* from t1 semi join t2 on t1.a = t2.a; -- Mysql執行時優化的結果半連接的內部實現
為了實現半連接Mysql也會根據不同的情況進行不同的處理,簡而言之就是去重。
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;
DuplicateWeedout Execution Strategy(重復值消除)
對于這種情況就是我們上面說的建立temp表的方法,凡是重復的值直接丟掉即可,只保留不重復的值。
FirstMath Execution Strategy(首次匹配)
這種辦法是取外層表的一條數據,然后到子表中查詢,找到的第一條符合條件的就放到結果集當中,并且停止匹配更多的記錄。如果沒有找到則拋棄當前外層記錄,繼續拿下一條外層記錄去子表里查找第一條符合的記錄,循環往復。
LooseScan(松散掃描)
這種方法回去掃描索引,當掃描到第一個可以匹配的索引的時候,就返回到結果集中。
半連接的適用條件
對于某些使用IN語句的相關相關子查詢,比方這個查詢:
select * from t1 where a in (select b from t2 where t1.b = t2.b); //可以轉換為半連接,然后再使用上面介紹的幾種semi-join實現方式來進行實現。 select * from t1 semi join t2 on t1.a = t2.a and t1.b = t2.b; -- 再次提醒: semi join這樣的語法是不存在的。但是注意:由于相關子查詢并不是一個獨立的查詢,所以不能轉換為物化表來執行查詢。 不是所有包含IN子查詢的查詢語句都可以轉換為semi-join,只有形如下面這樣的查詢才可以被轉換為semi-join:
SELECT ... FROM outer_tables WHERE expr IN (SELECT ... FROM inner_tables ...) AND ... //或者 SELECT ... FROM outer_tables WHERE (oe1, oe2, ...) IN (SELECT ie1, ie2, ... FROM inner_tables ...) AND ...如一下幾種情況就不能轉換為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查詢。
總結
本篇簡述了Mysql中的子查詢優化思路,以及Mysql查詢優化器針對sql語句中子查詢的優化原理。本篇中多數的內容其實都是通過查詢優化器和explain命令查看的,查詢優化器已經再以往的博客中詳細說過了,因此下一篇【Mysql深度講解 – explain關鍵字(一)】會詳細說下explain關鍵字的內容。
總結
以上是生活随笔為你收集整理的Mysql深度讲解 – 子查询优化的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 局域网分享本机文件
- 下一篇: 【已阅】日志与时间戳,客户端与服务器端,