唯一可靠路径是GROUP BY+HAVING COUNT(*)>1,因WHERE在分组前执行无法访问聚合结果;漏写HAVING或误用COUNT(email)会漏判NULL重复;多字段需全列GROUP BY;查完整重复行须用IN子查询或窗口函数。

GROUP BY + HAVING COUNT(*) > 1 是唯一可靠路径
想识别重复出现的字段组合,必须走 GROUP BY 分组 + HAVING COUNT(*) > 1 筛选这条路。任何试图把 COUNT() 塞进 WHERE 的写法都会报错:ERROR: aggregate functions are not allowed in WHERE。数据库执行顺序是 WHERE → GROUP BY → HAVING,聚合结果只在分组后存在。
常见错误包括:
- 漏写
HAVING,导致返回所有分组(含只出现 1 次的),误以为没查到重复 - 在
SELECT中写*或非分组字段(如id),MySQL 5.7+ 默认报错:Expression #1 of SELECT list is not in GROUP BY clause - 用
COUNT(email)替代COUNT(*),当email为NULL时会被忽略,漏掉含空值的重复组
多字段组合重复怎么写 GROUP BY
重复定义依赖业务逻辑,比如「同名同邮箱才算重复」,那 GROUP BY 必须包含全部判断字段,且顺序不影响结果但建议与 SELECT 保持一致。
正确写法示例:
SELECT first_name, last_name, email, COUNT(*) AS cnt FROM users GROUP BY first_name, last_name, email HAVING COUNT(*) > 1;
关键点:
- 子查询或窗口函数中若需关联,
PARTITION BY或IN子句里的字段列表必须和此处完全一致,否则逻辑断裂 - 如果字段可能为
NULL,注意所有NULL会被归为同一组——这是 SQL 标准行为,不是 bug - 字符串字段若需忽略大小写或空格干扰,可用
LOWER(TRIM(email)),但会失效索引,大表慎用
要拿到所有重复行的原始记录,别只靠 GROUP BY
GROUP BY + HAVING 只返回每组一条摘要行,拿不到 id、created_at 等原始字段。真要导出或删重,得额外一步。
两种主流方案:
- 兼容旧版(如 MySQL 5.7):用
IN子查询,但注意IN会跳过NULL值,需补OR email IS NULL - 推荐新版本(MySQL 8.0+/PostgreSQL):窗口函数
COUNT(*) OVER (PARTITION BY ...),可直接在原表上标记每行重复次数
示例(窗口函数):
SELECT *, COUNT(*) OVER (PARTITION BY email) AS cnt FROM users WHERE cnt > 1;
注意:这个写法在 PostgreSQL/Oracle 可直接运行;MySQL 8.0+ 需套一层子查询,因为不能在同一个 SELECT 中引用窗口别名 cnt。
大表上跑重复检测,性能陷阱比语法错误更致命
没索引的字段上跑 GROUP BY,等于全表扫描+内存排序,100 万行就可能超时。优化优先级高于写法炫技。
实操建议:
- 先确认重复字段是否有索引:
CREATE INDEX idx_users_email ON users(email); - 若只需判断“是否存在重复”,不用计数,改用
EXISTS写法,有索引时快一个数量级 - 避免在
GROUP BY中用函数包裹字段(如LOWER(email)),否则索引失效 - 生产环境首次执行前,先加
LIMIT 10测试结果和耗时
真正卡住人的往往不是“怎么写”,而是“为什么慢”和“NULL 怎么算”——这两点在上线前必须验证清楚。

















