group by + having count() > 1 是定位重复记录的标准解法;因sql执行顺序为where→group by→having,count()在where阶段尚未计算,故必须用having过滤,且分组字段须严格对应业务定义的“重复”依据。

直接用 GROUP BY + HAVING COUNT(*) > 1 就能快速定位重复记录,但必须确保分组字段是业务上定义“重复”的依据,否则结果毫无意义。
为什么 COUNT(*) 不能写在 WHERE 里
常见错误是写成 SELECT * FROM users WHERE COUNT(*) > 1,这会报错:ERROR: aggregate functions are not allowed in WHERE。因为 SQL 执行顺序是 FROM → WHERE → GROUP BY → HAVING → SELECT,WHERE 阶段聚合结果还没算出来。
-
HAVING是唯一能过滤聚合结果的地方,必须跟在GROUP BY后面 - 想查邮箱重复,得写
GROUP BY email HAVING COUNT(*) > 1,不是WHERE email IN (...)套娃 - 如果只关心“有没有重复”,用
EXISTS比全量GROUP BY快得多,尤其在有索引时
GROUP BY 分组字段怎么选才不漏数据
选错字段等于白跑——比如对含自增 id 或毫秒级 created_at 的表直接 GROUP BY *,永远查不出重复;而漏掉 NULL 处理,又可能让含空值的重复组消失。
- 单列重复:只写业务唯一键,如
email、order_no - 多列组合重复:写全判断依据,如
first_name, last_name, phone,注意用LOWER(TRIM(phone))统一格式 -
NULL默认被归为同一组(MySQL/PostgreSQL),若业务上认为NULL不参与去重,得提前用COALESCE(email, '<null>')</null>占位 - 千万别写
GROUP BY *—— 语法不合法,数据库也不支持
要完整原始行,别硬套 IN 子查询
用 GROUP BY 查出重复的 email 后,再用 SELECT * FROM users WHERE email IN (...) 回查,看似合理,实则埋雷。
-
IN对NULL完全失效:email IN (NULL)永远不返回任何行,必须额外补OR email IS NULL - 大表没索引时,
IN子查询可能触发两次全表扫描,性能断崖式下跌 - 更稳的替代是窗口函数:
COUNT(*) OVER (PARTITION BY email)直接在原表每行打标,> 1就是重复行 - SQLite 3.25.0 之前不支持窗口函数,只能用自连接,但务必加
a.id != b.id排除自匹配,否则每行都算“重复”
真正容易被忽略的是:重复的定义从来不在 SQL 语法里,而在业务规则中。比如订单号重复,到底是查“相同订单号的行数”,还是“不同用户提交了相同订单号”?前者用 GROUP BY order_no,后者得加 user_id 联合分组。没对齐这个,后面所有 COUNT 和 PARTITION BY 都只是在算错数。











