max(time) - min(time) 不能代表平均停留时长,因其反映的是流程首尾时间差,包含空档期;真实停留时长应累加相邻步骤间的时间间隔,需用lead()/lag()配对计算或自连接模拟,并排除异常值后求平均。

为什么用 MAX(time) - MIN(time) 不能直接代表平均停留时长
很多人看到“流程开始到结束的时间差”,第一反应是取每个流程的 MAX(event_time) - MIN(event_time),再对所有流程求平均。这看似合理,但实际会严重高估——因为一个流程中可能有多个事件,而 MIN 和 MAX 取的是该流程内最早和最晚的两个时间点,中间若存在长时间空档(比如用户提交后隔了一天才继续操作),这个差值就不是“停留”而是“挂起”。真正的停留时长应基于**相邻步骤之间的时间间隔**累加,而非首尾硬拉。
正确做法:用窗口函数配对相邻事件
核心思路是把每个流程的事件按时间排序,然后让当前行与下一行配对,计算两步之间的时间差。这需要 LEAD() 或 LAG() 窗口函数。以 PostgreSQL/MySQL 8.0+/SQL Server 为例:
SELECT
flow_id,
event_time AS start_time,
LEAD(event_time) OVER (PARTITION BY flow_id ORDER BY event_time) AS next_time,
LEAD(event_time) OVER (PARTITION BY flow_id ORDER BY event_time) - event_time AS duration_sec
FROM events
WHERE event_type IN ('submit', 'review', 'approve', 'complete');
关键点:
-
PARTITION BY flow_id确保只在同一流程内比较,不跨流程错配 -
ORDER BY event_time必须存在,否则LEAD()返回顺序不可控 - 最后一行的
next_time是NULL,对应流程终点,自然被过滤掉(不影响AVG()计算) - 如果时间字段是
TIMESTAMP,差值单位取决于数据库(PostgreSQL 返回INTERVAL,需转为秒;MySQL 返回秒数)
兼容老版本 MySQL(无窗口函数)的替代方案
MySQL 5.7 或更早必须用自连接模拟“下一行”。性能较差,且需确保时间戳唯一或加辅助排序字段:
SELECT a.flow_id, a.event_time AS start_time, MIN(b.event_time) AS next_time, TIMESTAMPDIFF(SECOND, a.event_time, MIN(b.event_time)) AS duration_sec FROM events a JOIN events b ON a.flow_id = b.flow_id AND b.event_time > a.event_time GROUP BY a.flow_id, a.event_time;
注意陷阱:
- 没有
ORDER BY保证,MIN(b.event_time)是“下一个最近时间”,但若存在并行事件(同一秒多个操作),可能跳过真正下一步 - 数据量大时
JOIN易爆内存,建议在(flow_id, event_time)上建联合索引 - 无法处理事件时间完全相同的情况,需提前去重或引入序列号字段
最终求平均时务必排除异常值
真实业务中常有测试数据、超时挂起、人工补录等导致极长间隔(比如 30 天)。直接 AVG(duration_sec) 会被严重扭曲:
- 先用
PERCENTILE_CONT(0.95)(PostgreSQL/SQL Server)或近似分位数估算上限阈值 - 或简单过滤:
WHERE duration_sec BETWEEN 1 AND 86400(1 秒到 1 天) - 更稳妥的做法是分段统计:用
CASE WHEN duration_sec 查看分布,再决定是否截断
别忘了确认时区一致性——所有 event_time 必须落在同一时区,否则跨夏令时或服务器本地时间混用会导致负值或巨差。










