ROW_NUMBER()是分组去重取最新记录的关键,需配合PARTITION BY分组、ORDER BY排序(如updated_at DESC, id DESC)生成序号,再通过子查询或CTE外层WHERE rn=1筛选;GROUP BY无法直接保留完整行,DISTINCT仅基于整行去重。

GROUP BY 本身不能直接去重,ROW_NUMBER() 才是关键
GROUP BY 不会“去掉重复行”,它只是把数据分组后聚合;真要去重(保留每组一条记录),得靠 ROW_NUMBER() 配合子查询或 CTE。核心思路是:先用 ROW_NUMBER() 给每组内行编号,再筛出序号为 1 的行。
用 ROW_NUMBER() + WHERE 筛选组内首行
典型写法是套一层子查询或 CTE,避免在主查询里直接对窗口函数加 WHERE(语法不支持)。常见错误是写成 SELECT *, ROW_NUMBER() OVER (...) AS rn FROM t WHERE rn = 1 —— 这会报错,因为 WHERE 执行早于窗口函数计算。
-
ROW_NUMBER()的ORDER BY子句必须明确:它决定哪条是“第 1 行。如果只关心任意一条,可用ORDER BY (SELECT NULL)(SQL Server)或ORDER BY 1(部分数据库),但注意这不保证稳定性 - 分区字段要和你期望的“组”完全一致,比如按
user_id, product_id分组去重,PARTITION BY就得写这两个字段,漏一个就会合并错误的行 - PostgreSQL 和 SQL Server 支持直接在 CTE 中写
WITH t2 AS (SELECT *, ROW_NUMBER() OVER (...) AS rn FROM t),MySQL 8.0+ 同样支持;老版本 MySQL 需用变量模拟,但不可靠、不推荐
示例:保留每个 user_id 下最新的一条订单
假设表 orders 有 user_id、order_time、amount 字段,要按用户取最新订单:
SELECT user_id, order_time, amount
FROM (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY order_time DESC) AS rn
FROM orders
) t
WHERE rn = 1;
注意:ORDER BY order_time DESC 确保最新时间排第一;如果 order_time 有重复,可能需要加二级排序(如 id DESC)来保证结果确定性。
性能与兼容性要点
ROW_NUMBER() 是标准窗口函数,但执行开销比纯 GROUP BY 大——它需扫描全量分组数据并排序。若表极大且只想要任意一行,DISTINCT ON(PostgreSQL)或 QUALIFY(BigQuery、Snowflake)更高效;但跨数据库时,ROW_NUMBER() 最通用。
- SQLite 直到 3.25.0 才支持窗口函数,旧版本无法用此方案
- Oracle 中
ROW_NUMBER()行为一致,但要注意NULLS FIRST/LAST默认行为可能影响排序结果 - 如果业务允许“去重后聚合”,比如只取每组最大金额,其实用
MAX(amount)配合GROUP BY更快,不必硬上窗口函数
真正容易被忽略的是:窗口函数里的 ORDER BY 不仅影响编号顺序,还隐式触发排序操作——没索引时,大表分组去重可能变慢十倍以上。

















