最稳删重复记录的方式是用row_number()窗口函数:先按重复字段分组并按id升序编号,再删除序号>1的行;mysql 8.0+、postgresql、sql server、oracle支持,但mysql 5.7及以下不支持,需改用自连接方案。

如何用 DELETE JOIN 找出并删除重复记录
MySQL 8.0+ 和 PostgreSQL 支持 DELETE ... USING 或 DELETE FROM ... JOIN 语法直接删重,但语法细节差异大,且容易误删整表——关键不是“能不能删”,而是“按什么字段判重、保留哪一条”。
常见错误现象:ERROR 1093 (HY000): You can't specify target table 't' for update in FROM clause(MySQL 5.7 及更早版本不支持在子查询中直接引用被删表);或删完发现只剩一条,但本该保留最新那条却删掉了。
- 先确认判重逻辑:是按
email去重?还是user_id + order_date联合去重?必须明确主键/唯一标识和排序依据(如id或created_at) - MySQL 8.0+ 推荐用
DELETE t1 FROM users t1 INNER JOIN users t2 WHERE t1.email = t2.email AND t1.id > t2.id—— 这样保留id最小的记录 - PostgreSQL 必须用
DELETE FROM users WHERE id NOT IN (SELECT MIN(id) FROM users GROUP BY email),或更安全的USING写法:DELETE FROM users u USING (SELECT MIN(id) AS keep_id FROM users GROUP BY email) keep WHERE u.id != keep.keep_id - 执行前务必加
SELECT验证:SELECT email, COUNT(*) FROM users GROUP BY email HAVING COUNT(*) > 1先看哪些值真重复
为什么不能直接用 GROUP BY + DELETE?
DELETE FROM t GROUP BY x 是非法语法。SQL 标准里 GROUP BY 属于聚合上下文,而 DELETE 是行级操作,二者语义冲突。强行套用会报错 ERROR 1140: In aggregated query without GROUP BY 或类似提示。
本质问题在于:数据库不知道你要删“每组第一条”还是“最后一条”,也没法自动推导保留策略。
- 替代思路是把“保留逻辑”转成条件:比如“保留每组
MIN(id)对应的行”,那就得先查出这些MIN(id),再用NOT IN或JOIN反向筛选要删的 - 注意
NULL值:如果判重字段含NULL,GROUP BY会把所有NULL归为一组,但=判断不成立,需改用IS NULL单独处理 - 大数据量时,
NOT IN (subquery)可能极慢甚至卡死,优先走JOIN或给判重字段加索引(如INDEX(email))
DELETE JOIN 在不同版本 MySQL 中的行为差异
MySQL 5.7 不允许 DELETE t1 FROM t1 JOIN t2 引用同一张表两次(除非用派生表绕过),而 MySQL 8.0+ 放宽了限制,但仍要求 JOIN 条件里有可索引字段,否则可能全表扫描+锁表。
- MySQL 5.7 安全写法:
DELETE FROM users WHERE id IN (SELECT id FROM (SELECT t1.id FROM users t1 INNER JOIN users t2 WHERE t1.email = t2.email AND t1.id > t2.id) AS tmp) - MySQL 8.0+ 可直写:
DELETE t1 FROM users t1 INNER JOIN users t2 ON t1.email = t2.email AND t1.id > t2.id,但必须确保email和id有联合索引,否则性能灾难 - 事务中执行:加上
BEGIN; ... DELETE ... ; COMMIT;,避免删到一半失败导致数据不一致 - 千万别省略
WHERE中的t1.id > t2.id类不等式——漏掉就变成自连接全匹配,删光整张表
真正危险的是没验证就跑 DELETE
线上库删错不可逆。最常被忽略的一点:重复判定字段是否真的业务唯一?比如 phone 字段看似唯一,但可能存了带空格、括号或国际区号的变体,直接按字符串相等判重会漏掉逻辑重复。
- 清洗前先跑
SELECT phone, TRIM(REPLACE(REPLACE(phone,' ',''),'-','')) AS clean_phone FROM users LIMIT 10看标准化效果 - 删重只是手段,根源在写入时没做约束:补上
UNIQUE INDEX (email)或用INSERT IGNORE/ON CONFLICT DO NOTHING(PG) - 哪怕测试库,也建议先导出待删ID:
SELECT t1.id FROM users t1 JOIN users t2 ON t1.email = t2.email AND t1.id > t2.id,保存结果再核对










