直接用JOIN无法合并重叠时间段,因其仅做笛卡尔匹配,不识别时间重叠逻辑;需用窗口函数按start排序并用LAG判断连续性来分组聚合,或用自连接+NOT EXISTS作ANSI兼容方案。

为什么直接用 JOIN 无法合并重叠时间段?
因为 JOIN 是笛卡尔式匹配,它本身不识别“时间重叠”逻辑,更不会自动合并区间。你写 ON a.start = b.start 只能找出所有有重叠的行对,但结果仍是多对多的原始组合,不是合并后的连续区间。真正要的是“把所有互相重叠或相邻的区间归为一组,再取每组的最小开始、最大结束”——这需要分组聚合,而分组依据得先构造出来。
用窗口函数标记连续重叠组(PostgreSQL / SQL Server / Oracle)
核心思路是:按 start 排序后,遍历每条记录,判断它是否能接在前一个区间的后面(即当前 start )。如果不能,就开启新组。用 <code>SUM() 窗口函数累加“断点标记”来生成组 ID:
SELECT
MIN(start) AS merged_start,
MAX(end) AS merged_end
FROM (
SELECT *,
SUM(is_new_group) OVER (ORDER BY start, end) AS grp_id
FROM (
SELECT *,
CASE WHEN start > LAG(end) OVER (ORDER BY start, end)
THEN 1 ELSE 0 END AS is_new_group
FROM your_table
) t1
) t2
GROUP BY grp_id;
注意:LAG(end) 默认取上一行的 end,首行返回 NULL,所以 start > NULL 为 FALSE,首行自然归入第一组。若存在严格相邻(如 [1,5] 和 [6,10]),需把条件改为 start > LAG(end) + 1 才视为断开。
MySQL 8.0+ 同样适用,但要注意排序稳定性
MySQL 8.0 支持窗口函数,但若 start 相同,LAG() 的行为依赖 ORDER BY 子句是否足够唯一。建议补上主键或 end 二级排序,避免因执行计划变动导致组划分错乱:
- 写成
ORDER BY start, end, id而非仅ORDER BY start - 确保
start和end列有索引,否则排序开销大,尤其数据量过万时 - 若
end可能为NULL,LAG(end)返回NULL,比较时需用COALESCE(LAG(end) OVER (...), '1970-01-01')避免逻辑短路
纯 ANSI SQL 兼容方案:自连接 + NOT EXISTS 排除中间段
如果数据库不支持窗口函数(如 SQLite 或旧版 MySQL),可用自连接找“不可被其他区间吞并的左端点”,再对每个左端点找它能覆盖到的最远右端点:
SELECT t1.start AS merged_start, MAX(t2.end) AS merged_end FROM your_table t1 WHERE NOT EXISTS ( SELECT 1 FROM your_table t0 WHERE t0.start < t1.start AND t0.end >= t1.start ) AND EXISTS ( SELECT 1 FROM your_table t2 WHERE t2.start <= t1.end AND t2.end >= t1.start ) GROUP BY t1.start;
这个写法性能差(O(n²)),且在边界情况(如完全嵌套区间)下可能漏合并。只建议用于几百行以内的临时分析,上线系统务必用窗口函数方案。
真正麻烦的不是写法,而是时间字段类型和时区。用 TIMESTAMP 还是 DATE?跨日、跨年、夏令时切换点是否被当作连续?这些细节一旦错,合并结果就偏移一小时甚至一天——别只盯着 JOIN 条件,先确认你的 start 和 end 是带时区的绝对时刻,还是本地日期范围。

















