用窗口函数COUNT(*) OVER(PARTITION BY group_col, target_col)标记每行重复次数,再WHERE cnt>1筛选原始记录;旧版数据库需用自关联或子查询JOIN,注意NULL处理与索引优化。

怎么用 GROUP BY + COUNT 找出分组内某字段“不唯一”的记录
直接查出那些在分组中出现多次的值,而不是只返回计数结果。核心是:先分组聚合计数,再把计数结果作为条件反推原始数据。
常见错误是只写 SELECT group_col, target_col, COUNT(*) FROM t GROUP BY group_col, target_col HAVING COUNT(*) > 1——这只能告诉你哪些(分组+目标值)组合重复了,但拿不到原始行;如果目标是“把所有重复的原始记录捞出来”,必须连回原表或用窗口函数。
- 最稳方案是窗口函数:
COUNT(*) OVER (PARTITION BY group_col, target_col),然后在外层WHERE cnt > 1 - 兼容旧版数据库(如 MySQL 5.6)只能用自关联或子查询,性能差且易漏 NULL 分组
- 注意
GROUP BY group_col, target_col中的target_col如果为 NULL,会被归为同一组——多数引擎把所有 NULL 当作相同值,业务上是否合理需确认
COUNT(*) OVER (PARTITION BY ...) 是不是万能解法
它能直接在每行附带一个计数,适合“标记并筛选重复行”,但有三个实际限制:
- MySQL 5.7 之前不支持窗口函数,强行用会报
ERROR 1064 - PostgreSQL 和 MySQL 8.0+ 支持,但
PARTITION BY里不能用表达式(如TRIM(name)),得先在子查询里处理好 - 如果
group_col和target_col组合基数极高(比如千万级唯一组合),OVER会触发大量内存排序,可能 OOM;此时不如先建临时去重表再 JOIN
示例(MySQL 8.0+):
SELECT * FROM ( SELECT *, COUNT(*) OVER (PARTITION BY dept_id, job_title) AS cnt FROM employees) t WHERE cnt > 1;
为什么不能只靠 GROUP BY + HAVING 返回原始记录
GROUP BY 本质是降维聚合,每组只返回一行。即使你写 SELECT *, COUNT(*),数据库也会报错或随机取某一行的非聚合列值(MySQL 5.7+ 默认拒绝)。
- 想看到所有重复的原始行,
GROUP BY必须配合子查询或 JOIN,例如:SELECT e.* FROM employees e INNER JOIN (SELECT dept_id, job_title FROM employees GROUP BY dept_id, job_title HAVING COUNT(*) > 1) dup ON e.dept_id = dup.dept_id AND e.job_title = dup.job_title - 这个写法在
job_title为 NULL 时可能失效:因为NULL = NULL永远为 false,得改用IS NOT DISTINCT FROM(PostgreSQL)或COALESCE(job_title, 'NULL')(MySQL)做安全比较 - 索引建议:给
(dept_id, job_title)建联合索引,能加速子查询中的GROUP BY和外层JOIN
NULL 值在重复判定里到底算不算“相同”
绝大多数主流数据库(MySQL、PostgreSQL、SQL Server)在 GROUP BY 和窗口函数的 PARTITION BY 中,都把所有 NULL 视为相等——即 (1, NULL) 和 (1, NULL) 属于同一组。但这不等于业务逻辑上该如此。
- 如果你的业务认为 “未知职位(NULL)” 和 “未知职位” 不应算重复(比如来自不同录入渠道),就得提前清洗:
COUNT(*) OVER (PARTITION BY dept_id, COALESCE(job_title, UUID())) - 检查 NULL 占比:先跑
SELECT COUNT(*) FILTER (WHERE job_title IS NULL) AS null_cnt, COUNT(*) FROM employees WHERE dept_id = 'D001'(PostgreSQL)或等效写法,避免误判 - 别依赖
DISTINCT预过滤——SELECT DISTINCT *会把整行去重,而你要的是“按某些字段组合去重”,语义完全不同
真正容易被忽略的是:当 target_col 本身是字符串时,末尾空格、大小写、不可见字符(如 \u200B)都会影响分组结果,但肉眼难识别。动手前先 SELECT HEX(job_title), LENGTH(job_title) FROM employees WHERE dept_id = 'X' LIMIT 5 看一眼原始字节。

















