having count(*) = 1 是筛选分组中唯一出现记录的核心条件,必须配合 group by 使用,用于找出分组键在整个表中仅出现一次的组;它不能在 where 中使用,且需注意 null 处理、大小写敏感性及获取完整行时需用子查询或窗口函数。

HAVING COUNT(*) = 1 是找“唯一出现记录”的核心条件
想从分组结果里挑出只出现一次的值,HAVING COUNT(*) = 1 是最直接的写法。它必须配合 GROUP BY 使用,因为 COUNT(*) 是聚合函数,不能在未分组的查询中直接用于过滤行——用 WHERE 会报错,这是初学者最常卡住的地方。
典型场景:查用户表中邮箱不重复的用户、查订单表里只下一单的客户ID、查日志中仅触发一次的错误码。
-
GROUP BY字段必须是你关心的“候选唯一值”,比如email或user_id - SELECT 列建议只包含
GROUP BY字段和COUNT(*),避免歧义(除非明确用了 ANY_VALUE 或启用了 ONLY_FULL_GROUP_BY 严格模式) - 如果还要取原始行的其他字段(如用户名、时间),不能直接 SELECT *,得用子查询或窗口函数兜底
用子查询补全原始行数据时,别漏掉关联条件
单纯 GROUP BY + HAVING 只能返回分组键和计数,但业务常需要整行数据。这时得套一层子查询或 JOIN。容易出错的是没把外层主键和内层分组键对齐,导致结果膨胀或丢失。
例如查邮箱唯一的完整用户记录:
SELECT u.* FROM users u INNER JOIN ( SELECT email FROM users GROUP BY email HAVING COUNT(*) = 1 ) uniq ON u.email = uniq.email;
- JOIN 条件
u.email = uniq.email缺一不可;写成u.id = uniq.email这类类型/语义错位会静默返回空 - 如果
email允许 NULL,GROUP BY会把所有 NULL 归为一组,COUNT(*) = 1可能误判——需提前用WHERE email IS NOT NULL过滤 - 性能上,确保
email有索引,否则子查询可能全表扫描
替代方案:用窗口函数避免两次扫描,但注意 MySQL 版本
MySQL 8.0+ 或 PostgreSQL 可用 COUNT(*) OVER (PARTITION BY ...),在单次扫描中计算频次,再用外层 WHERE 过滤。比子查询更高效,尤其大数据量时。
示例(MySQL 8.0+):
SELECT * FROM ( SELECT *, COUNT(*) OVER (PARTITION BY email) AS cnt FROM users ) t WHERE cnt = 1;
- 窗口函数写法无需
GROUP BY,也不压缩行数,天然保留原始字段 - MySQL 5.7 及更早版本不支持窗口函数,强行运行会报错
ERROR 1064 (42000) -
PARTITION BY的字段要和你要判定“唯一性”的字段完全一致,多一个少一个都会改变分组逻辑
NULL 值和字符大小写是两个隐形陷阱
COUNT(*) 会统计含 NULL 的整行,但 GROUP BY 对 NULL 的处理是“所有 NULL 视为同一组”。这意味着多个 NULL 值会被算作一次重复,而非各自独立——如果你的业务认为 NULL 是有效值且应单独计数,就得先用 COALESCE(email, CONCAT('NULL_', UUID())) 之类方式打标。
另一个坑是大小写敏感性:
- MySQL 默认 utf8mb4_0900_as_cs(区分大小写)时,
'A@B.COM'和'a@b.com'被视为不同值 - 但多数业务希望邮箱去重不区分大小写,此时得统一转小写:
GROUP BY LOWER(email) - PostgreSQL 默认大小写敏感,同样需显式用
LOWER()或带citext扩展类型
真正难的不是写出 HAVING COUNT(*) = 1,而是想清楚“唯一”在你业务里到底指什么:是数据库值的字面唯一?还是业务语义上的唯一?后者往往要先清洗、标准化、再分组。










