JOIN 必须写在 GROUP BY 之前,SQL 执行顺序固定为 FROM → JOIN → WHERE → GROUP BY → HAVING → SELECT;先通过 JOIN 拼出宽表,再分组聚合,否则子查询预聚合会漏数据、无法统计零记录分组。

JOIN 必须写在 GROUP BY 之前
SQL 执行顺序是固定的:FROM → JOIN → WHERE → GROUP BY → HAVING → SELECT。这意味着你不能“先分组再连接”,必须先把所有表用 JOIN 拼成一张宽表,再对这张临时结果做 GROUP BY。
常见错误是写成子查询先聚合再 JOIN,比如:
SELECT d.name, e.avg_salary FROM dept d JOIN (SELECT dept_id, AVG(salary) AS avg_salary FROM emp GROUP BY dept_id) e ON d.id = e.dept_id;
这看似简洁,但会漏掉 emp 中 dept_id IS NULL 的记录,且无法统计“零员工部门”的人数——因为子查询已把空部门过滤掉了。
- 正确做法:所有表先
JOIN,再统一GROUP BY - 如果某张表可能无匹配(如部门没员工),用
LEFT JOIN保主表全量 -
WHERE条件尽量放在JOIN后、GROUP BY前,避免无效行参与连接
LEFT JOIN 后 COUNT(*) 和 COUNT(字段) 完全不同
用 LEFT JOIN 保留左表全部记录时,右表无匹配会导致字段为 NULL,但 COUNT(*) 和 COUNT(字段) 行为差异极大:
-
COUNT(*)统计的是每组的**行数**,哪怕右表全为NULL,这一行也计入——所以常误以为“每个部门只有一条记录” -
COUNT(t2.id)只统计t2.id非NULL的数量,无匹配时结果为0,这才是你要的“部门员工数” - 聚合结果含
NULL(如SUM(t2.amount))时,前端展示易出错,建议用COALESCE(SUM(t2.amount), 0)显式转0
例如查每个客户的订单数和总金额:
SELECT u.id, u.name,
COUNT(o.order_id) AS order_count,
COALESCE(SUM(o.amount), 0) AS total_amount
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id, u.name;
GROUP BY 必须覆盖 SELECT 中所有非聚合字段
MySQL 5.7+ 默认开启 sql_mode=ONLY_FULL_GROUP_BY,PostgreSQL/Oracle/SQL Server 全部严格校验。只要 SELECT 里出现非聚合字段(如 d.name),它就必须出现在 GROUP BY 中。
- 错误写法:
SELECT d.name, u.name, COUNT(*) FROM dept d JOIN user u ... GROUP BY d.id——u.name既没聚合也没分组,直接报错 - 正确解法只有三种:
– 补全GROUP BY d.id, d.name, u.name(语义上通常是错的,因为一个部门多人)
– 改用聚合函数,如MAX(u.name)或GROUP_CONCAT(u.name)
– 确认业务逻辑允许后,用ANY_VALUE(u.name)(MySQL 特有,不跨库) - 多表同名字段(如
id、name)必须加前缀,GROUP BY id是非法的,得写GROUP BY d.id
三张及以上表 JOIN 时,DISTINCT 要慎用
当涉及订单、商品、分类三张表时,一条订单可能关联多个商品,再关联多个分类,容易因笛卡尔积导致重复计数。比如统计每个分类下的订单总金额,直接 SUM(o.amount) 会把同一笔订单按商品数翻倍。
- 优先用
COUNT(DISTINCT o.order_id)替代COUNT(*)防重复计数 - 金额类指标若需去重,通常得先用子查询聚合订单维度(如
SELECT order_id, SUM(amount) FROM orders GROUP BY order_id),再与其他表JOIN,而不是硬套DISTINCT -
GROUP_CONCAT(DISTINCT ...)在拼接去重字符串时有用,但性能随数据量下降明显,超万行慎用
跨表分组统计真正难的不是语法,而是搞清“哪张表是主干、哪些关联会放大行数、聚合粒度是否一致”。字段一加错、JOIN 类型一选错,结果就偏了,而且不容易被肉眼发现。

















