用 GROUP BY + COUNT() 找重复行需对所有列分组并加 HAVING COUNT() > 1;NULL 在 GROUP BY 中正确归组,但需注意 DISTINCT 和窗口函数的 NULL 行为差异;推荐显式列出所有列,避免 GROUP BY 或 COUNT(DISTINCT *)。

用 GROUP BY + COUNT() 找出重复的完整行
直接检测“整行重复”不能靠 COUNT(*) 单独用,必须配合 GROUP BY 对所有列分组。否则数据库不知道按什么标准判定“重复”。
常见错误是只对主键或某几列 GROUP BY,结果漏掉非主键字段值不同但其他字段全相同的“伪重复”。真正重复行意味着所有列(包括 NULL)完全一致。
- PostgreSQL / SQLite / MySQL 8.0+ 支持直接
GROUP BY *(但不推荐,可读性差且部分版本报错) - 稳妥做法:显式列出所有列,例如
GROUP BY col1, col2, col3, ... - 记得在
HAVING中过滤:HAVING COUNT(*) > 1,否则返回所有分组
示例(假设表 users 有 name, email, age):
SELECT name, email, age, COUNT(*) AS cnt FROM users GROUP BY name, email, age HAVING COUNT(*) > 1;
处理 NULL 值导致的重复识别失效
NULL = NULL 在 SQL 中返回 UNKNOWN,不是 TRUE,所以默认 GROUP BY 会把多个 NULL 当作同一组——这反而是对的;但如果你用 WHERE 或 JOIN 手动比对重复,就容易出错。
真正危险的是用 DISTINCT 或窗口函数时忽略 NULL 行为差异。比如 ROW_NUMBER() OVER (PARTITION BY col1, col2 ORDER BY id) 中,col1 或 col2 为 NULL 时仍能正确归组,但某些旧版 MySQL(5.7)在 ONLY_FULL_GROUP_BY 模式下可能拒绝含 NULL 的 GROUP BY。
- 测试前先确认:
SELECT COUNT(*) FROM table WHERE col IS NULL,看是否有空值参与重复逻辑 - 如需兼容弱类型比较,可用
COALESCE(col, '>')转换,但注意字符串长度和 collation 影响 - 避免用
!=或判断NULL,一律用IS NULL/IS NOT NULL
用窗口函数定位重复行的具体 ID(而非仅统计)
聚合查询只能告诉你“哪些值重复”,但无法返回原始记录的 id 或时间戳。这时候 ROW_NUMBER() 或 COUNT() OVER 更实用。
关键点在于:窗口函数的 PARTITION BY 必须覆盖全部列,且排序子句要明确(否则同组内顺序不可控,ROW_NUMBER() 结果不稳定)。
- 推荐写法:
ROW_NUMBER() OVER (PARTITION BY col1, col2, col3 ORDER BY id),保留最小id作为“主记录” - 若想标记所有重复行(包括首行),用
COUNT(*) OVER (PARTITION BY ...),再WHERE cnt > 1 - 注意性能:大表上对多列
PARTITION BY可能触发临时表或文件排序,建议提前在常用列建复合索引
示例(查出所有重复行及其原始 id):
SELECT id, name, email, age
FROM (
SELECT id, name, email, age,
COUNT(*) OVER (PARTITION BY name, email, age) AS cnt
FROM users
) t
WHERE cnt > 1;
避免 COUNT(DISTINCT *) 这类非法写法
COUNT(DISTINCT *) 是语法错误——所有主流 SQL 引擎都不支持。有人误以为它能“算去重后总行数”,其实 DISTINCT 后只能跟具体列或表达式。
真正需要“整行去重计数”时,得绕一下:
- PostgreSQL:用
SELECT COUNT(*) FROM (SELECT DISTINCT * FROM t) AS _ - MySQL:同上,但 5.7 及以前要求子查询带别名,且不能有无名列
- 如果表太大,
DISTINCT *会吃内存,优先考虑是否真需要这个数字,还是只需知道“有没有重复”(此时EXISTS+GROUP BY ... HAVING COUNT(*) > 1更快)
最常被忽略的一点:即使你只关心“是否存在重复”,也别用 SELECT 1 FROM t GROUP BY ... HAVING COUNT(*) > 1 LIMIT 1 就完事——没加 ORDER BY 时,LIMIT 1 返回哪一组是不确定的,调试时可能漏掉关键重复模式。











