having count(*) = 1 筛出的是分组键值在整个表中仅出现一次的组,即“组内唯一”而非“全表唯一”;需配合 group by 使用,select 非分组字段须聚合或用子查询/窗口函数获取完整记录。

为什么 HAVING COUNT(*) = 1 能筛出“组内唯一”的记录
它本质是先按字段分组(GROUP BY),再对每组统计行数,只保留那些恰好出现 1 次的组——也就是说,该组对应的分组键值在整个表里只有一条记录。注意:这筛选的是“整个分组唯一”,不是“某列值在全表唯一”。
常见误用场景:想查「用户名不重复的用户」,却漏掉 SELECT 中非分组字段未聚合,导致语法报错或结果不可靠。
- 必须把用于判断唯一的字段放在
GROUP BY中(如username) -
SELECT里若要返回其他字段(如id,email),需确保它们和分组键一一对应,否则得用聚合函数(如MAX(id))或子查询 - MySQL 5.7+ 默认开启
ONLY_FULL_GROUP_BY,直接SELECT id, username GROUP BY username会报错Expression #1 of SELECT list is not in GROUP BY clause
如何安全地查出完整记录(不止分组键)
仅靠 GROUP BY + HAVING 只能得到分组键本身。要拿到原始行(比如 id, created_at),得用子查询或窗口函数。
推荐用子查询关联原表:
SELECT t1.* FROM users t1 INNER JOIN ( SELECT username FROM users GROUP BY username HAVING COUNT(*) = 1 ) t2 ON t1.username = t2.username;
或者用窗口函数(PostgreSQL / MySQL 8.0+ / SQL Server)更直观:
SELECT id, username, email
FROM (
SELECT *,
COUNT(*) OVER (PARTITION BY username) AS cnt
FROM users
) t
WHERE cnt = 1;
- 窗口函数方案避免了自连接,可读性高,且能直接过滤后取任意字段
- 子查询方案兼容性更好,但要注意如果
username允许为NULL,JOIN会丢掉这些行(NULL != NULL),需额外处理 - 别用
IN替代JOIN:当子查询结果含NULL时,WHERE username IN (subquery)整个条件会恒为UNKNOWN,返回空集
HAVING COUNT(DISTINCT ...) 和 COUNT(*) 的区别在哪
绝大多数时候你要的是 COUNT(*)——它统计每组总行数。只有当你分组依据含重复字段、又想排除内部重复时,才可能用 COUNT(DISTINCT ...)。
例如查「每个部门中,使用不同邮箱域名的员工数为 1 的部门」:
SELECT dept FROM employees GROUP BY dept HAVING COUNT(DISTINCT SUBSTRING_INDEX(email, '@', -1)) = 1;
-
COUNT(*) = 1→ 这个部门只有 1 名员工 -
COUNT(DISTINCT domain) = 1→ 这个部门所有员工邮箱都来自同一个域名(哪怕有 10 人) - 混淆两者会导致逻辑完全偏离预期,尤其在业务语义涉及“去重计数”时务必核对字段含义
性能和索引怎么配才不慢
分组 + 计数操作容易触发全表扫描,尤其数据量大时。关键看 GROUP BY 字段是否有有效索引。
- 单字段分组(如
GROUP BY username):给username加普通 B-tree 索引即可显著提速 - 多字段分组(如
GROUP BY status, category):建联合索引,顺序按分组字段顺序来((status, category)),且 WHERE 条件字段最好前置 - 执行前务必
EXPLAIN看是否用了索引;如果type是ALL或index,说明没走索引扫描,可能需要调整索引或加WHERE过滤缩小数据集 - 避免在
GROUP BY字段上用函数(如GROUP BY UPPER(username)),这会让索引失效
真正容易被忽略的是:HAVING 过滤发生在分组之后,无法利用索引下推;所以优先通过 WHERE 先筛掉大量无关行,再分组,效率提升往往比优化 HAVING 本身更明显。










