“最长会话”是指同一用户连续操作中(间隔≤阈值,如5分钟)起点到终点时间差最大的那个会话,通过LAG()标记起点、SUM()累计生成session_id、再按session_id分组计算duration后用ROW_NUMBER()取最大值确定。

什么是“最长会话”?先明确时间字段和业务定义
会话(session)在SQL里没有内置概念,必须靠业务逻辑定义:通常是同一 user_id 的连续操作,以时间戳(如 event_time 或 created_at)为依据。关键在于——“连续”怎么判?常见做法是:若两次操作间隔 ≤ 5 分钟(或 30 分钟),视为同一次会话;超过则切分。这个阈值必须由你根据实际日志粒度确定,不能硬编码为“最大时间差”,否则会把用户全天零散点击合并成一个假长会话。
- 会话起点 = 某条记录,且它的前一条同用户记录时间差 > 阈值(或它是该用户第一条)
- 会话终点 = 从起点开始,向后延伸直到下一条记录超出阈值
- 最长会话 = 所有会话中(终点时间 − 起点时间)最大的那个
用窗口函数 + 自连接还是递归CTE?选对方法才能不爆内存
别用自连接暴力算每对时间差——N² 复杂度,万级用户就卡死。LAG() + 累计标记是更稳的路:
- 先按
user_id,event_time排序,用LAG(event_time) OVER (PARTITION BY user_id ORDER BY event_time)拿到上一行时间 - 计算间隔:
event_time - LAG(...) > INTERVAL '5 minutes'(PostgreSQL)或TIMESTAMPDIFF(MINUTE, ..., ...) > 5(MySQL) - 用布尔值转整数再
SUM() OVER做会话ID:每次间隔超阈值就+1,形成连续分组
示例(PostgreSQL):
SELECT user_id,
MIN(event_time) AS session_start,
MAX(event_time) AS session_end,
MAX(event_time) - MIN(event_time) AS duration
FROM (
SELECT *,
SUM(is_new_session) OVER (PARTITION BY user_id ORDER BY event_time) AS session_id
FROM (
SELECT *,
CASE WHEN event_time - LAG(event_time) OVER (PARTITION BY user_id ORDER BY event_time)
> INTERVAL '5 minutes' THEN 1 ELSE 0 END AS is_new_session
FROM user_events
) t1
) t2
GROUP BY user_id, session_id
查“每个用户最长会话”时,ROW_NUMBER() 比 MAX() 更可靠
有人想先算出每个用户的 MAX(duration),再回连原表找对应会话——这在有多个等长会话时会丢数据,且多一层JOIN易出错。直接用窗口函数排序取首行更干净:
- 在上一步的会话聚合结果上,加
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY duration DESC, session_start) - 外层
WHERE rn = 1即得每个用户的最长会话(加session_start是为了等长时确定性取最早那个)
注意:如果用 RANK(),等长会话都会被返回,可能不符合“每个用户一条”的需求。
MySQL 5.7 或 SQL Server 2016 以前版本怎么办?
这些旧版本不支持窗口函数里的 LAG() 或累计 SUM()。只能用变量模拟(MySQL)或自连接 + 子查询(通用但慢):
- MySQL 变量方案需严格按
ORDER BY user_id, event_time排序后逐行计算,且必须写成单条语句(不能视图或CTE里用) - SQL Server 2016 前可改用
CROSS APPLY查每个事件的“下一个超阈值时间”,但性能随数据量下降极快 - 如果数据量不大(< 10 万行),临时表 + 循环更新会话ID 也是可接受的退路
真正卡住的往往不是语法,而是没意识到:会话切分阈值一旦设错,整个结果就偏移;而日志里 event_time 若含毫秒但数据库类型是 DATETIME(精度秒),LAG() 算出的间隔可能全为 0。

















