目錄
- 前言
- 序章:為何要有JOIN?——“一個好漢三個幫”的數據庫哲學 🤝
- 第一章:JOIN的“七十二變”——常見JOIN類型速覽 🎭
- 第二章:MySQL的“紅娘秘籍”——JOIN執行原理大揭秘 🕵??♀?📖
- 2.1 簡單嵌套循環連接 (Simple Nested Loop Join, SNLJ) - “老實人的笨辦法” 🐢
- 2.2 索引嵌套循環連接 (Index Nested Loop Join, INLJ) - “聰明人的快捷方式” 🚀
- 2.3 塊嵌套循環連接 (Block Nested Loop Join, BNL) - “批量相親,減少跑腿” 🚌
- 2.4 MySQL 8.0+ 的新貴:哈希連接 (Hash Join) 🧙?♂?
- 第三章:JOIN優化的“葵花寶典”——核心法則與實戰技巧 🚀📖
- 1. 索引!索引!還是TMD索引!(最重要的事說三遍) 🔑🔑🔑
- 2. 驅動表的選擇:“誰先動筷子”的藝術 🥢
- 3. 過濾條件要“給力”:盡可能早地減少結果集 📉
- 4. join_buffer_size:不是萬能丹,合理使用才有效 💊
- 5. EXPLAIN:你的JOIN優化“導航儀” 🗺?
- 6. MySQL 8.0+ 的其他JOIN優化特性 (錦上添花) 🌸
- 7. JOIN查詢的“七宗罪” (常見避坑指南) 🚫
- 第四章:實戰演練——看個例子壓壓驚 👨?🏫
- 總結:JOIN優化🧘?♂?
🌟我的其他文章也講解的比較有趣😁,如果喜歡博主的講解方式,可以多多支持一下,感謝🤗!
其他優質專欄: 【🎇SpringBoot】【🎉多線程】【🎨Redis】【?設計模式專欄(已完結)】…等
如果喜歡作者的講解方式,可以點贊收藏加關注,你的支持就是我的動力
?更多文章請看個人主頁: 碼熔burning
前言
大家好!又雙叒叕是我,你們的老朋友,一個幽默的程序員。
今天,咱們來點更刺激的,聊聊那個讓無數英雄競折腰的——JOIN
查詢優化!
你是不是也寫過那種“九九八十一難”般的JOIN語句,一執行,MySQL就跟便秘似的,半天憋不出一個P(結果)?或者,你看著EXPLAIN
那一堆眼花繚亂的Nested Loop
、Using join buffer
,感覺智商被按在地上摩擦?
別慌!JOIN雖然復雜,但它不是“愛情魔咒”,只要你摸清了它的“脾氣秉性”,掌握了正確的“撩妹技巧”(優化方法),它也能從“世紀大難題”變成你SQL工具箱里的“瑞士軍刀”!
準備好了嗎?系好安全帶,咱們的“JOIN優化探索號”飛船,馬上起航!目的地——高效JOIN的“幸福彼岸”!🚀💑
序章:為何要有JOIN?——“一個好漢三個幫”的數據庫哲學 🤝
想象一下,你的數據被精心設計,分門別類地存放在不同的“小抽屜”(表)里。比如:
students
表:存放學生的基本信息(學號、姓名、班級ID)。classes
表:存放班級信息(班級ID、班主任、教室)。scores
表:存放學生的考試成績(學號、科目、分數)。
現在,你想知道“火箭班所有學生的姓名及其各科成績”,單靠一個“抽屜”肯定搞不定吧?你得把students
和scores
這兩個抽屜打開,根據“學號”這個共同的線索,把它們關聯起來。
JOIN
,就是數據庫世界里的“聯誼會主持人”! 它的核心任務,就是根據你指定的“共同話題”(連接條件),把來自不同表(抽屜)的相關數據行“拉郎配”,組合成一個更完整、更有意義的結果集。
沒有JOIN
,數據就是一座座孤島;有了JOIN
,數據才能匯聚成汪洋大海,展現出真正的價值!
第一章:JOIN的“七十二變”——常見JOIN類型速覽 🎭
在MySQL的“聯誼會”上,主持人(JOIN)會根據你的要求,采用不同的“配對策略”。咱們先來快速認識一下幾位常見的“聯誼會司儀”:
-
INNER JOIN
(內連接):最嚴格的“司儀”,只介紹那些在兩個表里都能找到“共同話題”(匹配連接條件)的行。如果A表的某行在B表找不到伴兒,或者B表的某行在A表找不到伴兒,對不起,它倆都不能參加這場“內涵派對”。- 口頭禪:“寧缺毋濫,非誠勿擾!”
- 寫法:
SELECT ... FROM tableA INNER JOIN tableB ON tableA.col = tableB.col;
(或者直接SELECT ... FROM tableA, tableB WHERE tableA.col = tableB.col;
,效果類似,但推薦顯式JOIN
)
-
LEFT JOIN
(左連接,也叫LEFT OUTER JOIN
):偏心眼的“司儀”,以左邊的表(FROM
子句中先出現的表)為準。左表的每一行都會出現在結果中。- 如果右表有匹配的行,就正常配對。
- 如果右表沒有匹配的行,右表相關的列會用
NULL
來填充,“強行配對,找不到對象就給你個空氣伴侶”。 - 口頭禪:“左邊的都是爺,一個都不能少!右邊的?能配就配,配不上拉倒(用NULL)!”
- 寫法:
SELECT ... FROM tableA LEFT JOIN tableB ON tableA.col = tableB.col;
-
RIGHT JOIN
(右連接,也叫RIGHT OUTER JOIN
):跟LEFT JOIN
反過來,以右邊的表為準。- 口頭禪:“右邊的都是姑奶奶,全都得伺候好!左邊的?隨緣吧!”
- 小技巧:很多時候,
A RIGHT JOIN B
都可以改寫成B LEFT JOIN A
,效果一樣,但LEFT JOIN
更常用,可讀性可能更好。
-
FULL JOIN
(全連接,也叫FULL OUTER JOIN
):最大方的“司儀”,左邊右邊的客人一個都不落下!- 左表有匹配,右表有匹配:正常配對。
- 左表有,右表沒有:左表數據顯示,右表數據為
NULL
。 - 左表沒有,右表有:右表數據顯示,左表數據為
NULL
。 - 口頭禪:“來者都是客,一個都不能少!找不到伴兒的,我給你們發‘安慰獎’(NULL)!”
- MySQL的“小遺憾”:MySQL本身不直接支持
FULL OUTER JOIN
關鍵字。但別灰心,你可以通過LEFT JOIN ... UNION ... RIGHT JOIN
(或者LEFT JOIN ... UNION ALL ... RIGHT JOIN WHERE A.key IS NULL
等變體) 來模擬實現全連接的效果。-- 模擬FULL JOIN (注意,對于匹配上的行會顯示兩次,如果想去重用UNION) SELECT * FROM tableA LEFT JOIN tableB ON tableA.id = tableB.id UNION ALL -- 或者 UNION 去重 SELECT * FROM tableA RIGHT JOIN tableB ON tableA.id = tableB.id WHERE tableA.id IS NULL; -- 只取右表有而左表沒有的部分
-
CROSS JOIN
(交叉連接,也叫笛卡爾積):最“瘋狂”的“司儀”,不做任何篩選,把A表的每一行和B表的每一行都強行“拉郎配”一次。- 如果A表有M行,B表有N行,結果集就會有 M * N 行!數據量一大,分分鐘把你的數據庫搞“爆炸”!🤯
- 口頭禪:“管他三七二十一,全都給我配一遍!寧可錯殺一千,不可放過一個(潛在組合)!”
- 寫法:
SELECT ... FROM tableA CROSS JOIN tableB;
或者SELECT ... FROM tableA, tableB;
(不加任何WHERE
連接條件時) - 用途:正常業務中用得極少,除非你真的需要所有可能的組合(比如生成測試數據、某些特定的數學運算)。大多數情況下,如果你不小心寫出了笛卡爾積,那很可能是你忘了加
ON
或WHERE
連接條件了!
了解了這些“司儀”的性格,我們才能更好地指揮它們干活。
第二章:MySQL的“紅娘秘籍”——JOIN執行原理大揭秘 🕵??♀?📖
當MySQL收到一個JOIN請求后,它內部是怎么運作的呢?難道真的是挨個比較嗎?不完全是,它也有一套自己的“相親算法”。
在MySQL的早期版本以及很多情況下,JOIN操作的核心算法是嵌套循環連接 (Nested Loop Join, NLJ) 及其變種。
2.1 簡單嵌套循環連接 (Simple Nested Loop Join, SNLJ) - “老實人的笨辦法” 🐢
這是最原始、最容易理解,但也通常是最低效的一種。
-
算法描述:
- 選擇一個表作為“外層表”(也叫驅動表,Driving Table)。
- 遍歷外層表的每一行。
- 對于外層表的每一行,都去遍歷“內層表”(也叫被驅動表,Driven Table)的所有行,找到匹配的行,然后組合輸出。
-
偽代碼示意:
FOR each row R1 in OuterTable:FOR each row R2 in InnerTable:IF R1 joins with R2 ON join_condition:Output (R1, R2)
-
性能噩夢:如果外層表有M行,內層表有N行,那么總的比較次數大約是 M * N!如果內外層表都沒有索引,那每次在內層表查找都是全表掃描,I/O次數大約是
M + M*N
(外層掃一遍,內層掃M遍)。數據量一大,簡直是“龜速行駛”。 -
MySQL的“嫌棄”:由于SNLJ效率太低,現代MySQL優化器會極力避免使用它,除非萬不得已(比如連接條件極其復雜,沒有任何索引可用)。
2.2 索引嵌套循環連接 (Index Nested Loop Join, INLJ) - “聰明人的快捷方式” 🚀
當被驅動表(內層表)的連接字段上有索引時,情況就大不一樣了!INLJ閃亮登場!
-
算法描述:
- 選擇一個表作為“外層表”(驅動表)。
- 遍歷外層表的每一行。
- 對于外層表的每一行,不再全表掃描內層表,而是拿著外層表的連接字段值,通過內層表連接字段上的索引,直接“精準定位”到內層表中匹配的行。
-
偽代碼示意:
FOR each row R1 in OuterTable:LOOKUP R2 in InnerTable USING INDEX ON InnerTable.join_column WHERE InnerTable.join_column = R1.join_column_value:IF R2 is found:Output (R1, R2)
-
性能飛躍:
- I/O次數:如果外層表M行,內層表通過索引查找(假設是B+樹索引,理想情況是
logN
或常數級別),總的I/O次數大約是M + M * (索引查找成本)
。相比SNLJ的M + M*N
,效率提升是數量級的! EXPLAIN
中的信號:當你用EXPLAIN
分析JOIN語句時,如果看到被驅動表的type
是eq_ref
(對于唯一索引/主鍵連接) 或ref
(對于普通二級索引連接),通常就意味著用上了INLJ,這是個好兆頭!
-- 假設 students.class_id 和 classes.id 都有索引,且 classes.id 是主鍵 EXPLAIN SELECT s.name, c.class_name FROM students s INNER JOIN classes c ON s.class_id = c.id;-- 可能的EXPLAIN結果:
id select_type table type possible_keys key key_len ref rows Extra 1 SIMPLE s ALL idx_class_id NULL NULL NULL 1000 1 SIMPLE c eq_ref PRIMARY PRIMARY 4 test.s.class_id 1 – (這里students是驅動表,classes是被驅動表,classes.id用了主鍵索引,type=eq_ref,完美!)
- I/O次數:如果外層表M行,內層表通過索引查找(假設是B+樹索引,理想情況是
2.3 塊嵌套循環連接 (Block Nested Loop Join, BNL) - “批量相親,減少跑腿” 🚌
當被驅動表(內層表)的連接字段上沒有可用索引時,SNLJ太慢,MySQL又不想坐以待斃,于是就有了BNL。這通常是MySQL在無法使用INLJ時的“無奈之舉”。
-
算法描述:
- 選擇一個表作為“外層表”(驅動表)。
- 開辟一塊內存區域,叫做Join Buffer(大小由
join_buffer_size
參數控制)。 - 從外層表一次性讀取一批行(比如10行、100行,取決于Join Buffer能裝多少),把這些行的連接字段值和需要查詢的列都放到Join Buffer里。
- 然后,全表掃描一次內層表。
- 對于內層表的每一行,都跟Join Buffer中緩存的所有外層表行進行匹配。
- 重復步驟3-5,直到外層表的所有行都處理完畢。
-
偽代碼示意:
Initialize JoinBuffer FOR each row R1 in OuterTable:IF JoinBuffer is full:FOR each row R2 in InnerTable: // Scan InnerTable onceFOR each buffered_R1 in JoinBuffer:IF buffered_R1 joins with R2 ON join_condition:Output (buffered_R1, R2)Clear JoinBufferStore R1's relevant columns in JoinBuffer// Process any remaining rows in JoinBuffer (last batch) IF JoinBuffer is not empty:FOR each row R2 in InnerTable: // Scan InnerTable again (potentially)FOR each buffered_R1 in JoinBuffer:IF buffered_R1 joins with R2 ON join_condition:Output (buffered_R1, R2)
-
性能特點:
- 減少了內層表的掃描次數:相比SNLJ(內層表掃M遍),BNL中內層表被掃描的次數是
外層表總行數 / Join Buffer能容納的外層表行數
。如果Join Buffer足夠大,甚至可能只掃描內層表一次(理想情況,很少見)。 - 依然是磁盤I/O大戶:因為內層表還是全表掃描。
EXPLAIN
中的信號:Extra
列出現Using join buffer (Block Nested Loop)
。
- 減少了內層表的掃描次數:相比SNLJ(內層表掃M遍),BNL中內層表被掃描的次數是
-
join_buffer_size
的關鍵:這個參數的大小直接影響BNL的效率。Buffer越大,一次能緩存的外層表行越多,內層表的掃描次數就越少。但要注意,這個Buffer是每個連接獨享的,設置過大可能導致內存問題(詳見上一篇《全局參數優化》)。
2.4 MySQL 8.0+ 的新貴:哈希連接 (Hash Join) 🧙?♂?
從MySQL 8.0.18版本開始,當JOIN操作無法使用索引(即BNL的適用場景)時,MySQL引入了更高效的哈希連接 (Hash Join) 算法,并在后續版本中逐漸用它來替代BNL。
-
算法描述 (簡化版):
- 選擇一個表作為“構建表”(通常是較小的那個表)。
- 讀取構建表的所有行,根據連接字段計算哈希值,并在內存中構建一個哈希表 (Hash Table)。哈希表的Key是哈希值,Value是指向原始行的指針或行數據本身。
- 然后,選擇另一個表作為“探測表”(通常是較大的那個表)。
- 逐行讀取探測表,對于每一行,同樣根據連接字段計算哈希值。
- 用這個哈希值去哈希表中“探測”是否存在匹配的行。如果哈希值匹配,再進一步比較原始連接字段的值是否精確相等。
- 如果匹配成功,則組合輸出。
-
性能優勢:
- 通常比BNL效率更高,尤其是在連接的表都比較大,且Join Buffer無法完全容納構建表時。
- 構建哈希表和探測哈希表的操作,平均時間復雜度接近O(1)。
-
EXPLAIN
中的信號:在MySQL 8.0.18+,如果JOIN無法使用索引,你可能會在Extra
列看到類似Using hash join
或在EXPLAIN FORMAT=JSON
的輸出中看到hash_join
的執行計劃。 -
內存依賴:哈希連接也需要內存來構建哈希表。如果內存不足以容納整個構建表的哈希表,MySQL可能會采用更復雜的“分塊哈希連接”或“溢出到磁盤的哈希連接”,性能會有所下降,但通常仍優于BNL。
-
替代BNL:在MySQL 8.0.20及更高版本中,BNL被哈希連接完全取代。也就是說,即使你看到
Using join buffer
,其底層實現也可能是哈希連接了。
小結JOIN執行算法:
MySQL優化器會“絞盡腦汁”地選擇最高效的JOIN算法。它的首選永遠是INLJ(用上索引),因為這通常是最快的。如果實在沒辦法用索引,它在8.0之前會退而求其次用BNL,在8.0.18之后則更傾向于用更強大的Hash Join。
第三章:JOIN優化的“葵花寶典”——核心法則與實戰技巧 🚀📖
知道了MySQL是如何“相親”的,我們就能對癥下藥,寫出讓它“一見鐘情”的JOIN語句了!
1. 索引!索引!還是TMD索引!(最重要的事說三遍) 🔑🔑🔑
這是JOIN優化的第一金科玉律,沒有之一!
- 給誰加索引?
- 連接條件中的列:
ON tableA.col1 = tableB.col2
,那么tableA.col1
和tableB.col2
都應該是索引候選者。尤其是被驅動表(內層表)的連接列,必須要有索引! WHERE
子句中用于篩選的列。ORDER BY
和GROUP BY
中用到的列。
- 連接條件中的列:
- 索引類型:B-Tree索引是JOIN的好朋友。
- 數據類型要一致:確保連接字段的數據類型、字符集、排序規則都完全一致。如果類型不匹配(比如一個
INT
,一個VARCHAR
),MySQL可能需要進行隱式類型轉換,這會導致索引失效!- 壞例子:
ON users.user_id (INT) = orders.user_id_str (VARCHAR)
-> 索引可能失效! - 好例子:
ON users.user_id (INT) = orders.user_id (INT)
- 壞例子:
- 避免在連接字段上使用函數:和普通查詢一樣,
ON FUNC(tableA.col) = tableB.col
也會讓tableA.col
上的索引失效(除非你用了MySQL 8.0的函數索引)。
段子手吐槽:
不給JOIN列加索引,就像派了一個近視800度的士兵去戰場上肉眼索敵,然后你還怪他打不準?!給他配個“八倍鏡”(索引)啊,大哥!
2. 驅動表的選擇:“誰先動筷子”的藝術 🥢
在嵌套循環類的JOIN中(SNLJ, INLJ, BNL),驅動表(外層表)的選擇對性能有很大影響。
- MySQL優化器的選擇:
- 通常,MySQL優化器會嘗試選擇結果集行數較少的那個表作為驅動表(在應用了
WHERE
條件過濾之后)。因為驅動表會被完整掃描(或部分掃描),它的行數越少,外層循環的次數就越少。 - 對于
INNER JOIN
,優化器有權調整表的連接順序。 - 對于
LEFT JOIN
,左表固定為驅動表。 - 對于
RIGHT JOIN
,右表固定為驅動表(或者MySQL可能將其改寫為等價的LEFT JOIN
再處理)。
- 通常,MySQL優化器會嘗試選擇結果集行數較少的那個表作為驅動表(在應用了
- 人工干預:
STRAIGHT_JOIN
:
如果你覺得MySQL優化器選的驅動表“不夠明智”(比如統計信息不準導致誤判),你可以用STRAIGHT_JOIN
關鍵字來“強制”指定連接順序。SELECT ... FROM tableA STRAIGHT_JOIN tableB ...
會強制tableA
作為驅動表。- 何時使用? 僅當你有充分的理由和測試數據證明優化器選錯了,并且
STRAIGHT_JOIN
能帶來明顯性能提升時才考慮。大多數情況下,相信優化器。 - 風險:如果你的判斷是錯的,
STRAIGHT_JOIN
反而可能讓性能更差。
- 何時使用? 僅當你有充分的理由和測試數據證明優化器選錯了,并且
驅動表選擇原則(通用思路):
- 小表驅動大表:盡量讓行數較少的表(經過
WHERE
過濾后)作為驅動表。 - 被驅動表連接列有索引是前提:無論誰驅動誰,保證被驅動表的連接列上有高效索引是必須的。
3. 過濾條件要“給力”:盡可能早地減少結果集 📉
-
WHERE
子句 VSON
子句:- 對于
INNER JOIN
:WHERE
和ON
中的條件在邏輯上是等價的,MySQL優化器可能會重新安排它們的執行順序。但通常建議連接相關的條件寫在ON
中,單表篩選條件寫在WHERE
中,更清晰。 - 對于
LEFT JOIN
/RIGHT JOIN
(OUTER JOIN):ON
和WHERE
的條件位置非常重要!ON
條件:是在生成臨時連接結果集之前就用來篩選被驅動表(對于LEFT JOIN是右表,對于RIGHT JOIN是左表)的記錄的。如果被驅動表的記錄不滿足ON
條件,它就不會參與連接,其對應的驅動表行在結果中相關列為NULL。WHERE
條件:是在臨時連接結果集(驅動表所有行 + 匹配上的被驅動表行或NULL)生成之后,再對這個結果集進行最終的篩選。- 關鍵區別:如果把針對被驅動表的篩選條件錯放到
WHERE
子句中,對于OUTER JOIN,可能會導致本應保留的驅動表行(因為OUTER JOIN的特性)因為被驅動表部分為NULL而不滿足WHERE
條件,從而被錯誤地過濾掉,使得OUTER JOIN的行為退化成類似INNER JOIN。 - 最佳實踐:對于OUTER JOIN,如果想根據被驅動表的列來限制哪些行可以參與連接,務必把這些條件寫在
ON
子句里!-- 需求:查詢所有學生及其數學課的成績(沒有數學成績的也顯示學生,成績為NULL) -- 正確寫法 (篩選數學課的條件在ON里) SELECT s.name, sc.score FROM students s LEFT JOIN scores sc ON s.student_id = sc.student_id AND sc.subject = '數學';-- 錯誤寫法 (篩選數學課的條件在WHERE里,會導致沒有數學成績的學生整行被過濾掉) SELECT s.name, sc.score FROM students s LEFT JOIN scores sc ON s.student_id = sc.student_id WHERE sc.subject = '數學'; -- 這實際上變成了INNER JOIN的效果
- 對于
-
盡早過濾:通過在
WHERE
子句或ON
子句中添加有效的篩選條件,盡早地把不需要的數據行給“咔嚓”掉,這樣參與JOIN運算的行數就少了,性能自然提升。
4. join_buffer_size:不是萬能丹,合理使用才有效 💊
- 何時起作用:只有當JOIN操作無法使用索引,MySQL被迫使用BNL或Hash Join時,
join_buffer_size
才派得上用場。 - 不是越大越好:它是個“連接獨享”的內存區域。如果設太大,并發連接一多,內存就爆了。
- 優先解決索引問題:調大
join_buffer_size
是“治標不治本”的。首要任務永遠是檢查并優化JOIN列的索引! - 文檔建議:除非你確定有很多無法避免的、需要大量內存的無索引JOIN,否則不建議將此值設得過大。幾MB到十幾MB通常是上限。
5. EXPLAIN:你的JOIN優化“導航儀” 🗺?
不厭其煩地再次強調EXPLAIN
的重要性!對于任何你覺得慢的JOIN查詢,第一件事就是把它扔給EXPLAIN
“體檢”一下。
- 重點關注的列:
table
: 表的讀取順序(大致反映了驅動表和被驅動表的順序)。type
: 連接類型!這是判斷JOIN效率的核心。- 最佳:
system
>const
>eq_ref
(唯一索引/主鍵JOIN) >ref
(普通二級索引JOIN) - 較差:
ref_or_null
>index_merge
>unique_subquery
>index_subquery
- 糟糕:
range
>index
(全索引掃描) >ALL
(全表掃描) - 對于JOIN,如果被驅動表的
type
是eq_ref
或ref
,說明索引用上了,很好!如果是ALL
或index
,那就要警惕了,可能是BNL或Hash Join。
- 最佳:
possible_keys
: 可能用到的索引。key
: 實際用到的索引。如果是NULL
,說明沒用上索引。key_len
: 用到的索引長度。越短越好(在能區分記錄的前提下)。ref
: 顯示了哪些列或常量被用于索引查找。rows
: MySQL估計需要掃描的行數。越小越好。Extra
: 包含大量重要信息!Using index
: 覆蓋索引,非常好!Using where
: 使用了WHERE子句進行過濾。Using temporary
: 可能用了臨時表(比如GROUP BY
或UNION
操作)。Using filesort
: 文件排序,性能殺手,需要優化ORDER BY
或相關索引。Using join buffer (Block Nested Loop)
: 說明用了BNL算法。Using join buffer (Batched Key Access)
: BKA是一種優化的BNL,結合了MRR(Multi-Range Read)。Using hash join
(MySQL 8.0.18+): 說明用了哈希連接。Not exists
: 用于反連接優化。
通過仔細解讀EXPLAIN
的輸出,你就能診斷出JOIN的瓶頸在哪里,是索引沒用上?還是驅動表選錯了?還是Join Buffer太小(或者說,應該加索引)?
6. MySQL 8.0+ 的其他JOIN優化特性 (錦上添花) 🌸
除了Hash Join,MySQL 8.0還在JOIN優化方面做了一些其他改進:
- Lateral Derived Tables (LATERAL關鍵字):MySQL 8.0.14引入。允許派生表(子查詢)引用FROM子句中在它之前定義的表的列。這可以實現一些以前很難或效率低下的“依賴性JOIN”。
- 優化器對IN子查詢的轉換:很多
IN (SELECT ...)
的子查詢會被優化器轉換為更高效的JOIN。 - 更智能的代價模型:優化器在選擇執行計劃時,會基于更精確的成本估算。
7. JOIN查詢的“七宗罪” (常見避坑指南) 🚫
- “無情”笛卡爾積:忘了寫
ON
或WHERE
連接條件,或者條件寫錯導致全匹配。 - “裸奔”連接列:連接字段沒有索引,或者索引失效(類型不匹配、用函數等)。
- “貪婪”SELECT:明明只需要幾列,卻非要
SELECT *
,尤其是在JOIN大表時,會增加大量不必要的IO和網絡傳輸,也可能讓覆蓋索引失效。按需索取,才是王道! - “迷糊”OUTER JOIN條件:把本該放在
ON
里的被驅動表篩選條件,錯放到了WHERE
里,導致結果不符合預期。 - “臃腫”大事務JOIN:在一個超大的事務里執行復雜的JOIN,長時間鎖住資源,影響并發。
- “盲目”相信優化器/“過度”人工干預:既不能完全不看
EXPLAIN
就上線,也不能芝麻大點事就用STRAIGHT_JOIN
或FORCE INDEX
。先理解,再優化。 - “忽視”數據分布和統計信息:如果表的統計信息嚴重過時或不準確,優化器可能會做出錯誤的執行計劃。定期
ANALYZE TABLE
可能有助于更新統計信息。
第四章:實戰演練——看個例子壓壓驚 👨?🏫
假設我們有兩張表:
employees
(員工表):emp_no
(PK),first_name
,last_name
,hire_date
,dept_no
(FK, 有索引)departments
(部門表):dept_no
(PK),dept_name
查詢需求:找出所有在 ‘Sales’ 部門,并且是在 '2023-01-01’之后入職的員工姓名。
糟糕的寫法 (可能):
-- 假設departments表非常大,employees表相對較小
SELECT e.first_name, e.last_name
FROM departments d, employees e -- 隱式JOIN,容易寫漏條件
WHERE d.dept_name = 'Sales'AND e.hire_date > '2023-01-01'AND e.dept_no = d.dept_no; -- 連接條件放最后,可讀性稍差
優化思路與較好的寫法:
- 明確JOIN類型和連接條件:使用顯式
INNER JOIN
。 - 索引檢查:確保
employees.dept_no
,departments.dept_no
,employees.hire_date
都有索引。departments.dept_name
也最好有索引,如果經常用它查詢。 - 驅動表考量:
- 如果
departments.dept_name = 'Sales'
能篩選出很少的部門(比如就1個),那么departments
作為驅動表可能更好。 - 如果
employees.hire_date > '2023-01-01'
能篩選出很少的員工,那么employees
作為驅動表可能更好。 - MySQL優化器通常會嘗試估算。
- 如果
推薦寫法:
SELECT e.first_name, e.last_name
FROM employees e
INNER JOIN departments d ON e.dept_no = d.dept_no -- 連接條件清晰
WHERE d.dept_name = 'Sales' -- 篩選條件1AND e.hire_date > '2023-01-01'; -- 篩選條件2-- 使用EXPLAIN分析:
EXPLAIN SELECT e.first_name, e.last_name
FROM employees e
INNER JOIN departments d ON e.dept_no = d.dept_no
WHERE d.dept_name = 'Sales'AND e.hire_date > '2023-01-01';
EXPLAIN結果分析要點:
- 看哪個表是驅動表,哪個是被驅動表。
- 看被驅動表的
type
是不是eq_ref
或ref
。 - 看
key
列是否都用上了合適的索引。 - 看
rows
列估算的掃描行數是不是盡可能小。 - 看
Extra
列有沒有Using filesort
或不希望出現的Using join buffer
。
如果發現性能不佳,比如EXPLAIN
顯示某個表的type
是ALL
,那就要重點檢查該表的連接列和WHERE
條件列的索引情況。
總結:JOIN優化🧘?♂?
呼!關于MySQL的JOIN優化,咱們今天這趟“星際穿越”算是把主要景點都逛了一遍。從JOIN的種類、執行原理,到各種優化秘籍和避坑指南,信息量確實不小。
但記住,JOIN優化不是一門“玄學”,它是有章可循的科學。核心就三點:
- 讓索引飛起來! (尤其是被驅動表的連接列)
- 讓數據量小下去! (通過有效的
WHERE
和ON
條件盡早過濾) - 讓算法跑起來! (理解MySQL如何選擇JOIN算法,并創造條件讓它選最優的)
而這一切的基礎,都離不開你對EXPLAIN
輸出的“火眼金睛”般的解讀能力。
JOIN優化,就像當一個數據庫界的“月老”,你的目標就是用最少的“相親成本”(系統資源),讓合適的“男女嘉賓”(數據行)最高效地“牽手成功”(組合成結果)。這需要你對雙方(表結構、數據分布、索引情況)都有深入的了解,還需要一點點“成人之美”的耐心和智慧。