group by + having count(*) > 1 是识别重复记录的标准解法;需先按判重字段分组,再用having过滤,where不能用于聚合条件,查完整重复行须结合窗口函数或子查询。

GROUP BY + HAVING COUNT(*) > 1 是最稳的起点
直接上手就写 WHERE COUNT(*) > 1 肯定报错——COUNT(*) 是聚合结果,WHERE 在分组前执行,根本看不到它。必须用 HAVING,它专为过滤分组后的聚合值而生。
查单列重复(比如邮箱):SELECT email, COUNT(*) FROM users GROUP BY email HAVING COUNT(*) > 1
查多列组合重复(比如用户ID+下单日期):SELECT user_id, order_date, COUNT(*) FROM orders GROUP BY user_id, order_date HAVING COUNT(*) > 1
-
COUNT(*)比COUNT(email)更可靠:后者会跳过NULL值,可能让含空值的重复组“隐身” - MySQL 5.7 默认不支持
ONLY_FULL_GROUP_BY以外的字段出现在SELECT列表里,别随手加id或created_at,否则报错 - 如果业务上认为大小写/空格不敏感(如手机号),得提前清洗:
GROUP BY LOWER(TRIM(phone))
要看到所有重复的原始行?别硬 GROUP BY,改用窗口函数
上面的写法只返回分组摘要,拿不到每条重复记录的完整字段(比如操作人、时间戳、来源渠道)。这时候 ROW_NUMBER() OVER (PARTITION BY ...) 才是正解。
示例(PostgreSQL / SQL Server / MySQL 8.0+):SELECT *, ROW_NUMBER() OVER (PARTITION BY email ORDER BY id) AS rn FROM users,再套一层 WHERE rn > 1 就能拿到所有“多余副本”。
-
PARTITION BY决定按什么算“一组”,就是你定义的重复键 -
ORDER BY id必须写,否则编号顺序不固定,rn = 1的“保留行”每次都不一样 - SQLite 不支持窗口函数,只能用自连接或子查询,性能差且容易漏掉
NULL匹配
大表跑不动?先看索引,再换 EXISTS
千万级表上直接 GROUP BY email 可能卡死,尤其 email 没索引时。别急着优化SQL,先查执行计划:EXPLAIN SELECT ... 看是不是全表扫描。
如果只是想确认“有没有重复”,不需要知道具体次数,EXISTS 往往更快:SELECT DISTINCT email FROM users u1 WHERE EXISTS (SELECT 1 FROM users u2 WHERE u2.email = u1.email AND u2.id != u1.id)
- 有索引时,
EXISTS是走索引查找,GROUP BY是排序+分组,前者通常快几倍 - 但
EXISTS无法返回重复次数,也难做ORDER BY cnt DESC排序 - 上线前务必加
LIMIT 100测速,避免拖垮生产库
JOIN 后 COUNT(*) 总是 1?那是连接逻辑错了
很多人把主表和日志表 JOIN 后再 GROUP BY,结果 COUNT(*) 全是 1——不是数据没重复,是 JOIN 把一行主记录炸成了多行,COUNT 统计的是连接后行数,不是业务意义上的重复。
正确做法分两步:先在单表上用 GROUP BY + HAVING 定位重复键(如 order_no),再用这个结果 JOIN 原表取详情。
- 别对宽表直接
GROUP BY *,多数数据库不支持,且时间戳、ID 这类唯一字段一加,永远归不了组 - LEFT JOIN 自查时,
ON a.email = b.email AND a.id != b.id缺了a.id != b.id,会导致每条记录都和自己匹配一次 - 老版本 MySQL(ROW_NUMBER() 模拟,比自连接清晰可控











