用 group by + having 查重复记录,需按所有判重字段分组,并用 having count(*) > 1 筛选;不能用 where 过滤聚合结果;要查完整重复行需自连接或窗口函数;注意 null、空格、大小写等业务细节。

怎么用 GROUP BY + HAVING 找出重复的行
直接查重复记录,核心就是按可能重复的字段分组,再筛出组内行数大于 1 的组。GROUP BY 要覆盖所有你认为“算重复”的列,HAVING COUNT(*) > 1 是筛选条件,不是 WHERE —— 因为聚合结果只能在 HAVING 里过滤。
常见错误:只按主键或自增 ID 分组,结果当然每组只有 1 行;或者漏掉某个业务上关键的判重字段(比如忽略大小写、空格、NULL)。
- 如果判断“完全相同”,就
GROUP BY col1, col2, col3列出所有目标字段 - 如果要忽略大小写,用
GROUP BY LOWER(col1), col2(注意索引失效风险) - NULL 值在 MySQL 中默认被当作相同值参与分组,但某些场景下你可能想排除 NULL,得加
WHERE col1 IS NOT NULL - 示例:
SELECT name, email, COUNT(*) FROM users GROUP BY name, email HAVING COUNT(*) > 1
查重复时为什么不能用 WHERE COUNT(*) > 1
COUNT(*) 是聚合函数,必须配合 GROUP BY 使用,而 WHERE 在分组前执行,根本看不到聚合结果。硬写 WHERE COUNT(*) > 1 会报错:Invalid use of group function。
另一个典型误用是试图在子查询里用 WHERE 过滤聚合结果,却忘了外层没分组——本质还是混淆了执行顺序:WHERE → GROUP BY → HAVING → SELECT。
- 正确路径只有一条:
GROUP BY后接HAVING - 如果还要查出所有重复行的完整数据(不只是分组摘要),得用自关联或窗口函数,不能只靠
HAVING - MySQL 5.7 及以前不支持窗口函数,得用
JOIN或IN (SELECT ...)配合上面的分组结果
查出重复记录的完整行数据(不只是统计)
上面的 GROUP BY + HAVING 只能返回每组一条摘要(比如 name, email, COUNT(*)),但业务常需要看到所有重复的原始记录——比如删掉其中几条,或导出给运营核对。
这时候得把刚才的分组结果当“条件”用,常见做法是自连接或子查询。注意性能:大表上没索引会很慢。
- 推荐写法(带索引友好):
SELECT u1.* FROM users u1 INNER JOIN (SELECT name, email FROM users GROUP BY name, email HAVING COUNT(*) > 1) u2 ON u1.name = u2.name AND u1.email = u2.email - 避免用
IN (SELECT ...),尤其子查询结果多时,MySQL 可能退化成 N+1 查询 - 如果用 MySQL 8.0+,更清晰:加个
COUNT(*) OVER (PARTITION BY name, email)窗口函数,再WHERE cnt > 1 - 别忘了检查
(name, email)是否有联合索引,否则GROUP BY和JOIN都可能全表扫描
NULL 和空字符串容易被当成相同值
MySQL 默认把所有 NULL 当作相等,所以 GROUP BY col 时,所有 col IS NULL 的行会挤进同一组——哪怕你主观上觉得“空邮箱不算重复”。同样,'' 和 ' '(空格)也可能被业务视为等价,但数据库不会自动 trim。
这导致两种坑:一是误报(一堆 NULL 被当重复),二是漏报(带空格的值和干净值没归到一组)。
- 显式排除 NULL:
WHERE col1 IS NOT NULL AND col2 IS NOT NULL再分组 - 标准化空格:
GROUP BY TRIM(col1), TRIM(col2),但注意TRIM()会让索引失效 - 业务强相关字段(如手机号、邮箱),建议提前清洗并建唯一约束,而不是每次靠查重复来补救
- 测试时务必插入含 NULL、空字符串、纯空格的样例数据,不然上线后才发现逻辑偏差
查重复看着简单,真正卡住人的从来不是语法,而是“到底什么算重复”这个业务定义没对齐,以及 NULL/空格/大小写这些细节在分组时悄悄改变了行为。











