用 group by 分组后配合 having count(*) = 1 是找出唯一出现记录的最常用可靠方式,适用于任意字段组合,不依赖主键或唯一索引。

怎么用 GROUP BY + HAVING 找出唯一出现的记录
直接结论:用 GROUP BY 分组后,配合 HAVING COUNT(*) = 1 是最常用、最可靠的方式。它不依赖主键或唯一索引,适用于任意字段组合去重统计。
关键点在于:先按目标字段分组,再筛出每组仅一条记录的组——这些组对应的原始行,就是“只出现过一次”的记录。
- 如果查单个字段(比如
email),GROUP BY email即可 - 如果查多个字段组合(比如
user_id和status同时唯一),要写全:GROUP BY user_id, status -
COUNT(*)比COUNT(列名)更稳妥,避免该列含NULL时被忽略
为什么不能只用 WHERE + 子查询而不加 GROUP BY
常见错误是想用 WHERE id IN (SELECT id FROM ...) 去套,但这样容易漏数据或报错——因为子查询若没聚合,多数数据库(如 MySQL 5.7+ 严格模式、PostgreSQL)会直接拒绝执行。
更隐蔽的问题是性能:没 GROUP BY 的相关子查询可能对每行都触发一次扫描,数据量稍大就明显变慢。
- 正确结构必须包含
GROUP BY,否则HAVING无意义(HAVING是对分组结果的过滤,不是对原始行) - 有些同学误写成
WHERE COUNT(*) = 1,这是语法错误——聚合函数不能出现在WHERE中 - MySQL 8.0+ 支持窗口函数,可用
COUNT(*) OVER (PARTITION BY ...)先算频次再过滤,但逻辑更绕,且不兼容老版本
如何返回完整原始行,而不仅是分组字段
只写 SELECT email FROM t GROUP BY email HAVING COUNT(*) = 1 只能拿到分组字段本身。要查整行,得把原表和分组结果做 JOIN 或用 IN 子查询包一层。
推荐写法(兼容性好、意图清晰):
SELECT * FROM users WHERE email IN ( SELECT email FROM users GROUP BY email HAVING COUNT(*) = 1 );
- 注意:若
email允许为NULL,IN会失效(NULL NOT IN (...)永真),此时改用JOIN或NOT EXISTS更安全 - PostgreSQL 和 MySQL 8.0+ 支持 CTE,可先
WITH unique_emails AS (...) SELECT u.* FROM users u JOIN unique_emails e ON u.email = e.email,语义更干净 - 别在子查询里选太多字段,尤其是大文本或 JSON 列,会拖慢分组速度
遇到 COUNT 返回 0 或结果为空时该检查什么
查不到数据不一定是逻辑错,很可能是隐性条件干扰了分组结果。
- 确认是否忽略了大小写或前后空格:
TRIM(LOWER(email))分组比直接email更严谨 - 检查字段类型:比如
INT和VARCHAR存的数字 '123' 与 123,在某些数据库中会被视为不同值 - 留意
NULL:同一字段多个NULL会被归为一组,COUNT(*)算进去,但COUNT(字段)不算——这会导致预期外的HAVING结果 - 如果表有分区或物化视图,确保查询走的是最新数据,而不是缓存快照
真正麻烦的永远不是语法,而是数据本身的歧义性——比如手机号带区号、邮箱大小写混用、时间字段精度不一致。先清洗再分组,比硬调 HAVING 更省时间。










