sql server分组去重必须用row_number()配合cte或子查询,因distinct无法指定保留行、group by强制非聚合字段全出现在分组列表中,而qualify不被支持。

SQL Server 中分组去重必须用 ROW_NUMBER() 配合子查询或 CTE,QUALIFY 不可用,硬上会报错。
为什么不能直接用 DISTINCT 或 GROUP BY 做“保留最新一条”的分组去重
因为 DISTINCT 是对整行去重,无法控制“留哪一条”;GROUP BY 要求所有非聚合字段都出现在 GROUP BY 列表里,否则 SQL Server 会直接报错(如 Column 'name' is invalid in the select list)。你想取每个 user_id 的最新订单,但还要带出 order_id、amount、created_at 等字段——这时候只有窗口函数能保全原始行结构。
常见翻车点:
- 写
SELECT user_id, order_id, MAX(created_at) FROM orders GROUP BY user_id→ 报错,order_id没在GROUP BY里 - 强行加
order_id进GROUP BY→ 结果变成每个(user_id, order_id)组都返回一行,根本没去重 - 用
DISTINCT user_id, order_id, created_at→ 所有记录都不同,毫无效果
ROW_NUMBER() + CTE 是 SQL Server 最稳的分组去重写法
核心逻辑:先按业务规则排序(比如 ORDER BY created_at DESC),再编号,最后只取每组第 1 名。
示例:取每个用户最新一条登录记录
WITH ranked AS (
SELECT *,
ROW_NUMBER() OVER (
PARTITION BY user_id
ORDER BY login_time DESC
) AS rn
FROM logins
)
SELECT user_id, login_time, ip_address
FROM ranked
WHERE rn = 1;
关键注意点:
-
PARTITION BY字段必须是你想“分组”的依据,比如user_id、device_id,别漏或多写 -
ORDER BY决定谁是“最新”或“最优”,时间字段要带方向(DESC取最新,ASC取最早) - 如果存在完全相同的
login_time,ROW_NUMBER()仍会强制编号(1,2,3…),结果不确定;可追加id作为次序键:ORDER BY login_time DESC, id DESC
删除重复数据时,ROW_NUMBER() 必须配合 CTE 或子查询才能 DELETE
SQL Server 不允许直接在 DELETE 语句里用窗口函数,所以必须把编号逻辑封装进 CTE,再对 CTE 执行删除。
示例:删除 DeviceAlarms 表中设备+告警类型+时间完全相同的重复项(仅保留 ID 最小的一条)
WITH dups AS (
SELECT *,
ROW_NUMBER() OVER (
PARTITION BY DeviceId, AlarmType, AlarmTime
ORDER BY Id
) AS rn
FROM DeviceAlarms
)
DELETE FROM dups WHERE rn > 1;
这个写法安全且高效,但要注意:
- 不要用
ORDER BY Id DESC然后删rn > 1—— 那会把最小 ID 留下,但编号顺序反了,逻辑易错 - 大表操作前务必
BEGIN TRANSACTION,验证后再COMMIT - 若表无主键或
Id不唯一,改用%%physloc%%或临时加标识列,但风险高,不推荐
DENSE_RANK() 和 RANK() 在去重场景里基本没用
它们设计目标是「并列排名」,不是「去重」。比如三个相同值,DENSE_RANK() 返回 1,1,1,ROW_NUMBER() 返回 1,2,3 —— 但你删 rn > 1 时,DENSE_RANK() 会把三行全留下,起不到去重作用。
真正需要去重时,只认 ROW_NUMBER();如果只是想展示“去重后的名次”(比如每个部门薪资前三名,相同薪资并列且不跳号),才考虑 DENSE_RANK(),但它不能替代去重动作本身。
最常被忽略的一点:窗口函数的结果不能直接用于 WHERE,必须套一层 CTE 或子查询——这个语法限制在 SQL Server 里没有绕过办法,硬写会报 Invalid column name 'rn'。










