化實(shí)戰(zhàn):從慢查詢到秒回的完整指南)
1. 從一段真實(shí)業(yè)務(wù)需求說起子查詢到底解決什么問題前幾天排查一個(gè)線上慢查詢業(yè)務(wù)方丟過來一段SQL說頁面加載要七八秒用戶快把客服電話打爆了。我先看了一眼表結(jié)構(gòu)和數(shù)據(jù)量又跑了一遍EXPLAIN問題很快就定位到了——三層嵌套子查詢最內(nèi)層掃了全表中間層又做了文件排序外層再逐個(gè)匹配。這大概是MySQL子查詢最典型也最容易被寫壞的場景之一。先給不太熟的朋友補(bǔ)個(gè)基礎(chǔ)定義子查詢就是嵌套在另一個(gè)SQL父查詢內(nèi)部的SELECT語句它可以出現(xiàn)在WHERE、FROM、HAVING、SELECT列表、甚至UPDATE/DELETE語句中。你可能會(huì)問明明用JOIN也能實(shí)現(xiàn)大部分需求為什么還要用子查詢因?yàn)橛行┻壿嬘米硬樵儽磉_(dá)更直觀比如“找出比部門平均工資高的員工”“查詢最近一單金額超過500的用戶”——這類需求如果硬改成JOIN要么需要先聚合生成臨時(shí)表要么得用窗口函數(shù)繞一大圈代碼可讀性反而更差。子查詢這個(gè)語法從MySQL 4.1就開始支持了經(jīng)歷過好幾個(gè)版本的優(yōu)化器改寫策略迭代。5.6之前性能經(jīng)常被吐槽5.7開始引入了半連接優(yōu)化和物化策略8.0又對(duì)派生表做了Lateral Derived優(yōu)化、窗口函數(shù)配合子查詢也更好用了。所以這篇文章不是簡單羅列語法而是結(jié)合我實(shí)際排查和優(yōu)化過的案例把子查詢的分類、執(zhí)行原理、性能陷阱、改寫技巧一次講透。2. 子查詢的七種形態(tài)與實(shí)戰(zhàn)拆解2.1 標(biāo)量子查詢最靈活但最容易忽視NULL標(biāo)量子查詢返回的是一行一列的單個(gè)值可以用在幾乎所有需要單個(gè)值的地方。舉幾個(gè)最常見的場景-- 查詢每個(gè)員工的工資與部門平均工資的差額 SELECT emp_name, salary, salary - (SELECT AVG(salary) FROM dept_emp WHERE dept_id e.dept_id) AS diff FROM employee e;這段SQL中SELECT AVG(salary)返回的是一個(gè)數(shù)值這就是標(biāo)量子查詢。實(shí)際開發(fā)中標(biāo)量子查詢最常見的問題不是性能而是NULL參與運(yùn)算導(dǎo)致結(jié)果變成NULL。如果某個(gè)部門還沒有員工內(nèi)層AVG返回NULL外層salary - NULL的結(jié)果就是NULL。很多報(bào)表因此出現(xiàn)大面積的空值排查半天才發(fā)現(xiàn)是子查詢返回了NULL。我的習(xí)慣是凡是參與數(shù)值運(yùn)算的標(biāo)量子查詢外層一定要加IFNULL或者用COALESCE兜底SELECT emp_name, salary, salary - COALESCE((SELECT AVG(salary) FROM dept_emp WHERE dept_id e.dept_id), 0) AS diff FROM employee e;標(biāo)量子查詢還有個(gè)隱蔽的性能問題如果它在SELECT列表里并且是相關(guān)子查詢引用了外層表的字段MySQL會(huì)對(duì)每一行外層記錄都執(zhí)行一次內(nèi)層查詢。也就是說外層有10萬行內(nèi)層就執(zhí)行10萬次。這在數(shù)據(jù)量小的時(shí)候沒感覺一旦上了規(guī)模就是災(zāi)難性的后面我會(huì)專門講相關(guān)子查詢的優(yōu)化方案。2.2 列子查詢與行子查詢語法糖背后的邏輯列子查詢返回一列多行通常配合IN、ANY、ALL使用-- 找出所有在“銷售部”待過的員工 SELECT emp_name FROM employee WHERE emp_id IN ( SELECT emp_id FROM dept_history WHERE dept_name 銷售部 );行子查詢返回一行多列可以同時(shí)比較多個(gè)字段-- 找出與張三在同一個(gè)部門且薪資相同的員工 SELECT emp_name FROM employee WHERE (dept_id, salary) ( SELECT dept_id, salary FROM employee WHERE emp_name 張三 );這里有個(gè)需要留意的點(diǎn)MySQL對(duì)行子查詢的等值比較支持得不錯(cuò)但如果比較的是多個(gè)字段且其中某個(gè)字段允許NULL行為可能和直覺不一致。比如(dept_id, salary) (NULL, 5000)這種比較在SQL的三值邏輯下永遠(yuǎn)不是TRUE查詢結(jié)果會(huì)靜默丟失。我建議能用列子查詢加IN解決的就不要用行子查詢組合比較因?yàn)榭勺x性更好優(yōu)化器處理IN的改寫策略也成熟很多。2.3 表子查詢與派生表FROM子句里的臨時(shí)視圖表子查詢出現(xiàn)在FROM子句中它的結(jié)果集被當(dāng)作一張派生表使用必須起一個(gè)別名SELECT d.dept_name, tmp.avg_salary FROM department d JOIN ( SELECT dept_id, AVG(salary) AS avg_salary FROM employee GROUP BY dept_id ) tmp ON d.dept_id tmp.dept_id;這里tmp就是派生表MySQL8.0之前派生表默認(rèn)是物化的也就是說MySQL會(huì)先把內(nèi)層查詢的結(jié)果存在一張內(nèi)部臨時(shí)表里再做外層JOIN。8.0.14之后優(yōu)化器支持了Lateral Derived Optimization允許派生表引用前面表的數(shù)據(jù)配合LATERAL關(guān)鍵字使用SELECT d.dept_name, tmp.emp_name FROM department d LEFT JOIN LATERAL ( SELECT emp_name FROM employee WHERE dept_id d.dept_id ORDER BY salary DESC LIMIT 1 ) tmp ON TRUE;這個(gè)寫法可以很方便地實(shí)現(xiàn)“取每個(gè)部門工資最高的員工”以前需要用窗口函數(shù)或者反連接技巧現(xiàn)在一條LATERAL派生表就搞定了。我實(shí)測過在分層匯總類需求中LATERAL派生表比傳統(tǒng)相關(guān)子查詢的性能好很多因?yàn)樗梢员粌?yōu)化器識(shí)別并采用更合理的連接順序。2.4 EXISTS與IN謂詞子查詢的抉擇EXISTS和IN是日常開發(fā)中用的最多的兩種謂詞子查詢但很多人并不清楚它們在MySQL內(nèi)部的不同處理路徑。先說結(jié)論半連接場景下MySQL 5.7 對(duì)IN和EXISTS的改寫策略已經(jīng)非常接近并沒有絕對(duì)的好壞之分關(guān)鍵取決于數(shù)據(jù)分布和索引情況。簡單記憶外層表小、內(nèi)層表大且內(nèi)層查詢能命中索引時(shí)IN通常表現(xiàn)更好外層表大、內(nèi)層表小且需要快速短路判斷時(shí)EXISTS配合索引通常更穩(wěn)NOT IN一定要小心NULL值一旦子查詢結(jié)果中有NULLNOT IN的判定會(huì)變成“不是UNKNOWN”導(dǎo)致整個(gè)查詢返回空集。相比之下NOT EXISTS不會(huì)踩這個(gè)坑。舉個(gè)例子公司有個(gè)接口要查詢“從未下單的用戶”新手往往寫SELECT user_id, user_name FROM users WHERE user_id NOT IN ( SELECT user_id FROM orders );只要orders表里有任何一條user_id為NULL的記錄這條查詢就永遠(yuǎn)返回空。這不是MySQL的bug而是SQL標(biāo)準(zhǔn)中NULL的邏輯語義。正確寫法應(yīng)該用NOT EXISTSSELECT u.user_id, u.user_name FROM users u WHERE NOT EXISTS ( SELECT 1 FROM orders o WHERE o.user_id u.user_id );我處理過好幾次生產(chǎn)事故都是NOT IN遇到NULL導(dǎo)致報(bào)表數(shù)據(jù)清零排查起來還很繞。所以我的鐵律是遇到NOT IN先問一句子查詢列允許NULL嗎允許就立刻改成NOT EXISTS。3. 子查詢的執(zhí)行計(jì)劃與性能優(yōu)化深挖3.1 看懂EXPLAIN中的子查詢線索優(yōu)化子查詢的第一件事是學(xué)會(huì)看執(zhí)行計(jì)劃。MySQL的EXPLAIN輸出中和子查詢直接相關(guān)的字段主要在select_type列常見取值有select_type 取值含義常見場景PRIMARY最外層查詢所有SQL都有SUBQUERY非相關(guān)子查詢會(huì)被物化WHERE中獨(dú)立的IN子查詢DEPENDENT SUBQUERY相關(guān)子查詢每行執(zhí)行一次EXISTS、相關(guān)標(biāo)量子查詢MATERIALIZED子查詢被物化為臨時(shí)表IN子查詢被優(yōu)化為物化DERIVEDFROM子句中的派生表派生表查詢UNIONUNION語句中的子查詢聯(lián)合查詢看到DEPENDENT SUBQUERY就要警覺它意味著內(nèi)層查詢會(huì)依賴外層行數(shù)據(jù)逐行執(zhí)行。DEPENDENT并不是優(yōu)化器給出的“相關(guān)性描述”而是執(zhí)行方式的聲明對(duì)應(yīng)的是每行重復(fù)執(zhí)行的嵌套循環(huán)策略。另一種值得留意的是MATERIALIZED它說明子查詢結(jié)果被提前物化成了一張內(nèi)部臨時(shí)表。物化本身不是壞事它避免了重復(fù)執(zhí)行但如果物化出來的臨時(shí)表很大且沒有合適的索引后續(xù)Join時(shí)可能出現(xiàn)大表驅(qū)動(dòng)小表的尷尬這個(gè)時(shí)候需要檢查臨時(shí)表的訪問路徑。3.2 半連接與物化MySQL如何悄悄改寫你的SQLMySQL優(yōu)化器中有一個(gè)非常重要的概念叫半連接Semi-join專門處理IN、EXISTS、ANY等形式的子查詢。半連接的含義是不關(guān)心子查詢匹配了多少行只關(guān)心“是否存在至少一行匹配”。優(yōu)化器會(huì)把這類子查詢改寫為半連接執(zhí)行計(jì)劃可選的策略主要包括FirstMatch對(duì)外層每一行只要在內(nèi)層找到第一條匹配就停止掃描。這是最樸素也最常用的策略等價(jià)于EXISTS的短路邏輯。DuplicateWeedout把內(nèi)層匹配結(jié)果去重后再做普通連接。適用于子查詢內(nèi)部本身會(huì)產(chǎn)生重復(fù)行的場景。LooseScan利用內(nèi)層表的索引對(duì)每個(gè)分組只取第一條匹配值避免重復(fù)掃描。Materialization先把子查詢結(jié)果物化成臨時(shí)表在臨時(shí)表上建索引再做連接。這些策略不是DBA手動(dòng)指定的是優(yōu)化器基于統(tǒng)計(jì)信息自動(dòng)選擇的。但在某些情況下優(yōu)化器會(huì)失明。比如內(nèi)外層表的數(shù)據(jù)分布嚴(yán)重傾斜、統(tǒng)計(jì)信息過期、或者WHERE條件里寫了函數(shù)導(dǎo)致索引失效優(yōu)化器就可能選錯(cuò)策略。這時(shí)候可以手動(dòng)改寫成JOIN形式來繞過錯(cuò)誤執(zhí)行計(jì)劃也可以調(diào)整optimizer_switch中的半連接開關(guān)。我在故障排查時(shí)見過最典型的案例一個(gè)IN子查詢被優(yōu)化器選成了FirstMatch但內(nèi)層表過濾后選擇性太差導(dǎo)致外層每一行都要掃描大量內(nèi)層記錄。把optimizer_switch里的firstmatch臨時(shí)關(guān)閉強(qiáng)制走物化查詢時(shí)間從3秒降到0.1秒。3.3 子查詢改JOIN的黃金法則很多優(yōu)化文章提倡“子查詢一律改JOIN”這個(gè)說法過于絕對(duì)。JOIN改寫后由于普通JOIN會(huì)產(chǎn)生重復(fù)行往往還要額外加DISTINCT去重反而可能引入新的性能問題。我的黃金法則是能保持語義不變的前提下才改JOIN。IN子查詢改JOIN一般要加DISTINCTEXISTS改成JOIN則不需要去重但要注意多表連接會(huì)放大行數(shù)。優(yōu)先檢查索引而不是優(yōu)先改寫法。子查詢慢很多情況下不是子查詢本身的問題而是缺少合適的索引。相關(guān)子查詢優(yōu)先考慮改寫為派生表或EXISTS而不是盲目改JOIN因?yàn)橄嚓P(guān)子查詢意味著N1模式改成JOIN可能直接用哈希連接或索引嵌套循環(huán)解決。聚合類子查詢改成JOINGROUP BY往往能利用索引和優(yōu)化器的排序優(yōu)化。用一句話總結(jié)子查詢和JOIN只是同一邏輯的兩種不同表達(dá)最終執(zhí)行性能取決于優(yōu)化器能不能把SQL翻譯成高效的執(zhí)行計(jì)劃。我們的目標(biāo)是給優(yōu)化器更多選擇而不是替代它做決策。4. 實(shí)戰(zhàn)案例從慢查詢到秒回的完整優(yōu)化4.1 場景構(gòu)建與初始SQL為了講清楚我構(gòu)造一個(gè)貼近真實(shí)業(yè)務(wù)的模擬場景。某電商平臺(tái)有兩張表orders訂單表約200萬行order_id、user_id、amount、status、order_timeusers用戶表約50萬行user_id、user_name、user_level業(yè)務(wù)需求是找出所有“黃金會(huì)員”中最近30天內(nèi)有下單但訂單金額低于100元的用戶。新手第一版SQL通常寫成相關(guān)子查詢SELECT u.user_id, u.user_name FROM users u WHERE u.user_level gold AND EXISTS ( SELECT 1 FROM orders o WHERE o.user_id u.user_id AND o.amount 100 AND o.order_time NOW() - INTERVAL 30 DAY );單獨(dú)看這條SQL的執(zhí)行計(jì)劃還行但如果users表中g(shù)old會(huì)員有20萬EXISTS子查詢每行執(zhí)行一次orders表上又沒有(user_id, amount, order_time)的聯(lián)合索引后果就是20萬次全表掃描耗時(shí)輕松超過10秒。4.2 逐步拆解與改寫第一步我不急著改SQL先檢查索引。orders表上原來只有主鍵和user_id單列索引user_id過濾后還要回表判斷amount和order_time這是一個(gè)典型的索引覆蓋不足問題。我先加上聯(lián)合索引ALTER TABLE orders ADD INDEX idx_user_amount_time (user_id, amount, order_time);加完索引后再跑一遍EXISTS版本性能提升已經(jīng)很明顯因?yàn)閮?nèi)層查詢可以直接走索引覆蓋掃描。第二步考慮把相關(guān)子查詢改成半連接JOIN。由于EXISTS只關(guān)心存在性JOIN時(shí)不會(huì)產(chǎn)生重復(fù)行無需額外DISTINCTSELECT DISTINCT u.user_id, u.user_name FROM users u JOIN orders o ON o.user_id u.user_id WHERE u.user_level gold AND o.amount 100 AND o.order_time NOW() - INTERVAL 30 DAY;注意這里我加了DISTINCT原因是理論上一個(gè)用戶可能有多條符合條件的訂單不加DISTINCT用戶會(huì)重復(fù)出現(xiàn)。雖然在這個(gè)業(yè)務(wù)場景下一對(duì)多的可能性不小所以保留DISTINCT更穩(wěn)妥。第三步進(jìn)一步考慮能不能減少外層掃描的行數(shù)。如果users表上user_level沒有索引那每次都要全表掃描50萬行去過濾gold會(huì)員這也是一個(gè)優(yōu)化點(diǎn)ALTER TABLE users ADD INDEX idx_level (user_level);4.3 優(yōu)化效果對(duì)比我在模擬環(huán)境實(shí)測了三組數(shù)據(jù)均為本地測試數(shù)據(jù)結(jié)果供參考優(yōu)化階段執(zhí)行耗時(shí)掃描行數(shù)備注原始EXISTS無聯(lián)合索引8.6秒訂單表多次全表掃描相關(guān)子查詢20萬次全表掃描加聯(lián)合索引后的EXISTS0.45秒訂單表索引范圍掃描索引覆蓋生效改JOIN并保留DISTINCT0.32秒兩張表均走索引優(yōu)化器選擇半連接策略最終版本再加大緩沖池0.28秒基本命中緩存冷啟動(dòng)場景接近這個(gè)案例的核心結(jié)論是第一步永遠(yuǎn)是索引第二步才是SQL改寫。很多開發(fā)同學(xué)一上來就改寫SQL結(jié)果索引沒建換什么寫法都白搭。5. 常見問題與避坑實(shí)錄5.1 子查詢的NULL陷阱前面提過NOT IN遇到NULL的問題這里再補(bǔ)兩個(gè)容易被坑的場景。第一個(gè)是NOT IN子查詢中包含NULL??催@個(gè)例子SELECT user_name FROM users WHERE user_id NOT IN (1, 2, NULL);直覺上應(yīng)該返回除了1、2之外的所有用戶實(shí)際結(jié)果卻是一個(gè)空集。原因user_id NULL的結(jié)果是UNKNOWNNOT IN等價(jià)于user_id 1 AND user_id 2 AND user_id NULL只要有一個(gè)AND條件是UNKNOWN整條過濾條件就是UNKNOWN行不會(huì)被選中。第二個(gè)是聚合函數(shù)內(nèi)部的NULL。COUNT(col)會(huì)跳過NULLAVG(col)也會(huì)忽略NULL參與計(jì)算。如果你在子查詢里聚合而目標(biāo)列本身有NULL值統(tǒng)計(jì)結(jié)果會(huì)和預(yù)期不一致。常見解決方式是提前用WHERE col IS NOT NULL過濾或者在聚合函數(shù)里顯式處理。5.2 相關(guān)子查詢的隱式性能殺手在開發(fā)環(huán)境測試相關(guān)子查詢數(shù)據(jù)量只有幾千行時(shí)效果很好一上線到生產(chǎn)環(huán)境幾百萬行就瞬間卡死。這類問題最隱蔽因?yàn)閱慰碨QL完全找不出問題。我的排查習(xí)慣是遇到DEPENDENT SUBQUERY第一反應(yīng)是確認(rèn)外層驅(qū)動(dòng)表的行數(shù)。如果外層只有幾十行相關(guān)子查詢完全沒問題如果外層幾十萬行每條內(nèi)層查詢哪怕只有0.01秒總耗時(shí)也要幾十分鐘。優(yōu)化手段按優(yōu)先級(jí)排序給內(nèi)層子查詢的關(guān)聯(lián)字段加索引把相關(guān)子查詢改寫成非相關(guān)的IN子查詢改寫成JOIN交優(yōu)化器決定連接順序如果邏輯復(fù)雜拆成多條SQL在應(yīng)用層做內(nèi)存關(guān)聯(lián)。我個(gè)人實(shí)際經(jīng)驗(yàn)是可讀性優(yōu)先保留子查詢性能敏感路徑一律改JOIN或拆SQL。生產(chǎn)環(huán)境穩(wěn)定性比代碼優(yōu)雅重要得多。5.3 子查詢與索引失效的邊界子查詢中經(jīng)常包含函數(shù)或隱式類型轉(zhuǎn)換這會(huì)讓索引失效。比如WHERE DATE(order_time) 2024-01-01即使order_time上有索引因?yàn)樘琢薉ATE函數(shù)MySQL無法使用索引范圍掃描只能全表掃描。更隱蔽的是隱式類型轉(zhuǎn)換。如果user_id是VARCHAR類型而子查詢傳入的是INTMySQL會(huì)把字符串列轉(zhuǎn)成數(shù)字再比較索引也可能失效。排查這類問題可以直接用EXPLAIN看type列如果從ref降級(jí)成ALL優(yōu)先考慮函數(shù)或類型轉(zhuǎn)換的影響。常見的保留索引寫法-- 不推薦對(duì)索引列使用函數(shù) WHERE DATE(order_time) 2024-01-01 -- 推薦使用范圍條件 WHERE order_time 2024-01-01 AND order_time 2024-01-025.4 子查詢的常見執(zhí)行計(jì)劃速查給一張速查表方便日常排查執(zhí)行計(jì)劃特征可能原因優(yōu)先處理方式DEPENDENT SUBQUERY 全表掃描相關(guān)列缺索引加索引或改JOINMATERIALIZED 大臨時(shí)表子查詢結(jié)果集過大檢查子查詢過濾條件考慮改EXISTSSUBQUERY DERIVEDFROM子查詢?nèi)頀呙杞o派生表關(guān)聯(lián)字段加索引Impossible WHERE子查詢條件恒為假檢查NULL和常量條件Using temporary / Using filesort排序和去重過大優(yōu)化索引減少掃描行數(shù)這套速查表是我自己排查慢查詢時(shí)經(jīng)常對(duì)照的清單遇到?jīng)]見過的執(zhí)行計(jì)劃形態(tài)先按表格分類再順著方向排查效率會(huì)高很多。6. 幾個(gè)值得記住的子查詢擴(kuò)展用法6.1 子查詢與窗口函數(shù)配合MySQL 8.0開始支持窗口函數(shù)子查詢和它的配合可以解決很多“取每組TopN”的問題。以前實(shí)現(xiàn)“每個(gè)部門工資最高的前兩人”需要用相關(guān)子查詢或者復(fù)雜的自連接現(xiàn)在可以這樣寫SELECT dept_id, emp_name, salary FROM ( SELECT dept_id, emp_name, salary, ROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rn FROM employee ) tmp WHERE rn 2;這里的派生表子查詢把窗口函數(shù)的計(jì)算結(jié)果當(dāng)作一列外層再基于rn過濾邏輯簡潔清晰性能也遠(yuǎn)好于同邏輯的相關(guān)子查詢版本。6.2 子查詢直接驅(qū)動(dòng)UPDATE和DELETE子查詢還可以用在UPDATE和DELETE中實(shí)現(xiàn)精確的批量操作-- 批量刪除“近一年未下單”的用戶 DELETE FROM users WHERE user_id IN ( SELECT user_id FROM ( SELECT u.user_id FROM users u LEFT JOIN orders o ON o.user_id u.user_id WHERE o.order_id IS NULL AND u.register_time NOW() - INTERVAL 1 YEAR ) t );這里需要特別注意的是MySQL不允許直接在同一張表上同時(shí)進(jìn)行SELECT和DELETE所以要包一層派生表來繞開限制。這種寫法有坑如果子查詢結(jié)果非常大DELETE操作會(huì)長時(shí)間持有行鎖建議分批刪除DELETE FROM users WHERE user_id IN ( SELECT user_id FROM ( SELECT u.user_id FROM users u LEFT JOIN orders o ON o.user_id u.user_id WHERE o.order_id IS NULL AND u.register_time NOW() - INTERVAL 1 YEAR LIMIT 5000 ) t );一批刪除5000行循環(huán)執(zhí)行避免長時(shí)間鎖表。我處理過不止一次因?yàn)橐淮涡訢ELETE數(shù)據(jù)量過大導(dǎo)致主從延遲飆升的事故都是靠分批刪除解決的。6.3 子查詢優(yōu)化器的版本差異最后提一個(gè)版本相關(guān)的問題。不同MySQL版本對(duì)子查詢的處理差異很大如果要做版本升級(jí)一定要重新壓測子查詢相關(guān)SQL。以我的經(jīng)歷來說5.6到5.7的升級(jí)中IN子查詢的執(zhí)行計(jì)劃從單純的SUBQUERY變成了支持Materialization和Semi-join很多原本慢的查詢?nèi)庋劭梢姷刈兛炝?。?.7到8.0的升級(jí)中派生表的Merge和Lateral能力增強(qiáng)但同時(shí)也可能因?yàn)楣_B接、窗口函數(shù)等新特性的引入導(dǎo)致一些子查詢的代價(jià)估算發(fā)生改變。升級(jí)前跑一遍全量慢查詢回歸是我見過最有效的防坑手段。不要光看兼容性文檔要用真實(shí)數(shù)據(jù)和真實(shí)SQL測試執(zhí)行計(jì)劃的變化。寫在最后的實(shí)操心得做了這么多年數(shù)據(jù)庫優(yōu)化我的體會(huì)是子查詢不是洪水猛獸真正危險(xiǎn)的是對(duì)執(zhí)行機(jī)制的無知。該用子查詢的時(shí)候就大膽用但寫完務(wù)必做三件事第一跑EXPLAIN確認(rèn)select_type和執(zhí)行策略第二檢查相關(guān)子查詢的嵌套層級(jí)盡量控制在兩層以內(nèi)第三對(duì)該子查詢涉及的所有關(guān)聯(lián)列確認(rèn)有索引。最后再分享一個(gè)小技巧如果查詢中用了IN子查詢且懷疑優(yōu)化器選錯(cuò)策略可以在SQL前面加EXPLAIN FORMATJSON輸出里會(huì)有optimized_away、semijoin_strategy、materialized_from_subquery這些詳細(xì)信息它們比單看EXPLAIN表格更直觀地展示優(yōu)化器到底做了什么決策。遇到玄學(xué)性能問題把JSON執(zhí)行計(jì)劃拉出來對(duì)照著看比盲目改SQL靠譜得多。