SQL如何获取分组中第一条或最后一条记录:利用FIRST_VALUE函数

为什么FIRST_VALUE返回的不是“每组第一条记录”?
很多朋友第一次用FIRST_VALUE窗口函数时,都会遇到一个困惑:结果怎么和想的不一样?它确实按你指定的顺序,取到了窗口里的第一个值,但问题是——它把那个值“复制”给了组里的每一行,行数一点没少。这就像给团队每个人都发了一份队长的名片,但队长本人并没有被单独请出来。而我们实际业务中想要的,往往是“每组只留一条代表记录”,比如最新的那笔订单、最早的那次登录、分数最高的那位学生。这时候你就会发现,FIRST_VALUE更像是个辅助工具,单靠它,解决不了“提取整行数据”这个核心问题。
怎么用FIRST_VALUE配合ROW_NUMBER拿到完整记录?
那么,正确的“组合拳”该怎么打呢?核心思路其实很清晰:分两步走。第一步,用ROW_NUMBER()给每个分组内的行,按照你的排序规则(比如时间倒序)编上号(1,2,3…)。第二步,在外层查询里,轻松地WHERE rn = 1,只把每组的“1号”选手筛选出来。在这个过程中,FIRST_VALUE可以作为一个验证字段出现,但它不是筛选的主力。
这里有三个关键点必须注意:
- 排序是灵魂:
ORDER BY子句必须明确写在OVER()里面,否则ROW_NUMBER的编号顺序就是不确定的,结果自然不可靠。 - 分组要对齐:
PARTITION BY里的字段(比如user_id),必须和你想分组的逻辑完全一致。 - “最后一条”怎么取:很简单,把
ORDER BY created_at DESC改成ASC就行了。当然,你也可以用LAST_VALUE,但要小心它的默认窗口范围,最好显式指定为RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING,以免出错。
来看一个经典例子(获取每个用户的最新订单):
SELECT order_id, user_id, amount, created_at
FROM (
SELECT order_id, user_id, amount, created_at,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn
FROM orders
) t
WHERE rn = 1;
FIRST_VALUE在SELECT里直接用有哪些陷阱?
如果把FIRST_VALUE当成“取一行”的函数来用,很容易踩坑。因为它本质上是个“填充”函数,不负责删减行数。下面这几种情况,就是典型的误用:
- 场景一:想找“每组金额最高的订单号”。如果直接写
FIRST_VALUE(order_id) OVER (PARTITION BY user_id ORDER BY amount DESC),结果会是每一行都显示同一个最高的订单号,但你却看不到这个订单对应的金额、时间等其他信息。这显然不是我们想要的完整记录。 - 场景二:忘了写
ORDER BY。这时数据库可能会按物理存储顺序来排,每次执行的结果都可能不同,毫无稳定性可言。 - 场景三:在聚合查询里混用。窗口函数和
GROUP BY聚合不能直接放在同一层SELECT中,否则要么报错,要么语义变得混乱不清。
所以,更稳妥的做法是:要么先用子查询或CTE把窗口计算的结果准备好,再去做关联查询;要么就坚持使用ROW_NUMBER配合过滤的方案,思路直观,控制力也强。
MySQL 8.0+ 和 PostgreSQL 的行为差异要点
虽然主流数据库都支持这些窗口函数,但魔鬼藏在细节里,跨平台使用时尤其要注意:
- MySQL 8.0+ 更“较真”:它对
ORDER BY子句里的表达式要求更严格。比如,你不能直接使用SELECT列表里定义的别名(像ORDER BY dt),必须写原始字段名或列位置。 - PostgreSQL 处理空值更灵活:它支持
ORDER BY ... NULLS FIRST/LAST语法,让你能明确控制空值排在前面还是后面,这在MySQL里目前还不行。 - SQL Server 有“捷径”:在某些简单场景下,使用
TOP 1 WITH TIES配合FETCH NEXT子句,写起来可能比窗口函数更简短。但这种方法不具备跨多个分组分别取第一条的能力,功能上还是有区别的。
总的来说,如果考虑代码的兼容性和可读性,ROW_NUMBER方案是更安全的选择。而FIRST_VALUE,更适合扮演一个“锦上添花”的角色,用来生成一些派生字段,不建议让它承担核心的数据筛选逻辑。
