窗口函数识别连续区间本质是用ROW_NUMBER()与有序字段作差生成锚点,使同段记录锚点相同;日期用DATE_SUB(date, INTERVAL rn DAY),整数直接seq_id-rn;需注意ORDER BY、PARTITION BY、去重、索引及业务定义。
用窗口函数识别连续日期或序号段
连续行为区间本质是把相邻的、差值固定的记录聚成一组,比如用户连续登录的日期、订单连续的
。核心思路是:对有序数据生成一个“锚点”,让同一连续段内所有行的锚点值相同。
最常用做法是用
配合分组字段做差值。例如按日期统计连续登录:
这里
就是锚点:对连续日期,
和真实日期的差是恒定的;一旦断开,差值跳变,新组就产生了。
注意点:
必须严格对应连续依据(如
或
),否则锚点错乱
若原始字段是
,先用
截断,避免时分秒干扰
PostgreSQL 用
,语法略有不同
处理非日期型连续序号(如 ID、版本号)
当连续依据是整数型字段(如
、
),逻辑一样,但差值计算更直接:
关键在
:连续整数减去连续序号,结果恒定;中间缺一个数,差值就+1,自动分组。
常见陷阱:
字段含重复值?
仍递增,但重复会导致“伪断连”——此时应改用
或先
起始值不为 1?不影响,差值偏移量一致即可
MySQL 8.0 以下不支持窗口函数,只能用变量模拟,稳定性差,慎用于生产
跨多列判断连续(如用户+日期联合连续)
实际场景常需“某用户在某设备上连续操作”,这时分组维度变多,锚点需结合多字段构造:
MySQL(Linux)
MySQL 9.6.0是面向Linux平台的2026年创新版本,核心架构迎来重大革新。其将外键约束与级联操作从InnoDB引擎层上移至SQL层,确保所有数据变更均被完整记录至Binlog,彻底解决了CDC(变更数据捕获)与主从复制中的数据不一致难题。此外,该版本引入container_aware启动选项以原生适配容器环境,并对审计日志进行了组件化重构,为追求极致数据一致性与云原生体验的开发者提供了全新选择。
下载
例如统计每个用户自己的连续登录段:
重点是
:确保每个用户的
独立计数,互不干扰。
容易忽略的细节:
字段必须和业务分组强一致,漏写或写错会导致跨用户混组
若还需按设备细分,就把
写全
MySQL 5.7 不支持
+ 窗口函数,得用自连接或存储过程硬算,性能极差
性能与边界情况提醒
连续区间统计看起来简单,但在
大数据
量下极易慢得离谱,尤其当没索引或排序字段无索引时。
必须检查:
ORDER BY 字段是否有索引?没有就加:
是否误把
条件放在子查询外?会导致全表扫描后再过滤,应尽量下推到内层
NULL 值存在吗?
会跳过 NULL 行,若字段允许 NULL,先
时间范围极大(如十年日志)?考虑按年/月分区后分别计算再合并,避免单次扫描过大
真正难的不是写出语句,而是确认“连续”的定义是否被业务方准确传达——比如“隔天登录算不算连续”“节假日是否排除”,这些逻辑必须在 SQL 之前厘清,否则窗口函数再准也没用。
order_idROW_NUMBER()SELECT
MIN(login_date) AS start_date,
MAX(login_date) AS end_date,
COUNT(*) AS days
FROM (
SELECT
login_date,
DATE_SUB(login_date, INTERVAL ROW_NUMBER() OVER (ORDER BY login_date) DAY) AS grp
FROM user_login
WHERE user_id = 123
) t
GROUP BY grp;grpROW_NUMBER()ORDER BYlogin_dateevent_timeTIMESTAMPDATE()login_date - ROW_NUMBER() OVER (...)::INTversion_noseq_idSELECT
MIN(seq_id) AS start_id,
MAX(seq_id) AS end_id,
COUNT(*) AS length
FROM (
SELECT
seq_id,
seq_id - ROW_NUMBER() OVER (ORDER BY seq_id) AS grp
FROM event_log
WHERE service = 'payment'
) t
GROUP BY grp;seq_id - ROW_NUMBER()ROW_NUMBER()DENSE_RANK()DISTINCTSELECT
user_id,
MIN(login_date) AS start_date,
MAX(login_date) AS end_date
FROM (
SELECT
user_id,
login_date,
DATE_SUB(login_date, INTERVAL ROW_NUMBER() OVER (
PARTITION BY user_id ORDER BY login_date
) DAY) AS grp
FROM user_login
) t
GROUP BY user_id, grp;PARTITION BY user_idROW_NUMBER()PARTITION BYPARTITION BY user_id, device_idPARTITION BYCREATE INDEX idx_login_user_date ON user_login(user_id, login_date)WHEREROW_NUMBER()WHERE col IS NOT NULL