识别完全重复行需用group by对所有字段分组并having count(*)>1筛选;含null时须用coalesce等处理;无主键时可借助row_number()保留首条或用自连接删除冗余行。

怎么识别完全重复的行(不含主键)
没有主键或唯一标识时,SELECT * 无法直接标出哪几行是“完全重复”的。关键在于把整行当作一个逻辑单元去比对——得用 GROUP BY 对所有字段分组,再用 HAVING COUNT(*) > 1 找出重复组。
注意:所有字段都必须显式列出,不能用 *;如果字段多、有 NULL,要特别小心 NULL = NULL 在 GROUP BY 中的行为(多数数据库视其为相等,但 PostgreSQL 默认不聚合 NULL,需用 IS NOT DISTINCT FROM 或补默认值)。
示例(MySQL/SQL Server):
SELECT col1, col2, col3, COUNT(*) FROM my_table GROUP BY col1, col2, col3 HAVING COUNT(*) > 1;
安全删除重复行,只保留一条
直接 DELETE 多行容易误删,推荐用子查询 + 自关联或窗口函数方式,确保每组重复中仅删掉“多余”的那几条,留下一条。
MySQL 8.0+ / PostgreSQL / SQL Server 支持 ROW_NUMBER(),最稳:
DELETE FROM my_table
WHERE (col1, col2, col3) IN (
SELECT col1, col2, col3 FROM (
SELECT col1, col2, col3,
ROW_NUMBER() OVER (PARTITION BY col1, col2, col3 ORDER BY id) AS rn
FROM my_table
) t WHERE rn > 1
);
要点:
-
ORDER BY id是关键——靠它决定“留哪一条”,没id就选一个能稳定排序的字段(比如时间戳),千万别用ORDER BY NULL或不写ORDER BY,否则保留哪条不可控 - SQLite 不支持窗口函数,得用自关联 +
MIN(id)方式,且必须有可比较的字段(如id) - 别在生产环境跑前不加
SELECT验证——先用上面的ROW_NUMBER()查询把待删的行打出来看看
没有主键也没有可排序字段怎么办
极端情况:表只有纯数据列,全为 TEXT/VARCHAR,且无时间、ID 等隐含顺序字段。这时无法可靠定义“保留哪一条”,只能接受任意性。
可行但危险的做法(仅限测试环境):
DELETE t1 FROM my_table t1 INNER JOIN my_table t2 WHERE t1.col1 = t2.col1 AND t1.col2 = t2.col2 AND t1.col3 = t2.col3 AND t1.rowid > t2.rowid; -- SQLite 用 rowid;MySQL 用别名 + 条件模拟
风险点:
- MySQL 没原生
rowid,得依赖引擎(InnoDB 的__rowid__非公开、不稳定,不推荐) - 这种写法本质是“删掉后出现的那条”,但物理存储顺序不保证,结果不可复现
- 一旦字段含
NULL,=判断失效,必须改用IS NULL显式判断,语句迅速变复杂
执行前必须做的三件事
不是语法对了就能安心删——重复数据常是业务逻辑缺陷的表征,删之前得确认影响面:
- 查外键:运行
SELECT * FROM information_schema.KEY_COLUMN_USAGE WHERE REFERENCED_TABLE_NAME = 'my_table';,看有没有其他表引用它;若有,得先处理级联或同步清理 - 备份快照:
CREATE TABLE my_table_backup AS SELECT * FROM my_table;(注意某些数据库不复制索引,记得补) - 关事务 & 检查隔离级别:在
REPEATABLE READ或更高级别下删,避免并发写入导致部分重复被跳过
真正麻烦的从来不是 SQL 写法,而是删完发现某张报表突然少了几百条订单——因为上游系统靠“重复”来标记重试状态。先搞清为什么会有重复,比怎么删更重要。











