FIRST_VALUE和LAST_VALUE是窗口函数,不减少行数,仅复制值;LAST_VALUE默认帧只到当前行,需显式指定UNBOUNDED FOLLOWING才得末次值;取完整记录应优先用ROW_NUMBER()过滤。

FIRST_VALUE 和 LAST_VALUE 能拿到首次/末次行为的值,但直接用很容易返回错行——它们不筛选记录,只给每行“打个标”,且 LAST_VALUE 默认窗口范围只到当前行,几乎必然出错。
为什么FIRST_VALUE(LAST_VALUE)返回的不是“首次/末次那条记录”
这两个函数是窗口函数,不是聚合函数,不会减少行数。你执行 SELECT user_id, FIRST_VALUE(page) OVER (PARTITION BY user_id ORDER BY ts) FROM events,结果仍是原始所有行,只是每行多了一列“该用户最早访问的 page”,但这条 page 对应的完整记录(比如时间、设备信息)并不一定在当前行上。
- 想取“整条首次行为记录”,得配合
ROW_NUMBER()+WHERE rn = 1过滤 -
FIRST_VALUE默认窗口帧是RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,哪怕没重复时间,它也只保证“从分区开头到当前行为止”的第一个值,不是整个分区的绝对首行 - 排序字段有重复(如多个行为同秒发生),不同数据库对相等行的物理顺序处理不一致,
FIRST_VALUE可能随机选一个
LAST_VALUE总等于当前行?必须显式指定窗口帧
这是最常踩的坑:LAST_VALUE(col) OVER (PARTITION BY x ORDER BY y) 默认帧仍是 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,所以它永远返回当前行的 col 值,根本不是“末次”。
- 必须写全:
LAST_VALUE(col) OVER (PARTITION BY x ORDER BY y ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) - 注意
ORDER BY方向:按时间升序时,LAST_VALUE才对应最新行为;若降序,则对应最早行为 - 单行分组(某用户只有一条行为)时,
UNBOUNDED FOLLOWING安全,但某些旧版 Hive 可能不支持,需实测
更稳妥的替代方案:ROW_NUMBER() + 子查询
当目标是“每个用户只留一条首次行为记录”或“一条末次行为记录”,直接用 ROW_NUMBER() 过滤比依赖 FIRST_VALUE/LAST_VALUE 更清晰、更可控。
- 取首次行为完整记录:
SELECT user_id, page, ts, device FROM ( SELECT user_id, page, ts, device, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY ts ASC) AS rn FROM events ) t WHERE rn = 1; - 取末次行为完整记录:把
ORDER BY ts ASC改成ORDER BY ts DESC - 如果需同时返回首次和末次(各一行合并为一列),再套一层聚合,例如用
MIN(ts)和MAX(ts)关联原表,而非硬套窗口函数
MySQL 8.0+ 与 PostgreSQL 的实际差异点
语法都支持 FIRST_VALUE/LAST_VALUE,但行为细节容易忽略:
- PostgreSQL 中
DISTINCT ON (user_id) ORDER BY user_id, ts是更常用、更简洁的取首条方式,且天然避免窗口帧陷阱 - MySQL 8.0+ 对
UNBOUNDED FOLLOWING支持稳定,但若排序字段无索引,OVER (PARTITION BY ... ORDER BY ...)性能会明显下降 - 两者在
ts字段存在 NULL 时处理逻辑不同:MySQL 把 NULL 排在最前(ASC 时),PostgreSQL 默认排最后,影响FIRST_VALUE结果——务必用COALESCE(ts, '1970-01-01')或明确NULLS FIRST/LAST(PostgreSQL 支持,MySQL 不支持)
真正难的不是写对函数名,而是意识到 FIRST_VALUE 和 LAST_VALUE 本质是“复制值”,不是“选取记录”;而窗口帧、排序稳定性、NULL 处理这些细节,一漏就导致线上数据偏差。

















