子查询去重统计不能直接用DISTINCT+GROUP BY,因二者语义冲突:DISTINCT作用于整行去重,而业务常需按某维度(如user_id)去重后再按其他字段(如region)分组统计;硬套会导致逻辑错位或语法报错,正确解法是先用子查询构造不重复统计单元(如ROW_NUMBER()标记首单、EXISTS判断存在性),再外层聚合。

子查询去重统计为什么不能直接用 DISTINCT + GROUP BY
因为 DISTINCT 作用于整行,而你真正想“去重”的往往只是某个业务维度(比如每个用户只算一次购买,不管买了几单),但又要按其他字段(比如地区、月份)分组统计。这时候硬套 GROUP BY 会把同一用户的多次行为拆开,DISTINCT 又无法嵌套在聚合函数里直接控制粒度。
正确解法是:先用子查询把“去重逻辑”提前收口,再对外层做聚合。核心思路不是“去掉重复行”,而是“构造不重复的统计单元”。
- 典型错误:写成
SELECT region, COUNT(DISTINCT user_id) FROM orders GROUP BY region—— 这看似可行,但一旦要加更复杂的条件(比如“只统计首次下单后 7 天内完成支付的用户”),COUNT(DISTINCT ...)就无能为力了 - 子查询能让你在去重前自由加
WHERE、JOIN、ROW_NUMBER()等逻辑,把“谁该被算一次”这件事彻底掌控 - 注意:MySQL 5.7 或更早版本不支持在
FROM子句中对同一张表做非相关子查询引用(即所谓 “You can't specify target table for update in FROM clause” 类错误),此时得用派生表 alias 套一层
用 ROW_NUMBER() 在子查询中标记首次行为
这是最常用也最可控的去重方式,尤其适合“每个用户只计第一次动作”的场景,比如首购用户数、新客留存。
关键点在于:用 ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY create_time) 给每个用户的行为排序,外层只取 rn = 1 的记录,再按需分组统计。
SELECT region, COUNT(*) AS new_user_count
FROM (
SELECT region, user_id,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY create_time) AS rn
FROM orders
WHERE status = 'paid' AND create_time >= '2024-01-01'
) t
WHERE rn = 1
GROUP BY region;
-
PARTITION BY user_id确保编号按用户隔离,不会跨用户错乱 -
ORDER BY create_time决定哪条是“第一次”;若要按注册时间判断,就得 JOIN users 表或改用signup_time - 子查询里必须包含外层需要的所有字段(如
region),否则外层无法分组 - 性能提示:给
(user_id, create_time)建联合索引能显著加速窗口函数执行
用 EXISTS 替代 IN 避免 NULL 和重复匹配问题
当去重逻辑是“某用户是否满足某条件”,而非“取哪一条记录”时,EXISTS 比 IN 更安全、语义更清晰,且不会因子查询返回 NULL 导致整行过滤失效。
例如:统计“至少下过一单且订单总金额 ≥ 500 的用户所在城市数”——重点是“存在性”,不是取具体订单。
SELECT city, COUNT(*) AS high_value_user_count
FROM users u
WHERE EXISTS (
SELECT 1 FROM orders o
WHERE o.user_id = u.id
AND o.status = 'paid'
GROUP BY o.user_id
HAVING SUM(o.amount) >= 500
)
GROUP BY city;
-
EXISTS子查询里用GROUP BY + HAVING是关键,它把聚合判断提前到子查询内部,避免外层对用户重复计数 - 不要写成
WHERE u.id IN (SELECT user_id FROM orders GROUP BY ...):如果子查询结果含NULL,整个IN判断会返回UNKNOWN,导致用户被意外排除 -
SELECT 1是惯例,实际不查数据,只确认存在性;用SELECT *不影响逻辑但可能降低可读性
多层子查询嵌套时如何避免性能雪崩
三层以上子查询不是语法错误,但很容易让优化器放弃使用索引,尤其是子查询里带 GROUP BY 或 ORDER BY 时,可能触发临时表 + 文件排序。
实操中优先检查三点:
- 最内层子查询是否真的需要全部字段?尽量只
SELECT外层依赖的列,减少中间结果集大小 - 是否存在可提前下推的过滤条件?比如外层
WHERE region = 'shanghai',应尽量移到内层子查询中,避免全量计算后再过滤 - 考虑用 CTE(
WITH)替代嵌套,尤其在 PostgreSQL / SQL Server / MySQL 8.0+ 中,CTE 可读性更好,部分引擎还能做物化优化 - 用
EXPLAIN对照执行计划:关注是否出现Using temporary; Using filesort,以及扫描行数是否远超预期
真正难的不是写出多层子查询,而是判断哪一层该承担“去重”,哪一层该承担“聚合”,中间少一层逻辑错位,结果就偏了。

















