GROUP BY + HAVING 可精准识别按指定字段判定的重复值,如 SELECT email, COUNT() FROM users GROUP BY email HAVING COUNT() > 1;但不可直接删除,需配合子查询、窗口函数或临时表保留最小/最大id行,并务必先备份、验证再执行,最后添加唯一索引从源头防控。
直接查重复行,关键不是看整行是否一样,而是明确你按哪些字段判定“重复”。比如用户表中 email 不该重复,那就只对 email 分组;如果业务要求 name 和 phone 组合唯一,就得写 GROUP BY name, phone。
常见错误是写 SELECT * 配合 GROUP BY —— 大多数数据库(如 MySQL 严格模式、PostgreSQL)会报错,因为非分组字段值不明确。正确做法是先聚焦识别逻辑:
SELECT email, COUNT(*) FROM users GROUP BY email HAVING COUNT(*) > 1;
这条语句能快速暴露哪些 email 出现了多次,且返回每组的出现次数,方便评估影响范围。
删之前必须决定留哪一条——通常留 id 最小的(最早插入),或最大的(最新更新)。别用 DELETE FROM table WHERE ... 直接套子查询删全部,容易误删或锁表太久。
推荐用自连接或窗口函数(取决于数据库版本):
ROW_NUMBER() 窗口函数标记重复组内的顺序例如在支持窗口函数的库中,删掉每个重复 email 中除最小 id 外的所有行:
DELETE FROM users WHERE id 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 定义重复组,ORDER BY id 决定谁被保留。改用 ORDER BY id DESC 就保留最新那条。
删除操作不可逆,尤其当表大、索引缺失、或 WHERE 条件写错时,可能删掉全部数据。线上环境严禁跳过验证步骤。
实操建议:
SELECT 把将被删的 id 全部查出来,人工抽检几条确认逻辑BEGIN; DELETE ...; SELECT COUNT(*) FROM users WHERE id IN (...); ROLLBACK;(测试完再 COMMIT)DELETE 可能触发大量索引更新和锁等待有些数据库(如 MySQL InnoDB)对大事务有 innodb_log_file_size 限制,删几十万行以上建议分批,比如每次删 1000 行加 LIMIT 1000。
删完只是止血,真正要解决的是源头。重复数据大概率是因为缺少约束,而不是应用层没校验。
立刻补上唯一索引:
CREATE UNIQUE INDEX idx_users_email ON users(email);
如果已有重复,建索引会失败,得先清完再建。另外注意:
NULL 值在唯一索引中不参与冲突判断(多数数据库允许多个 NULL),如果业务允许空邮箱,得额外用触发器或应用层控制CREATE UNIQUE INDEX idx_name_phone ON users(name, phone)
ERROR 1062 (23000): Duplicate entry ... for key ...,比事后清理成本低两个数量级真正麻烦的不是怎么删,而是删完发现下个月又冒出来——说明约束没加,或者上游系统绕过了校验逻辑。