直接用 count(*) 无法得到在线人数峰值,因为它只能统计区间内登录人数,无法反映瞬时叠加关系;必须将登录和登出拆为+1/-1事件流,按时间排序后窗口累加求最大值。

为什么直接用 COUNT(*) 无法得到在线人数峰值
在线人数是动态变化的:用户登录(+1)、登出(-1),同一时刻的在线数等于所有未登出用户的数量。如果只按 login_time 和 logout_time 分别统计进出,COUNT(*) 会丢失时间轴上的叠加关系——它只能告诉你“有多少人在这个区间内登录过”,而不是“某一秒最多有几人在场”。
核心矛盾在于:峰值发生在某个瞬时点,而原始数据是成对的区间。必须把每个登录事件转为 +1、登出事件转为 -1,并在时间轴上排序累加,才能还原真实人数曲线。
用 UNION ALL 拆解为带符号的时间事件流
关键不是“分组聚合”,而是“构造事件序列”。要把每条用户记录拆成两行:login_time 标记为 +1,logout_time 标记为 -1。注意:登出时间是否包含在在线区间内需统一约定(通常取左闭右开,即登出时刻已不在线,所以 -1 应放在 logout_time 点上,而非之后)。
- 必须给两个子查询都加上
ORDER BY event_time吗?不需要——UNION ALL不保证顺序,但后续ORDER BY可以统一排;不过各子查询里不能有ORDER BY(除非配LIMIT),否则报错 - 若存在精确到秒的并发登录/登出,需定义次级排序:比如同时间点先处理 +1 再 -1,避免峰值被低估;可用
ORDER BY event_time, type DESC(设type = 1表示登录,type = -1表示登出) - 示例片段:
SELECT login_time AS event_time, 1 AS delta FROM user_sessions WHERE login_time >= '2024-06-01' AND logout_time = '2024-06-01' AND logout_time <h3>用窗口函数 SUM() OVER(ORDER BY ...) 实时累计在线人数</h3> <p>有了有序事件流,就可用 <code>SUM(delta) OVER (ORDER BY event_time, type DESC)</code> 做前缀和。这个累计值就是“截至该事件发生后”的瞬时在线人数。注意:它不是“该时刻正在线的人数”,而是“该事件处理完后的结果”——因此只要排序逻辑正确(登录优先于同秒登出),累计值就能准确反映每个关键时间点的真实人数。</p>
- 不要用
PARTITION BY:这里需要全局时间轴,分区会重置累计值 - MySQL 8.0+、PostgreSQL、SQL Server 2012+、Oracle 10g+ 都支持该写法;SQLite 3.25+ 也支持,但旧版不支持窗口函数
- 性能敏感时,确保
event_time字段有索引;若原始表很大,先用 WHERE 过滤时间段再 UNION,比全表 UNION 后 WHERE 更快
取 MAX() 即得该时间段内的峰值人数
累计列生成后,只需对其取最大值。但注意:这个最大值对应的是某个事件点之后的状态,它就是该时间段内出现过的最高在线人数。无需插值或补点——因为峰值必然出现在事件发生的时刻(登录使人数突增,登出使人数突降,极值只可能在跳变点)。
- 若需同时返回峰值发生时间,可套一层
ROW_NUMBER() OVER (ORDER BY running_total DESC, event_time ASC)取第一条 - 警惕 NULL:若某用户只有登录无登出(异常数据),其
logout_time为 NULL,则第二支查询会丢掉该 -1 事件,导致累计值虚高;建议提前清洗或用COALESCE(logout_time, NOW())设定兜底下线时间 - 边界情况:若时间段内无人登录,累计值从 0 开始,
MAX()返回 0,符合预期
真正容易被忽略的,是事件时间精度与业务语义的对齐——比如日志记录到分钟,但你按秒聚合,峰值就不可信;又或者多个系统时钟不同步,login_time 和 logout_time 来自不同服务器,误差超过 1 秒,累加结果就会漂移。










