不能直接用distinct或group by删除重复行,因为二者仅用于查询去重,无法修改原表;delete不支持“留一删余”原子操作,且mysql禁止子查询中直接引用被删除表,必须借助临时表绕过限制。

为什么不能直接用 DISTINCT 或 GROUP BY 删除重复行
因为 DISTINCT 和 GROUP BY 只能用于查询去重结果,无法修改原表;而 DELETE 又不支持直接对多行相同记录做“留一删余”的原子操作。MySQL 不允许在子查询中直接引用被删除的表(会报错 You can't specify target table 't' for update in FROM clause),所以必须借助临时表绕过这个限制。
用 CREATE TABLE ... SELECT 构建唯一临时表
这是最稳妥、兼容性最好的方式,尤其适合大表或有主键/索引约束的场景。核心是先选出每组重复行中的“一行”(通常用 MIN(id) 或 MAX(id)),再把它们写入新表。
- 假设原表叫
users,所有字段为id, name, email, created_at,重复由name和email联合决定 - 先建临时表存去重后的数据:
CREATE TABLE users_dedup AS SELECT MIN(id) AS id, name, email, MAX(created_at) AS created_at FROM users GROUP BY name, email;
- 注意:这里用
MIN(id)保留最小 ID 的那条,MAX(created_at)是为了逻辑上取最新时间(按需调整) - 如果原表有自增主键、唯一索引或外键,临时表不会继承这些约束,后续需手动重建
用 RENAME TABLE 原子替换原表
比起 DROP + RENAME 或 TRUNCATE + INSERT,RENAME TABLE 是原子操作,避免中间状态丢失数据,也减少锁表时间。
- 执行替换:
RENAME TABLE users TO users_backup, users_dedup TO users;
- 确保
users_dedup表结构与原表一致(字段顺序、类型、NULL 属性等),否则应用层可能出错 - 如果原表有触发器、分区或 FTS 索引,
RENAME不会迁移它们,需单独处理 - 替换后建议立刻检查:
SELECT COUNT(*) FROM users;和SELECT COUNT(DISTINCT name, email) FROM users;应该相等
没有主键时怎么安全识别“完全重复行”
当表没有 id 或任何唯一标识列时,判断“完全重复”只能靠整行字段比对,此时 GROUP BY * 不合法,必须显式列出所有列。
- 例如表
logs有level, msg, ts三列且无主键:CREATE TABLE logs_dedup AS SELECT level, msg, ts FROM logs GROUP BY level, msg, ts;
- 如果列数很多,手写易错,可用
INFORMATION_SCHEMA.COLUMNS动态生成 SQL(但生产环境慎用动态 SQL) - 这种方案在存在
NULL值时要特别小心:GROUP BY中NULL = NULL成立,但某些旧版本 MySQL 对全NULL行聚合行为不稳定 - 更保险的做法是加辅助列:
ALTER TABLE logs ADD COLUMN row_hash CHAR(32) AS (MD5(CONCAT_WS('|', level, msg, ts))) STORED;,再按row_hash分组
ROW_NUMBER())在 MySQL 5.7 及更早版本中更通用。











