row_number()需配合partition by和order by才能定位每个用户的首次访问;漏掉partition by会导致仅返回全局首行;必须用cte或子查询过滤rn=1;索引应建(user_id, event_time)组合索引。

ROW_NUMBER() 必须配合 PARTITION BY 和 ORDER BY 才能定位“首次”
单独写 ROW_NUMBER() 不会自动识别“首次访问”,它只按指定顺序给行编号。日志中“首次”是相对每个用户(或设备、会话)而言的,所以必须用 PARTITION BY user_id(或 device_id、session_id)切分组,再用 ORDER BY event_time(或 log_timestamp)确保最早时间排第一。
常见错误是漏掉 PARTITION BY,结果整个表只排一次序,ROW_NUMBER() = 1 只返回全局最早那一条,完全不是每个用户的首次。
- 务必确认时间字段是可排序的类型(如
TIMESTAMP、DATETIME,不是字符串格式的 "2024-01-01 10:23" 除非已转为时间类型) - 如果日志含重复时间戳,建议追加二级排序,例如
ORDER BY event_time, log_id避免结果不稳定 - MySQL 8.0+、PostgreSQL、SQL Server、Oracle 都支持;但 SQLite 默认不支持窗口函数(需 3.25+ 且编译时启用)
WHERE 子句不能直接过滤 ROW_NUMBER() 别名,得用子查询或 CTE
ROW_NUMBER() 是窗口函数,在 WHERE 执行之后才计算,所以不能写 WHERE rn = 1 —— 会报错 “unknown column 'rn'”。必须先算出编号,再在外层筛选。
推荐用 CTE,清晰且兼容性好:
WITH ranked AS (
SELECT *,
ROW_NUMBER() OVER (
PARTITION BY user_id
ORDER BY event_time
) AS rn
FROM logs
)
SELECT user_id, source, event_time
FROM ranked
WHERE rn = 1;
如果数据库不支持 CTE(如旧版 MySQL),就用内联子查询,结构稍冗长但等效。
“来源”字段可能为空或脏数据,需要提前清洗
日志里的 source(或 utm_source、referral)常有空值、'direct'、'unknown'、URL 片段混杂等情况。直接取 rn = 1 的 source 可能返回无意义值。
- 可在
ORDER BY中调整优先级,例如把明确来源往前排:ORDER BY CASE WHEN source NOT IN ('', 'direct', 'unknown') THEN 0 ELSE 1 END, event_time - 或在最终 SELECT 中用
NULLIF(source, 'unknown')或CASE WHEN source IN ('', 'direct') THEN NULL ELSE source END过滤无效值 - 注意:ORDER BY 中的表达式不能引用别名,必须写完整逻辑
大数据量下性能敏感,索引设计直接影响效率
对千万级以上日志表,ROW_NUMBER() 会触发全表扫描+排序,没索引会极慢。关键索引不是单列 user_id 或 event_time,而是组合索引:
- 最优:
(user_id, event_time)—— 满足PARTITION BY + ORDER BY的最左匹配 - 若还需快速查
source,可扩展为(user_id, event_time, source),但索引体积增大 - 避免反向索引(如
(user_id, event_time DESC)),多数引擎对窗口函数优化有限,升序更稳妥
执行前务必 EXPLAIN 看是否走了索引;如果 PARTITION BY 字段基数极高(如 UUID),排序开销仍大,这时要考虑预聚合或物化中间表。











