最稳妥的方式是用窗口函数标序再删序号>1的行:先用ROW_NUMBER()按user_id分组、created_at降序编号,再删除rn>1的记录;不可用GROUP BY+DELETE联用,也不能直接删CTE。

用 ROW_NUMBER() 配合 CTE 删除重复组内多余行
直接删多条留一条,不能靠 GROUP BY + DELETE 联用——SQL 不允许在 DELETE 语句里直接引用分组聚合结果。最稳妥的方式是用窗口函数标序,再删序号 > 1 的行。
典型场景:按 user_id 分组,每组只保留最新一条(以 created_at 降序为准),其余删掉。
WITH ranked AS (
SELECT id, user_id, created_at,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn
FROM orders
)
DELETE FROM orders
WHERE id IN (
SELECT id FROM ranked WHERE rn > 1
);-
ROW_NUMBER()必须配合PARTITION BY和明确的ORDER BY,否则排序无意义,留哪条不可控 - 别用
RANK()或DENSE_RANK():它们对相同排序值会并列编号,可能导致多条被标为 1,或漏删 - 子查询里必须用主键(如
id)关联,不能直接DELETE FROM ranked WHERE rn > 1——CTE 在多数数据库中不支持直接删
MySQL 8.0+ 可用 JOIN 语法更直观
MySQL 8.0 开始支持 CTE 和窗口函数,但它的 DELETE ... JOIN 写法更贴近直觉,且避免嵌套子查询。
DELETE o1 FROM orders o1 INNER JOIN orders o2 ON o1.user_id = o2.user_id AND o1.created_at < o2.created_at;
- 这句逻辑是:“删掉所有存在同
user_id且时间更早的记录”——即每组只留created_at最大的那条 - 注意条件是
,不是 <code>,否则时间完全相同的记录会互相匹配,全被删光 - 该写法在 PostgreSQL 和 SQL Server 中不合法,仅 MySQL 支持;SQLite 不支持窗口函数也不支持这种 DELETE JOIN
没有主键或时间字段时,靠 MIN(id) 留一条
如果表只有业务字段、没时间戳也没唯一标识,又必须“每组留一条”,只能退而求其次:按 id(假设自增)选最小的留下。
DELETE FROM products WHERE id NOT IN ( SELECT MIN(id) FROM products GROUP BY category, brand );
-
GROUP BY字段必须是判定“重复组”的依据,比如category和brand相同算一组 - 这个写法在大表上极慢:子查询要全表扫描 + 分组,外层
NOT IN还可能因 NULL 值失效(若id允许 NULL) - PostgreSQL 可改用
NOT EXISTS规避 NULL 问题;SQL Server 推荐用ROW_NUMBER()替代——哪怕没业务时间字段,也能按id排序
执行前务必备份 + 加 WHERE 限流
这类操作本质是批量删除,一旦写错条件就是灾难。线上环境绝不能直接跑完整语句。
- 先用
SELECT模拟要删的数据:SELECT id, user_id, created_at FROM ranked WHERE rn > 1 LIMIT 10; - 加
LIMIT分批删(尤其 MySQL):DELETE ... LIMIT 1000;,避免锁表太久 - 确认表有索引:
(user_id, created_at)复合索引能极大加速PARTITION BY + ORDER BY场景 - 某些数据库(如旧版 MySQL)不支持在子查询里直接引用目标表,得套一层
SELECT * FROM (subquery) AS tmp
真正麻烦的从来不是语法怎么写,而是删完发现留下的那条不是业务想要的“最新”或“有效”数据——排序依据是否真能反映业务优先级,比函数怎么调用重要得多。

















