結(jié)論:LEFT JOIN后右表過濾必須放ON而非WHERE,否則退化為INNER JOIN;多表關(guān)聯(lián)優(yōu)先用CTE拆解;IN子查詢需防NULL失效,應(yīng)改用EXISTS。
直接說結(jié)論:子查詢過濾不能硬塞進(jìn) WHERE,尤其在 LEFT JOIN 后;三表以上關(guān)聯(lián)優(yōu)先用 CTE 拆解,而不是嵌套子查詢 + JOIN;多數(shù)性能問題根源不是語法寫錯(cuò),而是 ON 和 WHERE 的語義混淆。
LEFT JOIN 后對(duì)右表字段加 WHERE 就等于 INNER JOIN
這是線上最常踩的坑。比如你寫:
SELECT u.name, o.order_id
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE o.status = 'paid'``;
表面上想查“已支付訂單的用戶”,實(shí)際效果是:所有沒訂單、或訂單狀態(tài)不是 paid 的用戶全被過濾掉了——LEFT JOIN 形同虛設(shè)。
原因在于執(zhí)行順序:FROM → JOIN → WHERE,WHERE 是在連接完成之后才執(zhí)行的,此時(shí)右表字段為 NULL 的行直接被干掉。
- 正確做法:把右表過濾條件移到
ON子句里:LEFT JOIN orders o ON u.id = o.user_id AND o.status = 'paid' - 如果業(yè)務(wù)上真需要“所有用戶 + 僅已支付訂單”,就別在
WHERE碰右表字段 - 檢查執(zhí)行計(jì)劃:若
EXPLAIN的Extra列出現(xiàn)Using where且涉及右表字段,基本可以判定邏輯已變異
三張表以上,別堆 JOIN,先用 CTE 預(yù)聚合
寫 FROM a JOIN b JOIN c JOIN d 看似直白,但優(yōu)化器容易誤判驅(qū)動(dòng)表,尤其當(dāng)某張表數(shù)據(jù)量大、又沒索引時(shí),中間結(jié)果集可能爆炸。更糟的是,一旦其中一張表是一對(duì)多(比如一個(gè)用戶有多條日志),再連下一張表就會(huì)指數(shù)級(jí)放大行數(shù)。
比如要查“每個(gè)活躍用戶最新一筆訂單 + 訂單對(duì)應(yīng)的商品類目”,硬寫四表 JOIN 容易出錯(cuò)且難調(diào)試。
- 推薦拆成兩步:先用 CTE 算出每個(gè)用戶的最新訂單 ID(
WITH latest_order AS (SELECT user_id, MAX(created_at) AS max_time FROM orders WHERE status = 'shipped' GROUP BY user_id)) - 再把這個(gè) CTE 當(dāng)作“虛擬維度表”去 JOIN 用戶表和商品表
- CTE 不僅可讀性高,MySQL 8.0+ 和 PostgreSQL 還能復(fù)用其執(zhí)行結(jié)果,避免重復(fù)計(jì)算
- 避免用嵌套子查詢替代 CTE:子查詢?cè)?
FROM里必須顯式SELECT所有后續(xù)要用的字段,漏一個(gè)就報(bào)ERROR: column "xxx" does not exist
IN (SELECT ...) 要警惕 NULL 和性能斷崖
當(dāng)右表子查詢可能返回 NULL 時(shí),IN 會(huì)整個(gè)失效——這是 SQL 標(biāo)準(zhǔn)行為,不是 bug。例如:
SELECT * FROM users WHERE id IN (SELECT user_id FROM logs WHERE action = 'login'``);
只要 logs.user_id 里有一個(gè) NULL,整條 IN 判斷就變成 UNKNOWN,結(jié)果為空。
omegafw.sepis.com.cn
rolexfw.sepis.com.cn
patekfw.sepis.com.cn
omega1.gmcwatch.cn
rolex1.gmcwatch.cn
patek1.gmcwatch.cn
omega1.swatchsh.com
rolex1.swatchsh.com
patek1.swatchsh.com
omegawx.paydyj.com
rolexwx.paydyj.com
patekwx.paydyj.com
omegawx.watchku.com
rolexwx.watchku.com
patekwx.watchku.com
- 安全替代方案:用
EXISTS(語義清晰、不懼 NULL、通常更快):EXISTS (SELECT 1 FROM logs l WHERE l.user_id = u.id AND l.action = 'login') - 或者改用
JOIN,但注意去重:SELECT DISTINCT u.* FROM users u JOIN logs l ON u.id = l.user_id WHERE l.action = 'login' - 子查詢?nèi)魩Ь酆希ㄈ?
COUNT()),必須配GROUP BY,否則和外層 JOIN 會(huì)產(chǎn)生隱式笛卡爾積
真正麻煩的從來不是“會(huì)不會(huì)寫”,而是“有沒有意識(shí)到 ON 和 WHERE 的邊界在哪”、“有沒有驗(yàn)證過中間結(jié)果集大小”、“有沒有確認(rèn)子查詢返回的列是否完整暴露”。這些點(diǎn)不手動(dòng)查一遍 EXPLAIN 或跑個(gè)小樣本,光靠語法檢查根本發(fā)現(xiàn)不了。