ROW_NUMBER()是MySQL 8.0+多字段去重核心,需用子查询规避Error 1093;ORDER BY须明确方向及NULL处理,多条件优先级用CASE表达式实现;SELECT仅查视图,DELETE才真正删数据。

ROW_NUMBER() 是去重的核心函数
MySQL 8.0+ 中真正可控的多字段去重,必须依赖 ROW_NUMBER() 窗口函数。它不靠模糊聚合,也不依赖主键大小硬性取舍,而是先按业务逻辑分组排序,再精确标记“该留哪一条”。DISTINCT 和 GROUP BY 在需要保留完整行、且有优先级规则时根本不够用——比如“同一用户只留最新订单”,DISTINCT 无法指定时间字段,“GROUP BY user_id + MAX(order_time)”又拿不到订单金额、地址等其他字段。
DELETE 去重前必须套两层子查询
直接在 DELETE 语句里写 ROW_NUMBER() 会报错:Error 1093: You can't specify target table 'users' for update in FROM clause。这是 MySQL 的限制,必须把窗口函数结果先放到派生表(子查询)里,再让外层 DELETE 引用。
- 错误写法:
DELETE FROM users WHERE id IN (SELECT id FROM users GROUP BY name, email ...)—— 这不是窗口函数,但同样违反限制 - 正确结构:外层
DELETE→ 中层SELECT id FROM (...) t WHERE rn > 1→ 内层SELECT id, ROW_NUMBER() OVER (PARTITION BY name, email ORDER BY id DESC) AS rn FROM users - 关键点:
ORDER BY必须明确方向,DESC通常保留最新(最大 ID 或最新时间),ASC则保留最旧
多条件优先级排序要靠 CASE 表达式
当“最新”不是唯一标准,比如要优先保留 status = 'active' 的记录,其次才是时间,就不能只写 ORDER BY created_at DESC。得把业务规则翻译成排序权重:
-
ORDER BY CASE WHEN status = 'active' THEN 0 ELSE 1 END, created_at DESC:把 active 记录排最前,同状态内再按时间倒序 - 避免写
ORDER BY status DESC:字符串比较不可靠,'pending'可能比'active'排前面 - 如果字段可能为 NULL,加
IS NULL判断,否则 NULL 会被排在最前或最后,破坏预期顺序
查询时去重和删除时去重不能混用
很多人误以为 SELECT 里用了 ROW_NUMBER() 就等于数据已去重——其实只是临时视图。原表一条没少。真要删数据,必须走 DELETE 语句;只想查去重后结果,就用带 WHERE rn = 1 的子查询。两者语法结构相似,但意图和影响完全不同:
- 查:用
SELECT * FROM (SELECT *, ROW_NUMBER() OVER (...) AS rn FROM t) t2 WHERE rn = 1 - 删:用
DELETE FROM t WHERE id IN (SELECT id FROM (SELECT id, ROW_NUMBER() OVER (...) AS rn FROM t) t2 WHERE rn > 1) - 执行
DELETE前务必备份,或先用SELECT版本验证哪些 ID 会被删
实际操作中最容易漏掉的是 ORDER BY 子句里的 NULL 处理和多条件权重设计——看起来只改几个字,但结果可能全错。


















