子查询去重必须在聚合之前,否则join放大行数导致count、sum失真;正确做法是将distinct或row_number()置于最内层子查询中,并确保相关字段有覆盖索引。

子查询去重必须在聚合之前
聚合翻倍不是函数错了,是逻辑被重复行污染了。JOIN 或关联放大行数后才执行 COUNT、SUM,结果自然失真。比如 users 和 orders 一对多关联,一个用户 3 笔订单,COUNT(*) 就算出 3 —— 这不是统计“用户”,是在统计“连接后的记录”。
常见错误现象:SELECT user_id, COUNT(*) FROM v_user_orders GROUP BY user_id 返回值远大于 SELECT COUNT(*) FROM users,说明视图输出已膨胀;加 DISTINCT 在外层无效,因为聚合早已完成。
- 去重动作必须压到最内层:先确保参与聚合的维度本身不重复,再分组
- 正确写法是把
DISTINCT或窗口函数放在子查询里,而不是视图定义或外层SELECT - 例如统计每个用户的订单笔数(去重后):
SELECT user_id, COUNT(*) AS order_count FROM (SELECT DISTINCT user_id, order_id FROM orders) AS deduped GROUP BY user_id;
用 ROW_NUMBER() 取每组最新完整行
当你要保留右表全部字段(不止一个汇总值),且明确要某一条(如最新、最高分),ROW_NUMBER() 是最可控的方式。它不依赖时间字段是否唯一,也不怕并列值干扰。
MySQL 8.0+、PostgreSQL、SQL Server 支持;MySQL 5.7 不支持,得换相关子查询。
- 子查询中用
PARTITION BY user_id ORDER BY created_at DESC编号,外层筛rn = 1 - 注意
PARTITION BY和ORDER BY字段必须有覆盖索引,否则触发磁盘排序 - 示例(取每个用户最新一条订单):
SELECT u.id, u.name, p.order_no, p.created_at FROM users u LEFT JOIN ( SELECT user_id, order_no, created_at, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn FROM orders ) p ON p.user_id = u.id AND p.rn = 1;
GROUP BY 子查询没去重?先验证唯一性
嵌套子查询里写了 GROUP BY user_id,但外层结果仍有重复,大概率是子查询本身没收敛到唯一键。比如脏数据导致同一 user_id 出现在多行,或 GROUP BY 漏了关键约束(如该加 status 却没加)。
检查方法很简单:单独执行子查询,SELECT user_id, COUNT(*) FROM orders GROUP BY user_id,看有没有 user_id 对应多于 1 行。
- 确保子查询的
GROUP BY字段和外层关联键完全一致(只GROUP BY user_id,别多加status等无关字段) - 严格模式下,
SELECT *跟GROUP BY一起用会报错:SQLSTATE[42000],非分组字段必须套聚合函数 - 想取每组最新完整记录,别硬写
MAX(order_no)+MAX(created_at)—— 它们未必来自同一行
性能陷阱:子查询去重没索引就慢
子查询本身不增加逻辑开销,但 DISTINCT 或 ROW_NUMBER() 需要排序或哈希。没索引时,全表扫描 + 临时排序会拖垮整个查询。
必须检查的点:
-
DISTINCT user_id, order_id需要联合索引(user_id, order_id) -
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC)要求索引覆盖user_id和created_at - 用
EXPLAIN确认子查询是否命中索引;没走索引时,GROUP BY可能因临时表大小限制截断结果
真正容易被忽略的是:索引字段顺序不能反。比如 (created_at, user_id) 对 PARTITION BY user_id ORDER BY created_at DESC 基本无效。










