用GROUP BY+HAVING可查出重复值(如SELECT email,COUNT() FROM users GROUP BY email HAVING COUNT()>1),但无法返回完整重复行;查整行需用ROW_NUMBER()窗口函数(MySQL 8.0+)或自连接(5.7及以下)。

用 GROUP BY + HAVING 找出重复值
直接查重复数据,核心思路是先分组再筛数量大于 1 的组。不能只靠 SELECT * 加 DISTINCT,那只会去重,不会告诉你哪些重复了。
假设表 users 里想查邮箱重复的记录:
SELECT email, COUNT(*) AS cnt FROM users GROUP BY email HAVING COUNT(*) > 1;
-
GROUP BY email把相同邮箱归为一组 -
HAVING COUNT(*) > 1是关键:它在分组后过滤,只保留出现次数 ≥2 的邮箱 - 注意不能用
WHERE COUNT(*) > 1—— 聚合函数不能在WHERE里用,会报错Invalid use of aggregate function - 如果要查整行重复(所有字段都一样),
GROUP BY后得列出所有字段,或改用窗口函数(见下节)
查出全部重复行(含原始 ID)
上面只返回重复值和次数,但业务中常需要知道“哪几条记录重复了”。这时得关联原表或用窗口函数。
推荐用 ROW_NUMBER() 窗口函数(MySQL 8.0+、PostgreSQL、SQL Server 都支持):
SELECT id, email, name
FROM (
SELECT id, email, name,
ROW_NUMBER() OVER (PARTITION BY email ORDER BY id) AS rn
FROM users
) t
WHERE rn > 1;-
PARTITION BY email按邮箱分组,每组内按id排序 -
ROW_NUMBER()给每组里的行编号,重复邮箱的第一条是 1,第二条是 2…… - 外层
WHERE rn > 1就精准取出所有“非首次出现”的重复行 - 如果用
COUNT(*) OVER (PARTITION BY email)替代ROW_NUMBER(),也能拿到每行所属组的总数量,但筛选逻辑稍绕
MySQL 5.7 或更老版本怎么处理?
老 MySQL 不支持窗口函数,也没 CTE(WITH 子句),得靠自连接或子查询。
常用方案是用 INNER JOIN 自连接找重复:
SELECT DISTINCT u1.* FROM users u1 INNER JOIN users u2 ON u1.email = u2.email AND u1.id != u2.id;
- 这个语句会返回所有至少有一个“同伴”的行(即存在另一行邮箱相同但 ID 不同)
- 必须加
u1.id != u2.id,否则每行都会和自己匹配,结果全出来 -
DISTINCT是必须的,否则同一个重复组会返回多对组合(比如三条重复邮箱,会生成 6 行中间结果) - 性能上,大表慎用——没有合适索引时,
JOIN可能触发 O(n²) 扫描;给email加索引能明显改善
删除重复数据前务必确认备份
查重复只是第一步,真删之前最容易踩的坑是:没明确保留哪一条。
- 用
ROW_NUMBER()方案时,ORDER BY id决定了谁是第 1 条(保留)、谁是第 2 条起(待删);如果业务要求保留最新时间戳的,就得把ORDER BY改成created_at DESC - 老 MySQL 删除更麻烦,常见写法是先建临时表存要保留的 ID,再
DELETE WHERE id NOT IN (…),但NOT IN遇到NULL会失效,得额外处理 - 无论如何,执行
DELETE前先用SELECT复核一遍结果集,别跳过这步
重复数据逻辑看着简单,但字段组合、保留策略、数据库版本、NULL 值处理,每个点都可能让结果偏移。动手前先跑一遍 SELECT 看实际数据分布,比直接套模板安全得多。


















