必须用row_number()配合partition by和order by分组编号并删rn>1的行;需通过cte或子查询中转,不可直接在delete中嵌套窗口函数,且须处理null、索引及版本兼容性问题。

用 ROW_NUMBER() 窗口函数标记并删除旧记录
直接 DELETE 时无法“保留最新一条”,必须借助排序和行号来识别哪些该删。核心思路是:按时间字段(如 created_at 或 id)降序排,给每行打上序号,只保留 ROW_NUMBER() = 1 的那条,其余全删。
常见错误是写成 DELETE FROM table WHERE id NOT IN (SELECT MAX(id) FROM table) —— 这在有重复时间、或 id 不连续/非主键时会漏删或多删;更糟的是,若表为空或只有 1 行,MAX(id) 返回 NULL,导致整张表被误删。
- 务必确保排序依据字段能唯一确定“最新”——优先用带时区的
created_at,其次才是id(前提是自增且不跳号) - PostgreSQL / SQL Server / Oracle / MySQL 8.0+ 都支持
ROW_NUMBER(),但语法细节不同:MySQL 要求子查询套一层,SQL Server 可直接在DELETE中用 CTE - 执行前先用
SELECT验证要删的行:SELECT *, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn FROM logs;
确认rn > 1的确实是预期旧数据
MySQL 8.0+ 实际可执行的删除语句
MySQL 不允许在子查询中直接引用被删表,所以必须用 CTE 或派生表绕过限制。以下写法经实测可用(假设按 user_id 分组,保留每组最新一条):
WITH ranked AS ( SELECT id, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn FROM logs ) DELETE l FROM logs l INNER JOIN ranked r ON l.id = r.id WHERE r.rn > 1;
注意:PARTITION BY 是可选的——如果目标是全表只留最新一条(不分组),就把 PARTITION BY user_id 去掉,改成 ORDER BY created_at DESC 即可。
- 别漏写
INNER JOIN,否则 MySQL 会报错You can't specify target table for update in FROM clause - 若用
id排序,确保它代表插入顺序;用created_at更安全,但要注意 NULL 值——加WHERE created_at IS NOT NULL过滤 - 大表慎用:该操作会锁表或产生大量 binlog,建议在低峰期执行,并提前备份
SQLite 中没有 ROW_NUMBER() 怎么办?
SQLite 3.25.0+ 支持窗口函数,但旧版本(比如 macOS 自带的 3.19)不支持。这时得用自关联 + 子查询模拟:
DELETE FROM logs WHERE id NOT IN ( SELECT MIN(id) FROM logs l2 WHERE l2.user_id = logs.user_id GROUP BY user_id );
这个写法靠 GROUP BY + MIN(id) 找出每组“最小 id”(即最早插入的),然后删掉所有不在这个集合里的行——但它保留的是“最早一条”,不是“最新一条”。要保留最新,得把 MIN(id) 换成 MAX(id),前提是 id 严格递增且能代表时间顺序。
- 如果时间字段是
created_at,且 SQLite 版本够新,优先用ROW_NUMBER() OVER (ORDER BY created_at DESC) - 如果版本太老又必须按时间删,只能先导出最新行到临时表,清空原表,再导入——没有优雅的单语句解
- 注意
NOT IN遇到子查询返回NULL时整个条件为UNKNOWN,导致零行被删;加AND id IS NOT NULL防御
WHERE 条件里的时间字段有 NULL 值怎么办?
ORDER BY created_at DESC 会让 NULL 排在最前面(MySQL 默认行为),结果可能是删掉了非 NULL 的新记录,留下 NULL 的“脏数据”。这不是 bug,是 SQL 标准定义。
- 显式控制 NULL 位置:
ORDER BY created_at DESC NULLS LAST(PostgreSQL / Oracle 支持);MySQL 和 SQLite 用ORDER BY created_at DESC, id DESC辅助排序 - 更稳妥的做法是过滤掉 NULL:
WHERE created_at IS NOT NULL加在子查询或 CTE 中,避免干扰排序逻辑 - 如果业务允许,建表时就给时间字段加
NOT NULL DEFAULT CURRENT_TIMESTAMP,从源头杜绝这个问题
SELECT 把 rn > 1 的行查出来看一眼,比反复试删安全得多。











