GROUP BY + HAVING COUNT(*) > 1 是识别重复值最直接兼容的方式,WHERE 不能用聚合函数故必须前置过滤条件,HAVING 专用于筛选分组后结果,NULL 值需结合业务判断是否参与去重,索引缺失和 NULL 处理是易被忽略的性能与逻辑隐患。

GROUP BY + HAVING COUNT(*) > 1 是最直接、兼容性最好的重复值识别方式,但必须写对顺序和位置,否则查不到或报错。
为什么不能用 WHERE COUNT(*) > 1?
HAVING 是唯一能过滤聚合结果的子句,WHERE 在分组前执行,根本看不到 COUNT() 的值。写成 WHERE COUNT(*) > 1 会直接报错(如 PostgreSQL 报 ERROR: aggregate functions are not allowed in WHERE),MySQL 可能静默跳过或返回空结果——逻辑已错,却不易察觉。
正确链条是:GROUP BY email → 每组算一次 COUNT(*) → HAVING COUNT(*) > 1 筛掉只出现 1 次的组。
- 想筛“邮箱不为空且重复”的记录?
WHERE email IS NOT NULL必须放在GROUP BY前 - 想统计 2023 年后注册的重复邮箱?
WHERE created_at >= '2023-01-01'同样要前置,否则计数含历史数据 - 别在
HAVING里引用没出现在GROUP BY或SELECT中的字段,比如HAVING name = 'Alice'(没GROUP BY name)会报错
单字段重复:GROUP BY email HAVING COUNT(*) > 1
这是最常见场景,语义清晰、性能可控。例如查 users 表中重复的邮箱:
SELECT email, COUNT(*) AS cnt FROM users GROUP BY email HAVING COUNT(*) > 1;
注意点:
- 用
COUNT(*)而非COUNT(email),后者会跳过NULL值,可能漏掉含空邮箱的重复组 - 多数数据库(PostgreSQL/MySQL/SQL Server)默认把所有
NULL归为同一组,所以COUNT(*)也会把多条NULL email算作一次重复;若业务上认为“未填邮箱”彼此无关,得加WHERE email IS NOT NULL - 如果表很大,
email列没索引,GROUP BY可能变慢;加INDEX(email)能显著提速
多字段联合重复:GROUP BY a, b, c HAVING COUNT(*) > 1
订单号+商品ID+下单时间组合重复,才算异常?那就必须把三个字段全写进 GROUP BY:
SELECT user_id, product_id, order_date, COUNT(*) AS dup_cnt FROM orders GROUP BY user_id, product_id, order_date HAVING COUNT(*) > 1;
关键细节:
- 少写一个字段,分组粒度就变粗——比如漏掉
order_date,同用户同商品的所有订单都会被强行合并,误判成重复 - 字段顺序不影响结果,但所有字段必须同时出现在
GROUP BY和后续的IN子查询(如果要用)中 - MySQL 8.0+ 和 PostgreSQL 支持元组写法:
WHERE (user_id, product_id, order_date) IN (SELECT user_id, product_id, order_date FROM ...);老版本 MySQL 需用CONCAT拼接作临时键,但要注意NULL拼接后结果为NULL,可能漏数据
查出所有重复的原始行,不只是分组摘要
GROUP BY + HAVING 只返回每组一条汇总行,业务常需要看到全部重复记录(比如人工核对内容是否真一致)。这时得套一层:
推荐用窗口函数(MySQL 8.0+/PostgreSQL/SQL Server):
SELECT * FROM ( SELECT *, COUNT(*) OVER (PARTITION BY email) AS cnt FROM users ) t WHERE cnt > 1;
兼容老版本(如 MySQL 5.7)可用子查询:
SELECT * FROM users WHERE email IN ( SELECT email FROM users GROUP BY email HAVING COUNT(*) > 1 );
注意性能差异:
- 子查询在大表上可能慢,尤其
email无索引时;INNER JOIN写法通常更快: SELECT u.* FROM users u INNER JOIN (SELECT email FROM users GROUP BY email HAVING COUNT(*) > 1) dup ON u.email = dup.email;
- 窗口函数写法更直观,但
PARTITION BY email会触发排序,若email无索引,大表下延迟明显
真正容易被忽略的是 NULL 处理和索引缺失——前者导致误判重复组,后者让查询从秒级拖到分钟级,而且问题不会报错,只会让你反复怀疑 SQL 写错了。

















