group by + having count() = 1 是最直接解法,因其语义清晰:先按指定字段分组,再筛选出组内仅含单条记录的“孤例”;它不取代表值,而是准确识别全表中该分组键的唯一出现,且count()比count(id)更安全(含null行),但仅返回分组键,要完整行需配合窗口函数或子查询。

为什么 GROUP BY + HAVING COUNT(*) = 1 是最直接解法
要找“每组中唯一不重复的 ID”,本质是识别那些在分组维度下只出现一次的记录。不是去重后取一个代表值,而是筛出所有“孤例”。GROUP BY 定义分组逻辑,HAVING COUNT(*) = 1 过滤出仅含单条记录的组——这是语义最清晰、数据库优化器也最容易走索引的路径。
常见错误是误用 SELECT DISTINCT 或 MIN(id)/MAX(id),它们无法保证该 ID 在组内真的“唯一不重复”,只是取了个值而已。
- 必须用
HAVING(不是WHERE),因为过滤发生在分组聚合之后 -
COUNT(*)比COUNT(id)更安全:即使id为NULL,COUNT(*)仍计 1 条,而COUNT(id)会跳过NULL - 若需返回原表全部字段,得用子查询或窗口函数配合,不能只靠
GROUP BY主查询直接选
如何查出完整原始行(不止是 ID)
单纯 GROUP BY ... HAVING COUNT(*) = 1 只能返回分组键和聚合结果,拿不到整行数据。要还原原始记录,得把筛选逻辑“搬回”到行级别判断。
推荐用窗口函数,兼容性好且逻辑直白:
SELECT id, group_col, other_col
FROM (
SELECT *,
COUNT(*) OVER (PARTITION BY group_col) AS cnt
FROM your_table
) t
WHERE cnt = 1;
替代方案(MySQL 5.7 或旧版 PostgreSQL)可用自连接或相关子查询,但性能差、写法绕:
- 自连接:用
LEFT JOIN自己,匹配相同group_col但不同id的行,再筛出没匹配上的 - 子查询:对每行查一遍
(SELECT COUNT(*) FROM t2 WHERE t2.group_col = t1.group_col) = 1,无索引时极易变全表扫描
HAVING COUNT(DISTINCT id) 和 COUNT(*) 有什么区别?
绝大多数场景下,COUNT(*) = 1 就够了。COUNT(DISTINCT id) 只在一种边缘情况有意义:同一组里存在多条记录,id 字段值相同(即业务上允许重复 ID),但你想确认“这个 ID 值在组内是否唯一出现”。
例如:group_col = 'A' 有三行,id 分别是 101、101、102,那么:
-
COUNT(*) = 3,这组肯定不满足“唯一不重复” -
COUNT(DISTINCT id) = 2,说明有两个不同 ID,但都不算“唯一” - 只有
COUNT(DISTINCT id) = 1 AND COUNT(*) = 1才真正表示“该组只有一个 ID,且它只出现一次”
实践中,ID 本应主键或唯一,所以 COUNT(*) = 1 已隐含 COUNT(DISTINCT id) = 1;若 ID 不唯一,先得厘清业务规则是否合理。
性能关键点:索引怎么建才不拖慢查询
这类查询性能瓶颈几乎总在分组字段的扫描效率上。没有索引时,GROUP BY group_col 很可能触发临时表 + 文件排序。
- 最有效的是联合索引:
CREATE INDEX idx_group_cnt ON your_table (group_col, id)—— 覆盖分组与计数所需字段,避免回表 - 如果只查
id列,加INCLUDE(SQL Server / PostgreSQL 11+)或用覆盖索引(MySQL)减少 I/O - 注意:
HAVING条件无法使用索引加速,但前置的GROUP BY阶段可以;窗口函数版本则依赖PARTITION BY字段的索引
当分组键基数极高(比如百万级不同值),或单组数据量极大(某 group_col 值占全表 80%),即使有索引,COUNT(*) OVER (PARTITION BY ...) 也可能产生大量中间结果,这时得考虑业务是否真需要实时计算,还是改用预聚合表。










