1. 程式人生 > 其它 >mysql-left join的坑和優化經驗

mysql-left join的坑和優化經驗

參考文章:https://blog.csdn.net/weixin_39980841/article/details/110807850

CREATE TABLE classes (
`id` INT(11) NOT NULL PRIMARY KEY,
`name` VARCHAR(32) NOT NULL
)

INSERT INTO classes (`id`, `name`) VALUES
(1, '一班'),
(2, '二班'),
(3, '三班'),
(4, '四班')


CREATE TABLE students (
`id` INT(11) NOT NULL PRIMARY KEY AUTO_INCREMENT,
`class_id` INT(11) NOT NULL,
`name` VARCHAR(32),
`gender` VARCHAR(1)
)

INSERT INTO students(
`class_id`,
`name`,
`gender`
)
VALUES
(1, '小明', 'M'),
(1, '小紅', 'F'),
(1, '小軍', 'M'),
(1, '小米', 'F'),
(2, '小白', 'M'),
(2, '小兵', 'F'),
(2, '小林', 'F'),
(3, '小新', 'F'),
(3, '小王', 'M'),
(3, '小麗', 'F')

根源

mysql 對於left join的採用類似巢狀迴圈的方式來進行從處理,以下面的語句為例:

SELECT * FROM LT LEFT JOIN RT ON P1(LT,RT)) WHERE P2(LT,RT)

其中P1是on過濾條件,缺失則認為是TRUE,P2是where過濾條件,缺失也認為是TRUE,該語句的執行邏輯可以描述為:

FOR each row lt in LT {// 遍歷左表的每一行
  BOOL b = FALSE;
  FOR each row rt in RT such that P1(lt, rt) {// 遍歷右表每一行,找到滿足join條件的行
    IF P2(lt, rt) {//滿足 where 過濾條件
      t:=lt||rt;//合併行,輸出該行
    }
    b=TRUE;// lt在RT中有對應的行
  }
  IF (!b) { // 遍歷完RT,發現lt在RT中沒有有對應的行,則嘗試用null補一行
    IF P2(lt,NULL) {// 補上null後滿足 where 過濾條件
      t:=lt||NULL; // 輸出lt和null補上的行
    }         
  }
}

 

當然,實際情況中MySQL會使用buffer的方式進行優化,減少行比較次數,不過這不影響關鍵的執行流程,不在本文討論範圍之內。

從這個虛擬碼中,我們可以看出兩點:

  • 如果想對右表進行限制,則一定要在on條件中進行,若在where中進行則可能導致資料缺失,導致左表在右表中無匹配行的行在最終結果中不出現,違背了我們對left join的理解。因為對左表無右表匹配行的行而言,遍歷右表後b=FALSE,所以會嘗試用NULL補齊右表,但是此時我們的P2對右錶行進行了限制,NULL若不滿足P2(NULL一般都不會滿足限制條件,除非IS NULL這種),則不會加入最終的結果中,導致結果缺失。
  • 如果沒有where條件,無論on條件對左表進行怎樣的限制,左表的每一行都至少會有一行的合成結果,對左錶行而言,若右表若沒有對應的行,則右表遍歷結束後b=FALSE,會用一行NULL來生成資料,而這個資料是多餘的。所以對左表進行過濾必須用where。

下面展開兩個需求的錯誤語句的執行結果和錯誤原因:

需求1

需求2

需求1由於在where條件中對右表限制,導致資料缺失(四班應該有個為0的結果)

正確的slq應該為

SELECT c.name, COUNT(s.name) AS num 
    FROM classes c LEFT JOIN students s 
    ON (s.class_id = c.id AND s.gender = 'F')
    GROUP BY c.name

  

需求2由於在on條件中對左表限制,導致資料多餘(其他班的結果也出來了,還是錯的)

總結

通過上面的問題現象和分析,可以得出了結論:在left join語句中,左表過濾必須放where條件中,右表過濾必須放on條件中,這樣結果才能不多不少,剛剛好。

SQL 看似簡單,其實也有很多細節原理在裡面,一個小小的混淆就會造成結果與預期不符,所以平時要注意這些細節原理,避免關鍵時候出錯。

========================

根據經驗。我更喜歡直接用where組合成為虛擬表,這樣虛擬表能直接使用到索引,也能較少笛卡爾乘積,使得大表的查詢效率大大提高,也絕不會發生錯誤。

優化後的sql為

SELECT c.name, COUNT(s.name) AS num 
    FROM classes c LEFT JOIN (SELECT * FROM students s WHERE s.gender = 'F') AS s
    ON s.class_id = c.id
    GROUP BY c.name