窗口函数不能直接合并时间段,需用LAG()识别新区间起点并累计求和生成分组ID,再通过GROUP BY聚合;须注意NULL处理、排序唯一性、精度对齐及业务定义的重叠规则。

纯窗口函数无法直接“合并”时间段,因为合并会改变行数——但可以用它标记每段的归属组,再靠 GROUP BY 实现合并。关键不是让窗口函数干完所有事,而是让它帮你把“哪些行该归为一组”这件事算清楚。
用 LAG() + 累计求和生成分组 ID
核心是识别“新区间起点”:当前 start_date ≥ 上一条记录的 end_date(即不重叠、也不紧邻),就该开新组。其他情况都延续上一组。
-
LAG(end_date) OVER (PARTITION BY account_id ORDER BY start_date)拿到前一行的结束时间 - 用
CASE WHEN start_date > LAG(end_date) THEN 1 ELSE 0 END标记是否为起点 - 再套一层
SUM(...) OVER (PARTITION BY account_id ORDER BY start_date)做累计,得到稳定不变的grp_id
注意:MySQL 8.0+、PostgreSQL、SQL Server 都支持;旧版 MySQL 不支持窗口函数嵌套,得换变量模拟,容易出错。
MAX(end_date) OVER () 累计右边界必须配 ROWS UNBOUNDED PRECEDING
贪心合并依赖“按 start_date 排序后,维护当前能覆盖到的最远 end_date”。如果写成 RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,在 start_date 相同时会误聚合——比如两条 2023-01-01 开始的区间,RANGE 会把它们一起算进当前行的累计值,导致提前合并。
- 务必用
ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING计算前序最大end_date - 或用
MAX(end_date) OVER (ORDER BY start_date ROWS UNBOUNDED PRECEDING)累计,但要确保start_date唯一,否则加id或ROW_NUMBER()辅助排序
合并后取 MIN(start_date) 和 MAX(end_date) 要防 NULL 和跨年
真实数据里 start_date 或 end_date 可能为 NULL,直接 MIN()/MAX() 会导致整组被过滤(取决于 NULLS FIRST/LAST 设置);跨年区间若用 DATE 类型相减,可能因时区或精度丢天数。
- 先用
WHERE start_date IS NOT NULL AND end_date IS NOT NULL过滤掉无效行 - 合并前对齐精度:如用
DATE(start_date)或FLOOR(UNIX_TIMESTAMP(start_date)/86400)归到天级,避免毫秒差异拆断连续段 - 若需保留原始精度,聚合后用
LEAST()/GREATEST()替代MIN()/MAX()更稳妥(尤其当字段含TIMESTAMP WITH TIME ZONE)
真正难的不是写对那几行窗口函数,而是确认你的“不重叠”定义是否和业务一致——比如间隔 5 分钟算不算重叠?跨午夜的区间要不要强制合并?这些逻辑一旦定错,grp_id 就全偏了,后面怎么聚合都没用。

















