WHERE不能用COUNT()筛选重复行,因为WHERE在分组前执行而COUNT()是聚合函数,必须配合GROUP BY使用;HAVING才能在分组后对聚合结果过滤,如HAVING COUNT(*) > 1。

为什么 WHERE 不能用 COUNT() 筛重复行
因为 WHERE 在分组前执行,此时 COUNT() 还没计算——它属于聚合函数,必须配合 GROUP BY 使用。你写 WHERE COUNT(*) > 1 会直接报错:ERROR: aggregate functions are not allowed in WHERE。
HAVING 必须和 GROUP BY 配合使用
HAVING 是唯一能在分组后对聚合结果做条件筛选的子句。想查“哪些字段组合出现了多次”,就得先按这些字段分组,再用 HAVING COUNT(*) > 1 过滤:
SELECT user_id, email FROM users GROUP BY user_id, email HAVING COUNT(*) > 1;
- 只写
GROUP BY不加HAVING,你会看到每组的计数,但无法筛出重复项 -
HAVING后面只能出现分组字段或聚合表达式,不能写未分组的列(比如HAVING created_at > '2023-01-01'会报错) - MySQL 5.7+ 默认开启
sql_mode=only_full_group_by,强制要求SELECT列必须在GROUP BY中出现或被聚合,否则报错
想查完整重复记录(不止分组字段)?用窗口函数或自关联
上面的 GROUP BY + HAVING 只返回去重后的分组值,不带原始行数据。要拿到所有重复的完整记录,推荐用 COUNT() OVER:
SELECT *
FROM (
SELECT *,
COUNT(*) OVER (PARTITION BY user_id, email) AS cnt
FROM users
) t
WHERE cnt > 1;- 窗口函数不需要
GROUP BY,也不改变行数,适合保留原始字段 - PostgreSQL / SQL Server / MySQL 8.0+ / Oracle 都支持;SQLite 目前不支持窗口函数
- 如果数据库太老(如 MySQL 5.6),可用自关联:先用
GROUP BY + HAVING找出重复键,再JOIN原表捞出全量记录
性能要注意:重复字段上建联合索引
当表很大时,GROUP BY user_id, email 可能很慢。如果经常按这两列查重复,务必建联合索引:
CREATE INDEX idx_user_email ON users(user_id, email);
- 索引顺序很重要:要把最常用于等值过滤的列放前面(比如
user_id = ? AND email = ?) - 如果只对
email单独查重复,这个联合索引效果有限,需要额外建单列索引 -
HAVING COUNT(*) > 1本身不走索引,但GROUP BY阶段能利用索引加速分组排序
实际业务中,最容易漏掉的是「是否要排除 NULL」——NULL 在 GROUP BY 中被视为相同值,但语义上往往不该算作有效重复。需要显式过滤:WHERE user_id IS NOT NULL AND email IS NOT NULL。

















