结论:过滤条件应提前到子查询或cte中再join,性能通常提升显著。因中间结果集大幅缩减,join计算量可降两个数量级;on与where语义不同,left join中where过滤右表会转为inner join;聚合需先group by再join;索引失效常见于类型不匹配、隐式转换及复合索引顺序错误。

WHERE条件写在JOIN前还是后,性能差十倍?
直接说结论:把过滤条件提前到子查询或CTE里,再JOIN,几乎总是更快。这不是“理论上可能快”,而是执行计划里能明确看到中间结果集从百万行压到几百行——JOIN的计算量直接掉两个数量级。
常见错误现象是:写一个四表JOIN,WHERE里加了status = 'PAID'和created_at > '2025-01-01',但执行计划显示rows_examined高达200万。其实订单表本身只有10万行,问题出在JOIN没做任何剪枝就先展开了。
- JOIN前不筛选:数据库先执行
orders JOIN users JOIN products,中间临时结果可能是10万 × 1万 × 5千行(笛卡尔积风险),哪怕最终只返回100行 - JOIN前筛选:用子查询
(SELECT * FROM orders WHERE status = 'PAID')作为驱动表,再JOIN,输入行数直接从10万降到8000 - MySQL 8.0+和PostgreSQL会尝试自动重排,但遇到
OR、LIKE '%xx'、函数包裹字段(如DATE(created_at))时,优化器大概率放弃下推
LEFT JOIN里ON和WHERE加条件,为什么结果和性能都不同?
ON和WHERE不是同一件事。前者决定“怎么连”,后者决定“连完留哪些”。尤其对LEFT JOIN,放错地方会彻底改变语义和性能。
比如统计每个用户订单数,但只关心2025年之后的订单:
- 错误写法:
LEFT JOIN orders o ON u.id = o.user_id WHERE o.created_at > '2025-01-01'→ 把WHERE加在右表字段上,会把没订单的用户全过滤掉(等价于INNER JOIN) - 正确写法:
LEFT JOIN (SELECT * FROM orders WHERE created_at > '2025-01-01') o ON u.id = o.user_id→ 先筛订单,再左连,用户全量保留 - 性能差异:后者让
orders扫描行数从全表降到1/10,且o.user_id索引能被真正用上;前者可能触发全表扫描+临时表排序
聚合类查询必须先GROUP BY再JOIN,否则就是自找麻烦
涉及COUNT、SUM等聚合时,“先JOIN再GROUP BY”是典型反模式。订单+用户+商品三表关联后聚合,中间结果动辄千万行;而“先按user_id聚合订单,再JOIN用户信息”,JOIN对象只是几百行的汇总表。
看这个对比:
- 慢写法:
SELECT u.name, COUNT(o.id) FROM users u LEFT JOIN orders o ON u.id = o.user_id GROUP BY u.id→orders全量参与JOIN - 快写法:
SELECT u.name, co.order_count FROM users u LEFT JOIN (SELECT user_id, COUNT(*) AS order_count FROM orders WHERE status = 'PAID' GROUP BY user_id) co ON u.id = co.user_id - 关键点:
WHERE status = 'PAID'必须放在子查询里,否则聚合前没过滤,COUNT算的是所有订单(包括已取消的) - 额外收益:子查询结果集小,
co.user_id天然走索引,JOIN几乎无压力
索引失效的三个隐蔽坑,Filter再早也白搭
就算你把所有过滤都提前了,如果索引没配对,性能照样崩。最容易被忽略的是类型不一致和隐式转换。
-
orders.user_id是INT,但JOIN字段写成users.id::TEXT→ 强制转字符串,索引失效 -
WHERE created_at > '2025-01-01'看着正常,但如果created_at是TIMESTAMP WITH TIME ZONE,而传入字符串没带时区,PG可能放弃索引 - 复合索引顺序错:想加速
WHERE status = 'PAID' AND created_at > '2025-01-01' GROUP BY user_id,但建的是INDEX idx_orders_created_status (created_at, status)→status在第二位,范围查询时无法高效跳过
真正卡住查询的,往往不是逻辑多复杂,而是某一行索引建错了,或者某个类型悄悄变了。执行计划里的type: ALL或key: NULL就是最直白的警告。











