用row_number()与日期差构造连续段:对达标记录按group_id分组、check_date排序,计算date_sub(check_date, interval row_number() day)作为grp_key,相同grp_key即为连续日期段,再按group_id和grp_key分组统计天数并筛选≥3的group_id。

用 ROW_NUMBER() 和日期差构造连续段
直接套用 MIN()、COUNT() 等聚合函数无法识别“连续”,必须先把连续日期打上同一组号。核心思路是:对每个分组内按日期排序,用日期本身减去 ROW_NUMBER() 的序号——只要日期连续,这个差值就恒定。
假设表 t_daily 有字段 group_id、check_date(DATE 类型)、is_pass(1 表示达标),先筛出达标记录:
SELECT group_id, check_date FROM t_daily WHERE is_pass = 1
再加一列计算“伪组键”:
SELECT group_id, check_date, DATE_SUB(check_date, INTERVAL ROW_NUMBER() OVER (PARTITION BY group_id ORDER BY check_date) DAY) AS grp_key FROM t_daily WHERE is_pass = 1
这个 grp_key 就是连续段的唯一标识。比如 2024-01-01、02、03 对应的 ROW_NUMBER 是 1/2/3,减出来都是 2023-12-31。
按 grp_key 分组计数并过滤长度 ≥3
上一步结果再套一层 GROUP BY group_id, grp_key,用 COUNT(*) 算每段连续天数。注意必须同时按 group_id 和 grp_key 分组,否则不同分组的连续段会混在一起。
- MySQL 8.0+ / PostgreSQL / SQL Server 都支持窗口函数,语法一致
- SQLite 需用
ROWID或子查询模拟序号,但日期必须无重复且已排序 - 如果
check_date是DATETIME类型,先用DATE(check_date)归一化,否则同一天多次记录会导致ROW_NUMBER错乱
完整查询:
WITH ranked AS (
SELECT
group_id,
check_date,
DATE_SUB(check_date, INTERVAL ROW_NUMBER() OVER (PARTITION BY group_id ORDER BY check_date) DAY) AS grp_key
FROM t_daily
WHERE is_pass = 1
),
streaks AS (
SELECT group_id, grp_key, COUNT(*) AS cnt
FROM ranked
GROUP BY group_id, grp_key
)
SELECT DISTINCT group_id
FROM streaks
WHERE cnt >= 3;
遇到 NULL 或跨年日期时的处理
DATE_SUB 在 MySQL 中对 NULL 输入会返回 NULL,如果 check_date 允许为空,务必在 WHERE 子句中加上 check_date IS NOT NULL,否则这些行会全被归到同一个 grp_key = NULL 段里,造成误判。
跨年不构成问题——DATE_SUB('2024-01-01', INTERVAL 1 DAY) 正确返回 '2023-12-31',日历计算由数据库引擎保证。但要注意时区:若应用层写入的是带时区的 TIMESTAMP,而数据库配置为 SYSTEM 时区,可能因夏令时导致某天“消失”或“重复”,建议统一用 DATE 类型存业务日期。
性能关键点:索引必须覆盖 (group_id, check_date)
窗口函数 ROW_NUMBER() OVER (PARTITION BY group_id ORDER BY check_date) 的效率极度依赖联合索引。只建 group_id 单列索引没用,ORDER BY 的 check_date 必须进入索引右侧。
- 正确索引:
CREATE INDEX idx_group_date ON t_daily (group_id, check_date); - 错误索引:
CREATE INDEX idx_date_group ON t_daily (check_date, group_id);—— 这个能加速按日期查,但无法加速按 group_id 分组再按日期排序 - 如果表很大且达标率低,可先用物化视图或临时表预存
is_pass = 1的子集,避免每次全表扫描
连续达标这类问题本质是“序列模式识别”,SQL 不是它的天然主场;一旦逻辑变复杂(比如允许 1 天中断后继续计数),就得考虑用程序语言处理,别硬撑。











