查重复数据须先明确唯一键字段,再用group by+having定位重复值,接着用row_number()或子查询获取完整重复记录,最后谨慎delete并备份。

查重复数据前先确认唯一键字段
数据库里“重复”不是凭感觉,而是看哪几个字段组合起来本该唯一。比如订单表用 order_id 做主键,那重复就是两条一样的 order_id;但如果是日志表没主键,靠 user_id + event_time + event_type 联合判断是否重复,就得先明确这个组合。
常见踩坑:直接对 SELECT * 去重,结果发现删掉的不是“业务意义”的重复——比如两条记录 create_time 差1秒,但其他都一样,你本意是留最新那条,却按默认排序留了旧的。
- 用
SHOW CREATE TABLE table_name看实际的UNIQUE KEY或PRIMARY KEY - 没有显式唯一约束?翻业务文档或问后端,确认逻辑唯一性字段(别猜)
- 注意大小写和空格:MySQL 默认不区分大小写,但 PostgreSQL 区分;
''和' '在某些 collation 下可能被当作相同
用 GROUP BY + HAVING 快速定位重复值
这是最直接、兼容性最好的方法,适用于所有主流 SQL 数据库,不需要窗口函数支持。
核心思路:按唯一键字段分组,数每组行数,大于 1 的就是重复块。
- 示例:查
users表中重复的email
SELECT email, COUNT(*) AS cnt FROM users GROUP BY email HAVING COUNT(*) > 1;
(product_id, store_id),GROUP BY 后面就写两个:GROUP BY product_id, store_id
HAVING cnt > 1 在 MySQL 5.7+ 可行,但 PostgreSQL 和老版本 MySQL 不认,坚持写 HAVING COUNT(*) > 1
查出重复行的完整记录(不只是重复值)
只看到哪些值重复还不够,得知道具体是哪几条记录,才能决定保留哪条、删哪条。
这时候不能只靠 GROUP BY,得用子查询或窗口函数。推荐用 ROW_NUMBER() —— 它能给每组内的行编号,方便取第 1 条或排除第 1 条。
- MySQL 8.0+ / PostgreSQL / SQL Server 都支持:
SELECT *
FROM (
SELECT *,
ROW_NUMBER() OVER (
PARTITION BY email ORDER BY updated_at DESC
) AS rn
FROM users
) t
WHERE t.rn > 1;
PARTITION BY 是你要去重的字段(对应唯一键),ORDER BY 决定保留哪条——通常按时间倒序,留最新的避免误删:先备份,再用 WHERE 精确限定范围
查出来只是第一步,真正危险的是 DELETE。线上环境执行前没加 WHERE 或条件写错,一删就是全表。
尤其注意:很多教程教的 “DELETE FROM t USING t t1 INNER JOIN t t2 ...” 语句,在不同数据库语法差异大,MySQL 和 PostgreSQL 写法完全不同,抄错直接报错或删错。
- 安全做法:先把要删的 ID 拿出来,人工抽检几条
- 例如,删重复
email中旧的记录(保留updated_at最大的那条):
DELETE FROM users
WHERE id NOT IN (
SELECT id FROM (
SELECT MAX(id) AS id
FROM users
GROUP BY email
) t
);
email 组里有 NULL,GROUP BY 会把它们归为一组,而 NOT IN (subquery) 遇到 NULL 会整个失效——必须加 WHERE email IS NOT NULL
CREATE TABLE users_dup_backup AS SELECT * FROM users WHERE ... 备份目标子集事情说清了就结束。唯一键定义不清、NULL 处理、窗口函数版本兼容性,这三个点最容易在导入后排查时漏掉。










