row_number()需配合partition by user_id与order by event_time(asc/desc)标记首末行,注意null及同秒排序稳定性;first_value/last_value须显式指定unbounded preceding to unbounded following窗口帧;条件聚合+max(case when)是合并首末行为到单行的可靠方法。

用 ROW_NUMBER() 标记首次和末次行为
窗口函数本身不直接返回“首次”或“末次”的值,但可以用 ROW_NUMBER() 按时间排序后取序号为 1 的行。关键在于分区与排序逻辑:按用户分组(PARTITION BY user_id),再按行为时间升序(首次)或降序(末次)排序。
常见错误是忽略 NULL 时间值导致排序错位,或未明确指定 ORDER BY 中的二级排序(比如同秒内多条行为时结果不稳定)。建议在 ORDER BY event_time 后追加 event_id 等唯一字段保序:
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY event_time ASC, event_id ASC) AS rn_first
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY event_time DESC, event_id DESC) AS rn_last
用 FIRST_VALUE() 和 LAST_VALUE() 直接取值
这两个函数更贴近“取首次/末次行为字段”的直觉需求,但必须注意默认窗口帧(ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)会让 LAST_VALUE() 只看到当前行及之前——它不会自然返回组内最后一行的值。
正确写法需显式声明完整窗口范围:
FIRST_VALUE(event_type) OVER (PARTITION BY user_id ORDER BY event_time ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS first_event
LAST_VALUE(event_type) OVER (PARTITION BY user_id ORDER BY event_time ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS last_event
若只关心时间字段,MIN(event_time) 和 MAX(event_time) 更简洁且无帧陷阱,但无法附带关联字段(如对应的行为类型)。
合并首次与末次行为到同一行输出
多数场景需要把每个用户的首次行为时间、类型和末次行为时间、类型放在一行里,不能靠单个窗口函数一步到位。得先用子查询或 CTE 标出标记,再聚合。
推荐做法是:用 ROW_NUMBER() 打标后,在外层用条件聚合(CASE WHEN rn_first = 1 THEN ... END)提取字段:
SELECT
user_id,
MAX(CASE WHEN rn_first = 1 THEN event_time END) AS first_time,
MAX(CASE WHEN rn_first = 1 THEN event_type END) AS first_type,
MAX(CASE WHEN rn_last = 1 THEN event_time END) AS last_time,
MAX(CASE WHEN rn_last = 1 THEN event_type END) AS last_type
FROM (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY event_time ASC) AS rn_first,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY event_time DESC) AS rn_last
FROM events
) t
GROUP BY user_id;
注意:这里用 MAX() 是因为每组只有一个非 NULL 值,本质是取值而非真求最大;若用 MIN() 效果一样,但语义不如 MAX 清晰(毕竟我们没在比大小)。
性能与兼容性提醒
FIRST_VALUE() / LAST_VALUE() 在 PostgreSQL、SQL Server、Oracle、Trino 中行为一致,但在 MySQL 8.0+ 才支持,且旧版 Hive 不支持完整 ROWS 子句;ROW_NUMBER() 兼容性更好。
大数据量下,PARTITION BY user_id ORDER BY event_time 的排序开销不可忽视。如果表已有 (user_id, event_time) 复合索引,能显著加速窗口计算——否则可能触发全局排序,拖慢查询数倍。
真正容易被忽略的是行为时间精度:如果原始字段是 DATETIME 但业务上“同秒即视为同时”,那排序时仅用 event_time 可能导致首次/末次判定随机。此时应提前截断或增加确定性排序键。










