应先聚合再JOIN:对明细表提前按关联字段分组汇总,再与宽表连接,避免中间结果集爆炸;LEFT JOIN中COUNT(*)统计行数、COUNT(列)忽略NULL;WHERE条件应移至ON子句以保全左表数据;GROUP BY字段须显式出现在SELECT或聚合函数中。

GROUP BY 前先 JOIN 还是后 JOIN?
绝大多数人默认先 JOIN 再 GROUP BY,但这是性能陷阱的起点。数据库得先把所有匹配行拼出来,再筛、再分组——中间结果集可能爆炸式膨胀,尤其一对多关联时。
正确做法是:能提前聚合的,就在 JOIN 前完成。比如统计每个用户的订单数和总金额,别急着把 users 和 orders 全连起来,先对 orders 按 user_id 聚合出汇总字段,再和 users 关联。
- 适用场景:
JOIN一侧是宽表(如用户信息),另一侧是明细表(如订单、日志) - 错误现象:
EXPLAIN显示rows高达百万级,Using temporary; Using filesort - 示例(错):
SELECT u.name, COUNT(o.id), SUM(o.amount) FROM users u LEFT JOIN orders o ON u.id = o.user_id GROUP BY u.id - 示例(对):
SELECT u.name, co.cnt, co.total FROM users u LEFT JOIN (SELECT user_id, COUNT(*) AS cnt, SUM(amount) AS total FROM orders GROUP BY user_id) co ON u.id = co.user_id
LEFT JOIN + COUNT 为什么总是 0?
COUNT(*) 在 LEFT JOIN 后会把空关联行也计为 1,而 COUNT(非空列) 才真正跳过 NULL 行——这是最常被忽略的语义差异。
比如想查“每个用户订单数”,用 COUNT(*) 会返回 1(因为左表一行还在),但用 COUNT(o.id) 才返回 0(o.id 是 NULL)。
- 关键区别:
COUNT(*)统计行数,COUNT(col)只统计该列非 NULL 的行 - 常见错误:写
COUNT(o.created_at)却没注意该字段允许 NULL - 安全写法:优先用主键或明确非空的字段,如
COUNT(o.id) - 兼容性提示:MySQL 5.7+ 默认开启
sql_mode=STRICT_TRANS_TABLES,但行为不变;PostgreSQL 完全一致
WHERE 条件放 JOIN 内还是 GROUP BY 后?
放在 ON 子句里,是关联时过滤右表;放在 WHERE 里,是关联完再筛整行——这对 LEFT JOIN 结果影响巨大。
例如查“2024 年下单的用户及其订单数”,若把时间条件写在 WHERE o.created_at >= '2024-01-01',那些没在 2024 年下单的用户就直接被踢出结果;而写在 ON 里,用户还在,只是订单数为 0。
- LEFT JOIN 场景下,业务条件尽量塞进
ON,而非WHERE - 错误现象:本该有 1000 行用户结果,只返回几十行
- 性能影响:WHERE 过滤晚,中间数据量更大;ON 过滤早,JOIN 输入更小
- 示例(错):
... LEFT JOIN orders o ON u.id = o.user_id WHERE o.created_at >= '2024-01-01' - 示例(对):
... LEFT JOIN orders o ON u.id = o.user_id AND o.created_at >= '2024-01-01'
GROUP BY 字段必须出现在 SELECT 中吗?
MySQL 5.7 默认关闭 ONLY_FULL_GROUP_BY,允许 SELECT a, b, COUNT(*) FROM t GROUP BY a(b 没在 GROUP BY 里);但 PostgreSQL、SQL Server、新版本 MySQL 都严格报错:column "t.b" must appear in the GROUP BY clause or be used in an aggregate function。
这不是语法洁癖,而是语义风险:当多行 a 相同但 b 不同时,数据库随便返回哪一行的 b 值,结果不可预测。
- 真实坑点:本地开发用 MySQL 8.0 默认模式跑通,上线到云数据库(如 AWS RDS PostgreSQL)直接报错
- 安全做法:要么加到
GROUP BY,要么包进聚合函数,如MAX(b)或ANY_VALUE(b)(MySQL 特有,慎用) - 迁移提醒:检查
sql_mode,生产环境建议显式开启ONLY_FULL_GROUP_BY
关联 + 分组真正的复杂点不在语法,而在你是否清楚每一行数据从哪来、被谁过滤、在哪个环节膨胀。执行计划里的 rows 和 Extra 字段,比任何文档都诚实。

















