SQL Server中DELETE不能直接使用ROW_NUMBER(),必须通过CTE封装;CTE需正确设置PARTITION BY和ORDER BY以精准去重,并建议先用SELECT预览、添加索引优化性能,生产环境须备份后操作。

直接用 ROW_NUMBER() 在 DELETE 里会报错
SQL Server 不允许在 DELETE 语句的顶层直接调用窗口函数,比如写成 DELETE FROM t WHERE ROW_NUMBER() OVER (...) > 1,会立刻报错:Windowed functions can only appear in the SELECT or ORDER BY clauses。这不是语法偷懒的问题,是引擎限制——窗口函数必须先产出一个带序号的结果集,再对它操作。
必须用 CTE 包一层才能删
CTE 是最干净、最可控的方式,它把 ROW_NUMBER() 的计算结果固化为一个可更新的命名结果集(SQL Server 2005+ 全支持)。关键点有三个:
-
PARTITION BY必须覆盖所有判定“重复”的列,比如按email和tenant_id判重,就不能只写PARTITION BY email,否则跨租户数据会被误合并 -
ORDER BY决定哪条被保留:要留最新就用updated_at DESC,留最早就用ID ASC;别用(SELECT NULL),它不保证顺序,不同执行可能保留不同行 - CTE 中
SELECT列无需全选,只生成ROW_NUMBER()就够了,能减少内存和排序开销
正确写法示例(删 news 表中重复 url、保留 publishtime 最大的):
WITH dupes AS (
SELECT ROW_NUMBER() OVER (
PARTITION BY url ORDER BY publishtime DESC
) AS rn
FROM news
)
DELETE FROM dupes WHERE rn > 1;
删之前不查 SELECT 预览等于闭眼开车
永远先用 SELECT 替换 DELETE,确认逻辑无误再动手。尤其注意两点:
- 检查
PARTITION BY列组合是否真能表达业务语义——比如两个用户邮箱相同但属于不同公司,PARTITION BY email就会错误合并 - 观察预览结果里的
rn = 1行是否确实是你要保留的那条,特别是时间字段精度到毫秒时,ORDER BY publishtime DESC可能因时区或默认值导致意外交替
预览语句就是把上面的 DELETE 换成 SELECT *,并补上关键字段方便核对:
WITH dupes AS (
SELECT ID, url, publishtime,
ROW_NUMBER() OVER (PARTITION BY url ORDER BY publishtime DESC) AS rn
FROM news
)
SELECT * FROM dupes WHERE rn > 1;
大表删重卡住?多半是缺索引
ROW_NUMBER() 在大数据量下会触发全表扫描 + 排序,如果 PARTITION BY 和 ORDER BY 涉及的列没索引,几百万行可能卡十几秒甚至超时。解决方法很直接:
- 建复合索引:字段顺序必须匹配窗口函数,比如
PARTITION BY a, b ORDER BY c DESC,对应索引应为CREATE INDEX IX_tab_a_b_c ON tab(a, b, c DESC) - 避免在
ORDER BY里用函数或表达式,比如ORDER BY YEAR(created_at)会导致索引失效 - 如果表本身有主键且你按主键保留(如
ORDER BY ID ASC),主键索引通常已覆盖排序需求,不用额外建
最后提醒一句:CTE 删除虽快,但不可回滚(除非显式包在事务里)。生产环境务必先备份,哪怕只是 SELECT * INTO news_backup_20261103 FROM news 一行。










