跨表聚合统计需优化执行路径:严格GROUP BY、LEFT JOIN慎用COUNT(*)、过滤条件下推、预聚合降载、组合索引匹配执行顺序。

跨表聚合统计不能只靠写对JOIN和GROUP BY,关键在让数据库少算、早筛、不回表。
GROUP BY必须覆盖所有非聚合字段,否则MySQL/PostgreSQL直接报错
严格模式下,SELECT user_id, name, COUNT(*) FROM orders JOIN users ON orders.user_id = users.id GROUP BY user_id 会失败,因为 name 既没参与分组,也没套聚合函数。这不是语法错误,是数据库强制要求结果确定性。
- 补全分组:
GROUP BY user_id, name(前提是每个user_id对应唯一name) - 用聚合取值:
MAX(name)或ANY_VALUE(name)(MySQL 5.7+ 支持,但 PostgreSQL 不认ANY_VALUE) - 别关
ONLY_FULL_GROUP_BY——关了只是掩盖问题,上线换库可能崩
LEFT JOIN后COUNT(*)和COUNT(字段)行为完全不同
用 LEFT JOIN 统计“每个用户订单数”时,COUNT(*) 会把没订单的用户行也计为 1,而 COUNT(orders.id) 才真正返回 0。这是最容易出数据偏差的地方。
- 要统计“有记录的条数”,一律用
COUNT(右表主键),比如COUNT(o.id) -
AVG(orders.amount)遇到 NULL 直接返回 NULL,不是 0,得写成COALESCE(AVG(o.amount), 0) - WHERE 里对右表字段过滤(如
WHERE o.status = 'paid')会让LEFT JOIN退化为INNER JOIN,条件必须挪到ON里:LEFT JOIN orders o ON u.id = o.user_id AND o.status = 'paid'
大表JOIN前务必先过滤,别等关联完再WHERE
SELECT * FROM orders JOIN users ON orders.user_id = users.id WHERE users.status = 'active' 是典型低效写法:先全量关联百万订单和用户,再筛活跃用户。正确做法是把过滤提前。
- 子查询预过滤:
JOIN (SELECT id FROM users WHERE status = 'active') u ON o.user_id = u.id - ON 内嵌条件:
JOIN users u ON o.user_id = u.id AND u.status = 'active'(注意:必须写在ON,不是WHERE) - 时间类强过滤(如
create_time > '2026-04-01')优先下推到主表子查询中,避免中间结果膨胀
聚合尽量下推,别在最终结果集上硬算
订单明细表千万行,JOIN 订单表后再按 order_id 分组,等于把每条明细都拉出来再合并——IO 和内存压力陡增。应该先在明细表里聚好,再轻量关联。
- 用 CTE 或子查询预聚合:
WITH order_totals AS (SELECT order_id, SUM(qty * price) AS total FROM order_items GROUP BY order_id) - 关联时只连精简后的结果,而不是原始明细
- 若报表固定按
region + category + date统计,直接建预聚合表,比每次实时 JOIN 快一个数量级
最常被忽略的一点:索引不是建了就生效,JOIN 字段、WHERE 字段、GROUP BY 字段如果分散在不同列,单列索引效果极差;组合索引顺序必须匹配实际执行路径,比如 (status, create_time, user_id) 才能同时支撑过滤与关联。










