先聚合再JOIN是避免一对多关联导致金额翻倍、计数爆炸的刚性要求,因JOIN仅行级拼接不自动去重,必须通过子查询或CTE对各明细表按业务主键预聚合后再对齐连接。
多数人卡在这一步,是因为误以为必须先 SUM() 或 COUNT() 再去连其他表。其实更常见、更高效的做法是:先 JOIN,再聚合。比如统计每个客户的订单总金额和最新订单时间,应该写成:
INNER JOIN 或 LEFT JOIN 把 customers、orders、order_items 连起来GROUP BY customers.id, customers.name,然后用 SUM(oi.quantity * oi.unit_price) 和 MAX(o.order_date)
只有当某张表本身只提供汇总值(如日志表每天只存一条UV/PV)、或聚合逻辑复杂到无法在主查询中表达时,才考虑“先聚合再关联”。
例如要显示每位客户信息 + 他最近30天的订单数 + 所属城市人口,而城市人口来自另一张静态维度表 cities,这时订单数必须预聚合:
SELECT c.customer_id, c.name, co.order_count, ci.populationFROM customers cLEFT JOIN ( SELECT customer_id, COUNT(*) AS order_count FROM orders WHERE order_date >= CURRENT_DATE - INTERVAL '30 days' GROUP BY customer_id) co ON c.customer_id = co.customer_idLEFT JOIN cities ci ON c.city_code = ci.code;
关键点:
co),否则外部 ON 无法引用GROUP BY 字段必须和子查询中非聚合字段完全一致,否则 PostgreSQL/MySQL 8.0+ 会报错customers 表有千万级,但 orders 中近30天只占0.1%,那这个子查询实际扫描量很小,性能可控WHERE c.customer_id IN (SELECT customer_id FROM orders...) —— 这种写法无法利用索引,且不返回 order_count = 0 的客户当聚合逻辑变多(比如同时要近7天、近30天、近90天订单数),CTE 比多层子查询更易维护:
WITH recent_orders AS ( SELECT customer_id, COUNT(*) FILTER (WHERE order_date >= CURRENT_DATE - INTERVAL '7 days') AS cnt_7d, COUNT(*) FILTER (WHERE order_date >= CURRENT_DATE - INTERVAL '30 days') AS cnt_30d, SUM(amount) FILTER (WHERE order_date >= CURRENT_DATE - INTERVAL '30 days') AS sum_30d FROM orders GROUP BY customer_id)SELECT c.name, ro.cnt_7d, ro.sum_30d, ci.provinceFROM customers cLEFT JOIN recent_orders ro ON c.customer_id = ro.customer_idLEFT JOIN cities ci ON c.city_code = ci.code;
注意:
FILTER 是 PostgreSQL 特有语法;MySQL 需用 SUM(IF(...)),SQL Server 用 SUM(CASE WHEN ... THEN ... END)
SELECT *,尤其当后续 JOIN 的表也有 id 字段时,ro.id 和 c.id 会冲突一旦用了子查询或 CTE 预聚合,关联时的细节错误会直接导致结果失真:
LEFT JOIN 而用了 INNER JOIN:客户无订单就彻底消失,而不是显示 order_count = NULL
customer_id 是 BIGINT,而 customers 表里是 VARCHAR,某些数据库会静默转成字符串再比较,导致索引失效DISTINCT:比如用户有多个收货地址,JOIN addresses 后再 COUNT(order_id) 会重复计数——应先用 COUNT(DISTINCT order_id) 或提前去重ON 而非 WHERE:在 LEFT JOIN 中,把过滤条件放在 ON 里会让右表记录被过滤掉,等效于 INNER JOIN
真正难的不是写出语法正确的 SQL,而是判断哪一层该聚合、哪一层该关联、以及 NULL 到底该代表“无数据”还是“数据缺失”。这得结合业务语义逐层验证,不能只看执行计划。