row_number() 不能直接用于 delete 的 where 条件,因 sql 标准禁止窗口函数在删除上下文中使用;必须通过 cte 或子查询先生成序号再关联主键删除,否则报错或误删。

ROW_NUMBER() 不能直接删除任何数据,必须配合 CTE 或子查询定位冗余行后再执行 DELETE;否则语法报错或误删整表。
为什么直接写 DELETE + ROW_NUMBER() 会失败
SQL 标准禁止在 DELETE 或 WHERE 子句中直接使用窗口函数。像 DELETE FROM t WHERE ROW_NUMBER() OVER (PARTITION BY a ORDER BY b) > 1 这种写法,所有主流数据库(PostgreSQL、SQL Server、MySQL 8.0+)都会拒绝执行,报错类似 ERROR 1064: Window function is not allowed in this context。
根本原因是:ROW_NUMBER() 是运行时计算的逻辑序号,不是存储在表里的值,无法被 WHERE 条件直接引用。
- 必须先用 CTE 或派生表把序号“固化”成临时结果集
- 再通过主键/唯一字段关联原表,明确告诉数据库“删哪几行”
- 漏掉这一步,就等于没写删除逻辑——只是在看热闹
CTE + DELETE 是最安全的通用写法
CTE 写法清晰、可预览、支持事务回滚,适合生产环境。先运行 SELECT 验证,再改 DELETE 执行:
WITH ranked AS (
SELECT id, ROW_NUMBER() OVER (
PARTITION BY user_id, event_type ORDER BY created_at DESC, id DESC
) AS rn
FROM event_history
)
SELECT * FROM ranked WHERE rn > 1;
确认无误后,改成:
WITH ranked AS (
SELECT id, ROW_NUMBER() OVER (
PARTITION BY user_id, event_type ORDER BY created_at DESC, id DESC
) AS rn
FROM event_history
)
DELETE FROM event_history
WHERE id IN (SELECT id FROM ranked WHERE rn > 1);
-
PARTITION BY user_id, event_type定义“什么算重复”——业务上真正需要去重的维度 -
ORDER BY created_at DESC, id DESC确保最新事件优先得rn = 1;加id DESC是为打破时间相同导致的非确定性 - MySQL 8.0+ 和 PostgreSQL 支持该写法;SQL Server 可进一步简写为
DELETE FROM ranked WHERE rn > 1
MySQL 5.7 必须绕过 CTE 限制
MySQL 5.7 不支持在子查询中引用目标表,所以上面的 IN (SELECT ...) 会触发 ERROR 1093: You can't specify target table ... for update in FROM clause。必须改用自连接:
DELETE e1 FROM event_history e1 INNER JOIN event_history e2 ON e1.user_id = e2.user_id AND e1.event_type = e2.event_type AND e1.created_at
- 含义是:“删掉所有比同组另一条记录更旧的行”,等价于保留每组最新一条
- 如果存在
created_at完全相同的情况,需补上AND e1.id 避免漏删 - 务必在
(user_id, event_type, created_at)上建联合索引,否则多表扫描极慢
容易被忽略但致命的三点
实际执行时,以下问题不处理,轻则删错,重则锁表数分钟:
- NULL 值参与
PARTITION BY:不同数据库对NULL = NULL的判定不一致(PostgreSQL 默认不相等),可能导致本该归一组的行被拆开——建议提前用COALESCE(col, 'N/A')处理 - 没加事务包裹:千万级表删重可能耗时数十秒,期间其他写入会被阻塞;应始终用
BEGIN; ...; COMMIT;或ROLLBACK;控制 - 没验证影响行数:执行前先跑
SELECT COUNT(*) FROM (...) t WHERE rn > 1,若返回 10 万+,立刻停手检查PARTITION BY是否写错列










