COUNT()比COUNT(字段)更可靠,因后者跳过NULL值而重复判定需统计整行出现次数;正确做法是GROUP BY多字段后用COUNT()配合HAVING COUNT()>1,或用窗口函数COUNT() OVER(PARTITION BY...)直接获取重复行。

查重复记录时为什么 COUNT(*) 比 COUNT(字段) 更可靠
因为 COUNT(字段) 会跳过 NULL 值,而重复判定关注的是整行组合是否重复,不是某个字段是否为空。用 COUNT(*) 才能真实反映“这组值出现了几次”。
常见错误现象:用 COUNT(email) 查邮箱重复,结果漏掉含 NULL 邮箱的重复行;或者误以为 COUNT(id) 能统计重复,其实 id 通常是主键,永远不重复。
- 正确写法:
GROUP BY name, email后跟COUNT(*) - 错误写法:
COUNT(name)或COUNT(email)用于去重计数 - 如果只想看重复项(出现 ≥2 次),必须加
HAVING COUNT(*) > 1,WHERE不能替代
GROUP BY 多字段组合的坑:顺序无关,但 NULL 的行为要小心
GROUP BY a, b 和 GROUP BY b, a 结果完全一致,SQL 标准不依赖字段顺序。真正容易出错的是 NULL 在分组中的表现——多数数据库(如 MySQL、PostgreSQL)把所有 NULL 当作相同值分到一组,但 Oracle 默认不这样(需显式配置)。
- MySQL/PostgreSQL 中:
(‘Alice’, NULL)和(‘Alice’, NULL)会被归为同一组 - 若业务上认为
NULL表示“未知”,不应参与重复判断,得先用COALESCE(email, ‘<null>’)</null>统一占位 - 避免直接
GROUP BY *—— 语法不合法,也无意义
只查重复行本身,而不是重复次数:用窗口函数更直接
如果目标不是统计频次,而是“把所有重复的原始记录捞出来”,硬套 GROUP BY + HAVING 得再连一次原表,既啰嗦又易错。这时 COUNT(*) OVER (PARTITION BY ...) 是更干净的选择。
使用场景:导出全部重复数据做人工核对、清洗前预览、ETL 中标记脏数据。
- 示例:
SELECT *, COUNT(*) OVER (PARTITION BY user_id, order_date) AS cnt FROM orders - 后续加
WHERE cnt > 1就能拿到所有重复的原始行(含完整字段) - 注意:窗口函数在 SQLite 中不支持(除非是 3.25+ 且编译时启用了),老版本得退回到自连接方案
- 性能上,大表慎用未索引的
PARTITION BY字段,会触发全表排序
MySQL 8.0+ 之前怎么高效查重复?别用自连接暴力扫
老 MySQL(如 5.7)不支持窗口函数,有人写 SELECT * FROM t t1 WHERE EXISTS (SELECT 1 FROM t t2 WHERE t2.a = t1.a AND t2.b = t1.b AND t2.id != t1.id),看似直观,实际可能慢出天际——没索引时是 O(n²)。
- 优先建联合索引:
CREATE INDEX idx_dup ON table_name (col_a, col_b) - 更稳的写法是子查询 +
IN:SELECT * FROM table_name WHERE (col_a, col_b) IN (SELECT col_a, col_b FROM table_name GROUP BY col_a, col_b HAVING COUNT(*) > 1) - 注意括号写法:
(col_a, col_b)是行构造器语法,MySQL 支持,但 PostgreSQL 要写成(col_a, col_b) IN (SELECT col_a, col_b ...)形式才安全 - 如果表超大(千万级),先
EXPLAIN看是否走了索引,没走就别硬跑
真正麻烦的不是语法,是搞不清“重复”的定义边界:字段要不要忽略大小写?空格算不算差异?时间字段要不要截断到天?这些都得在 GROUP BY 前用 TRIM()、UPPER()、DATE() 预处理,漏一步,结果就不可信。

















