DELETE语句不能直接去重,必须配合子查询或窗口函数;核心是先识别应保留的行(如按email分组取MIN(id)或ROW_NUMBER()=1),再删除其余重复行,否则易误删全部;执行前须备份、测试并用SELECT预览待删数据。

DELETE 语句不能直接去重,必须配合子查询或窗口函数
SQL 标准的 DELETE 本身不支持“保留一条、删其余”的原子操作。硬写 DELETE FROM table WHERE ... 很容易误删全部或一条不剩。核心思路是:先识别哪些行该留,再删掉不在这个集合里的重复行。
常见错误现象:DELETE FROM users WHERE id IN (SELECT id FROM users GROUP BY email HAVING COUNT(*) > 1) —— 这会删掉所有重复邮箱对应的行,包括你想保留的那一条。
- MySQL 8.0+ / PostgreSQL / SQL Server 支持
ROW_NUMBER()窗口函数,最稳妥 - 旧版 MySQL(
- SQLite 不支持窗口函数,需用
ROWID+ 子查询,且要求表有主键或唯一标识
用 ROW_NUMBER() 保留每组第一条,删除其余(推荐)
这是目前最通用、可读性最强、不易出错的方式。关键在于按分组字段排序后编号,只保留 rn = 1 的行。
DELETE FROM users
WHERE id NOT IN (
SELECT id FROM (
SELECT id, ROW_NUMBER() OVER (PARTITION BY email ORDER BY id) AS rn
FROM users
) t WHERE t.rn = 1
);注意点:
-
PARTITION BY email指定去重依据字段;多个字段写成PARTITION BY email, name -
ORDER BY id决定哪条被保留——选最小id就写ORDER BY id,选最新就写ORDER BY created_at DESC - 外层
NOT IN要套两层子查询,否则 MySQL 会报 “You can't specify target table for update in FROM clause” - PostgreSQL 可改用
USING语法更高效,但跨数据库兼容性差
没有窗口函数时,用自连接或 ROWID(MySQL 5.7 / SQLite)
MySQL 5.7 或 SQLite 用户无法用 ROW_NUMBER(),得靠关联比较。本质是:对每组重复数据,找出“更大/更小”的那个 ID 删掉。
MySQL 示例(假设 email 是重复字段,id 是主键):
DELETE t1 FROM users t1 INNER JOIN users t2 WHERE t1.email = t2.email AND t1.id > t2.id;
SQLite 示例(依赖隐式 ROWID):
DELETE FROM users WHERE ROWID NOT IN ( SELECT MIN(ROWID) FROM users GROUP BY email );
- MySQL 自连接写法中,
t1.id > t2.id表示删掉组内 ID 更大的记录,保留最小 ID - SQLite 的
MIN(ROWID)前提是表没显式定义INTEGER PRIMARY KEY,否则ROWID可能不连续或不可靠 - 这两种方法在大数据量下易锁表、慢,务必在
email字段加索引
执行前必须做的三件事:备份、测试、加 WHERE 限流
删数据不是 SELECT,没撤回机制。哪怕语句逻辑正确,也可能因数据分布异常导致删多或删少。
- 先用
SELECT模拟要删的行:SELECT * FROM users WHERE id IN (SELECT id FROM (...) ),确认数量和样本是否合理 - 在测试库跑通,再上生产;线上操作前
mysqldump -u user db table > backup.sql - 大表别一次删完,加
LIMIT 1000分批(MySQL 支持,PostgreSQL 需用 CTE + offset) - 特别注意事务隔离级别:REPEATABLE READ 下可能看到过期快照,导致重复删或漏删
真正麻烦的不是语法怎么写,而是你没法 100% 确保“重复”的定义和业务预期一致——比如空值是否算相同、大小写是否敏感、JSON 字段是否需要规范化后再比。这些都得在 PARTITION BY 或 GROUP BY 之前处理干净。

















