ROW_NUMBER()必须配合PARTITION BY和ORDER BY才能实现分组内排序编号;漏掉PARTITION BY会导致全表编号,ORDER BY需用带时序语义字段并处理NULL及时区问题。

ROW_NUMBER() 必须配合 PARTITION BY 和 ORDER BY 才能分组排序
单独写 ROW_NUMBER() 不会自动分组,它默认对整个结果集编号。想按业务维度(比如用户、订单号、设备ID)各自取最新或最早一条,PARTITION BY 是硬性前提,ORDER BY 决定“最新”或“最早”的含义。
常见错误是漏掉 PARTITION BY,结果所有记录被当成一组编号;或者 ORDER BY 用了不带时序语义的字段(如 name),导致“最新”不可控。
-
PARTITION BY user_id:每个用户独立编号 -
ORDER BY create_time DESC:时间倒序 → 编号为 1 的就是最新记录 -
ORDER BY create_time ASC:时间正序 → 编号为 1 的就是最早记录 - 注意:如果
create_time可能重复,建议追加唯一字段(如id)避免排序不稳定,例如ORDER BY create_time DESC, id DESC
嵌套子查询是主流做法,CTE 更易读但非必须
窗口函数不能直接出现在 WHERE 子句中,所以必须把 ROW_NUMBER() 放在子查询或 CTE 里,再在外层筛选 rn = 1。
两种写法效果一致,选哪种取决于可维护性需求:
- 子查询适合简单场景:
SELECT * FROM ( SELECT *, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY update_time DESC) AS rn FROM orders ) t WHERE t.rn = 1
- CTE 更清晰,尤其当基础数据需要多次引用:
WITH ranked AS ( SELECT *, ROW_NUMBER() OVER (PARTITION BY device_id ORDER BY report_time DESC) AS rn FROM sensor_logs ) SELECT id, device_id, value, report_time FROM ranked WHERE rn = 1
慎用 ROW_NUMBER() 取“最新”代替 DISTINCT ON 或 GROUP BY MAX()
ROW_NUMBER() 返回的是完整行,而 GROUP BY + MAX(update_time) 只能拿到时间值,要回表查整行就得连表;DISTINCT ON(PostgreSQL 特有)语义更直白,但跨数据库不兼容。
性能上要注意:
- 如果只是要某个聚合值(如最新时间、最大版本号),
GROUP BY + MAX()通常更快,不需要排序开销 - 如果必须返回整行且字段多、关联深,
ROW_NUMBER()配合索引(如(user_id, update_time DESC))效率不错 - 没有索引支撑的
PARTITION BY + ORDER BY可能触发大量临时磁盘排序,执行计划里看到WindowAgg+Sort耗时高就要警惕
NULL 值和时区可能让“最新”判断失效
ORDER BY update_time DESC 遇到 NULL 时,不同数据库行为不一致:PostgreSQL 默认把 NULL 排最后(符合直觉),MySQL 8.0+ 默认排最前(可能把空时间误判为“最新”)。
解决方案不是靠数据库默认,而是显式控制:
- 过滤掉无效时间:
WHERE update_time IS NOT NULL - 或统一补默认值:
ORDER BY COALESCE(update_time, '1970-01-01') DESC - 时区问题更隐蔽:如果
update_time是TIMESTAMP WITHOUT TIME ZONE,且应用写入时未标准化,同一物理时刻在不同会话里可能被解释成不同时间 → 最终排序错乱
真正稳定的“最新”,依赖写入时就存 UTC 时间戳,并用 TIMESTAMP WITH TIME ZONE 类型存储。否则,窗口函数再准也救不了源头数据。










