1. 程式人生 > 實用技巧 >MySQL 8.0 中的 anti-join

MySQL 8.0 中的 anti-join

MySQL 8.0.17版本引入了一個antijoin的優化,這個優化能夠將where條件中的not in(subquery), not exists(subquery),in(subquery) is not true,exists(subquery) is not true,在內部轉化成一個antijoin(反連線),以便移除裡面的子查詢subquery,這個優化在某些場景下,能夠將效能提升20%左右。

原文地址:

https://mytecdb.com/blogDetail.php?id=108

1. antijoin適用場景

antijoin適用的場景案例通常如下:

  1. 找出在集合A且不在集合B中的資料
  2. 找出在當前季度裡沒有購買商品的客戶
  3. 找出今年沒有通過考試的學生
  4. 找出過去3年,某個醫生的病人中沒有進行醫學檢查的部分

上面這些場景,以第4個為例,轉換成一個SQL,通常如下:

select * from patients where not exists(
    select * from exams where  
    exams.type='check-up' and 
    exams.date>=date_sub(now(), interval 3 year) and 
    exams.patient_id=patients.patient_id
);

如果SQL按照這種形式去寫,通常沒有太多的優化空間。我們需要從patients表讀取每條記錄,對於每條記錄,帶到子查詢中,檢查是否滿足條件。因為子查詢的where子句依賴patients.patient_id,patients 表的每一條記錄遍歷,都會導致子查詢被重複執行,嚴重影響效能。

優化這種SQL的第一步就是打破上層查詢與子查詢之間的邊界,將後者也就是子查詢合併到上層查詢裡面。

看一下優化之後的SQL,如下:

select * from patients antijoin exams on 
    exams.type='check-up' and 
    exams.date>=date_sub(now(), interval 3 year) and 
    patients.patient_id=exams.patient_id;

上面的SQL使用了關鍵字antijoin,實際上這個關鍵字是不存在的,或者說它只在MySQL內部使用,使用者並不知道它,它和join操作類似,join通常尋找滿足匹配條件的記錄,而antijoin尋找不匹配的記錄。更準確地說,它從左邊表選擇記錄,然後檢查右邊表,根據on條件,檢查是否沒有記錄能夠匹配上,如果沒有記錄匹配,那麼左邊的這條記錄就可以作為結果返回。

檢視SQL的執行計劃,加上 format=tree,就能看到執行計劃中使用了antijoin,如下:

 -> Nested loop anti-join
    -> Table scan on tb1  (cost=850482.22 rows=8372128)
    -> Single-row index lookup on <subquery2> using <auto_distinct_key> (id=tb1.id)
        -> Materialize with deduplication
            -> Index scan on tb2 using PRIMARY  (cost=1.85 rows=16)

2. antijoin內部優化策略

MySQL有兩種策略用於執行antijoin。

  • First Match
  • Materialization
2.1 First Match策略

First Match 策略,從patients表中讀取一條記錄,然後在exams表中尋找匹配,如果沒有匹配上,則將這條記錄作為結果返回。這種方式與使用子查詢並沒有太大的區別。

2.2 Materialization策略

Materialization策略,對於上述SQL例子,ON子句有3個子條件,分別是

exams.type='check-up'
exams.date>=date_sub(now(), interval 3 year)
patients.patient_id=exams.patient_id

3個條件中只有一個依賴patients表,所以MySQL可以建立一個臨時表tmp,這個臨時表由exams表按照前兩個條件過濾後的資料組成,就像下面這樣:

create table tmp 
    select patient_id from exams where 
    exams.type='check-up' and 
    exams.date>=date_sub(now(), interval 3 year);

MySQL優化器會自動在tmp的patient_id欄位上新增索引,然後從patients表讀取記錄,使用索引匹配tmp表中的記錄,如果沒有匹配上,則返回這條記錄。

相對於First Match策略,Materialization策略主要有以下優勢:

  1. exams表只讀取一次,用於建立tmp表。
  2. tmp表相對於exams表,有更少的記錄,訪問tmp表要比exams表更快。
  3. tmp表上由於建立了索引,訪問起來更快,而原始表exams很可能沒有索引。

當然,這種策略建立臨時表,也有一些前期的成本消耗,比如需要申請記憶體來儲存臨時表資料,比如臨時表非常大,需要將臨時表儲存在磁碟上。因此兩種策略哪一種更好,依賴於具體的場景。幸運的是,MySQL有一個基於成本的優化器,通過計算兩種策略基於資料行數的成本,條件的選擇性,索引的使用,最終選擇成本最低的那個策略。

3. 總結

  • antijoin優化適用於not exists(subquery), not in(subquery)這類查詢,在某些場景下,能夠對這類SQL進行很好的優化和效能提高。
  • antijoin有兩種執行策略,First Match和Materialization,優化器根據成本模型進行選擇。