用row_number()标记用户首次行为本质是按event_time升序排序后取序号为1的记录,需partition by user_id、order by event_time asc(建议加where event_time is not null及二级排序防歧义),后续where rn=1即可提取。

用 ROW_NUMBER() 标记首次行为
用户“第一次行为”本质是按时间排序后序号为 1 的那条记录。必须明确指定排序依据(通常是 event_time 或 created_at),且需注意 NULL 值处理——如果时间字段可能为空,ORDER BY event_time ASC 会把 NULL 排最前,导致误判。实际中建议加 WHERE event_time IS NOT NULL 预过滤。
- 窗口定义必须含
PARTITION BY user_id,否则全表只排一次序 -
ORDER BY event_time ASC中建议加上NULLS LAST(PostgreSQL/Oracle 支持)或用COALESCE(event_time, '9999-12-31')(兼容 MySQL) - 示例:
SELECT user_id, event_type, event_time, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY event_time ASC) AS rn_first FROM events;后续只需WHERE rn_first = 1即可取出每人首条
用 ROW_NUMBER() 或 LAST_VALUE() 获取末次行为
“最后一次行为”同样依赖时间排序,但方向相反。用 ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY event_time DESC) 最直观、兼容性最好;LAST_VALUE() 虽语义贴切,但默认是 RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,不加 ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING 会返回错行——这是新手高频翻车点。
- MySQL 8.0+、PostgreSQL、SQL Server 均支持
ROW_NUMBER(),无需额外配置 - 若需在单条 SELECT 中同时拿到首末,可嵌套两个
ROW_NUMBER(),分别按 ASC/DESC 排序,再用条件过滤 - 注意:多个行为时间相同时,
ROW_NUMBER()会任意分配序号,如需稳定结果,应在ORDER BY中加入唯一列(如id)做二级排序:ORDER BY event_time DESC, id DESC
合并首末行为到一行:用条件聚合或自连接
直接用两个窗口函数标记后,在外层用 MAX(CASE WHEN rn_first = 1 THEN ... END) 是最轻量的做法,避免关联开销。自连接(如 LEFT JOIN 首次表 ON u.id = f.user_id)在数据量大时容易拖慢,尤其没建好 (user_id, event_time) 复合索引时。
- 条件聚合写法简洁,但字段多时易冗长;推荐封装为 CTE 提高可读性
- 如果还需计算首末间隔(如留存天数),用
MIN(event_time)和MAX(event_time)更高效,无需窗口函数 - 示例(获取每人首末事件类型与时间):
WITH ranked AS ( SELECT user_id, event_type, event_time, 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 WHERE event_time IS NOT NULL ) SELECT user_id, MAX(CASE WHEN rn_first = 1 THEN event_type END) AS first_event, MIN(event_time) AS first_time, MAX(CASE WHEN rn_last = 1 THEN event_type END) AS last_event, MAX(event_time) AS last_time FROM ranked GROUP BY user_id;
MySQL 5.7 或旧版 PostgreSQL 不支持窗口函数怎么办
这些版本无法用 ROW_NUMBER(),得退回到相关子查询或变量模拟。变量方式(如 @row_number := IF(@prev = user_id, @row_number + 1, 1))在 MySQL 中有执行顺序风险,官方已不推荐;更稳妥的是用自关联求最小/最大时间:
- 先算每人最早/最晚时间:
SELECT user_id, MIN(event_time) AS first_t, MAX(event_time) AS last_t FROM events GROUP BY user_id - 再连回原表取完整行:
JOIN events e1 ON e1.user_id = t.user_id AND e1.event_time = t.first_t - 注意:若存在时间重复,可能返回多行,需加
LIMIT 1或用GROUP BY+ 聚合函数(如MIN(e1.id))保唯一
窗口函数确实让首末行为提取变得干净,但真正上线前得确认目标数据库版本、时间字段的空值分布、以及是否存在毫秒级时间冲突——这些细节不处理,结果就不可信。










