SQL窗口函数如何辅助复杂删除逻辑?

作者:技术拾光

时间:2026-07-21

浏览:117

利用窗口函数ROW_NUMBER()无法直接用于DELETE的WHERE子句。需先通过CTE或子查询计算行号,再在外层DELETE中删除rn1的重复行。PARTITIONBY定义重复依据,ORDERBY决定保留顺序。注意MySQL需额外嵌套子查询,执行前应先用SELECT验证结果。

文章目录

一个很常见的需求是:在 SQL 里删除重复行,只保留一条。很多人第一反应想到窗口函数,毕竟 ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...) 能轻松给数据排出行号。但有个问题——窗口函数不能直接用在 DELETE 的 WHERE 子句里。直接写 DELETE FROM t WHERE ROW_NUMBER() OVER (...) = 1 是行不通的,数据库会直接报错,告诉你窗口函数只能出现在 SELECT 或 ORDER BY 子句中。

所以,正确的思路是先通过子查询或 CTE 把行号算出来,然后在外层 DELETE 里根据条件(比如 rn > 1)去删除。这里的核心逻辑分两部分:PARTITION BY 定义什么算“重复”,ORDER BY 决定你留下哪一条(比如时间最晚的那一条)。

SQL窗口函数如何辅助复杂删除逻辑?

窗口函数本身不能直接删除数据

这个问题值得先明确一下。你不能在 DELETE 语句的 WHERE 条件里直接调用窗口函数,比如 DELETE FROM t WHERE ROW_NUMBER() OVER (...) = 1 这种写法就是犯规的,数据库会抛出错误提示:“Windowed functions can only appear in the SELECT or ORDER BY clauses”。窗口函数允许出现的位置就那么几个:SELECT 子句、ORDER BY 子句,某些数据库里还能用于 GROUP BY,但绝不支持用在 WHERE 或 DELETE 的过滤条件中。

真正可行的路径是:先用窗口函数把要删的行“找出来”,然后把结果作为子查询或公共表表达式(CTE)提供给 DELETE 使用。这是一个必须记住的基本约束。

用 CTE + ROW_NUMBER() 删除重复行保留最新一条

这是最常见的使用场景。某个业务键存在重复记录(比如同一个 user_id 有多条数据),你想按照某个时间字段(比如 updated_at)只保留最新的一条,把其他的都删掉。

  • 必须用 WITH 定义 CTE,在里面计算 ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY updated_at DESC)
  • DELETE 操作需要基于 CTE 的别名来执行。PostgreSQL 和 SQL Server 对这种用法支持比较直接;MySQL 8.0+ 的话,需要用 DELETE ... FROM cte JOIN table 这种写法绕一下
  • 排序方向很关键:用 DESC 才能让最新记录排到第 1 位,否则就会误删你要留的那条
  • 以 PostgreSQL 为例,标准写法是这样的:
WITH ranked AS (  SELECT id, ROW_NUMBER() OVER (    PARTITION BY user_id ORDER BY updated_at DESC  ) rn  FROM users)DELETE FROM usersUSING rankedWHERE users.id = ranked.id AND ranked.rn > 1;

MySQL 8.0 中需绕过 CTE 直接删除的限制

MySQL 有一个比较“坑”的限制:不允许在 DELETE 中直接引用同一个表的 CTE,否则会报错——“You can't specify target table for update in FROM clause”。换句话说,你不能让它自己在自己身上动刀子。解决方案通常有两种:

  • 采用派生表(子查询再套一层)
  • 用临时表保存要删除的 id 列表
  • 推荐第一种,因为不需要显式创建临时表,更简洁
  • 比如删除重复邮箱中创建时间较早的记录,可以这样写:
DELETE FROM usersWHERE id IN (  SELECT id FROM (    SELECT id,      ROW_NUMBER() OVER (        PARTITION BY email ORDER BY created_at ASC      ) rn    FROM users  ) t WHERE t.rn > 1);

注意这里最外层套了一层 SELECT id FROM ( ... ) t,这层包装是必不可少的,少了它 MySQL 就会报错。

性能和事务安全提醒

窗口函数本身的性能还算不错,但一旦和 DELETE 搭配起来,有两个容易忽略的地方需要特别留意:

  • 在执行删除前,务必确认 PARTITION BY 指定的字段上有索引。否则窗口函数内部做排序的开销会非常大,尤其是在大表上
  • DELETE 是 DML 操作,会锁行甚至锁表。建议在业务低峰期执行,或者在数据量特别大时分批次删除(例如加上 LIMIT 1000 然后循环执行)
  • 一个值得养成的好习惯:永远先用 SELECT 验证一下 CTE 或子查询的结果对不对。比如先跑一下 SELECT * FROM (CTE) WHERE rn > 1,确认要删的确实是那些该删的行
  • 另外需要注意的是,某些旧版本的数据库,比如 PostgreSQL 的 USING 语法在不同版本之间有细微差别,生产环境执行前最好在同版本环境下做一次实测

说到底,窗口函数只是一个“找人”的工具,真正执行删除操作的是 DELETE。找得准不准,全看你的 PARTITION BY 和 ORDER BY 写得对不对——工具本身再强大,也得看怎么用它。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章