lag()检测时间重叠本质是判断当前start_time是否早于上一行prev_end,需按start_time和end_time双排序确保顺序稳定;端点相接时用

用 LAG() 比较相邻行的时间边界
重叠检测本质是判断当前记录的开始时间是否早于上一条记录的结束时间(按开始时间排序后)。最直接的方式是用 LAG() 取出前一行的 end_time,再与当前行的 start_time 比较:
SELECT *,
LAG(end_time) OVER (ORDER BY start_time, end_time) AS prev_end
FROM events;
注意必须同时按 start_time 和 end_time 排序——否则当多条记录起始时间相同时,LAG() 返回的“前一行”可能随机,导致误判。如果业务允许端点相接(如 [10:00, 11:00) 和 [11:00, 12:00)),比较时用 ;若需排除相接,改用 <code>。
用 LEAD() 配合 OVERLAPS 更稳妥地覆盖所有重叠对
OVERLAPS 是 PostgreSQL 原生的时间范围运算符,能正确处理开闭区间、NULL 和各种边界情况,比手写 BETWEEN 或逻辑表达式更可靠。但 OVERLAPS 不能直接用于窗口函数,所以得配合 LEAD() 扫描后续行:
- 先用
LEAD(start_time, 1)和LEAD(end_time, 1)获取下一条记录的时间段 - 再用
(start_time, end_time) OVERLAPS (next_start, next_end)判断是否重叠 - 若需找所有重叠对(不止相邻),就得用自连接或
LATERAL,LEAD()只能查固定偏移
示例(仅查紧邻下一条):
SELECT *,
(ROW(start_time, end_time)::tsrange)
&& (ROW(LEAD(start_time) OVER w, LEAD(end_time) OVER w)::tsrange) AS overlaps_next
FROM events
WINDOW w AS (ORDER BY start_time);
用 tsrange + && 运算符避免手动边界逻辑
把时间字段转成 tsrange 类型后,直接用 &&(overlap operator)比写 start1 start2 更简洁、更少出错。尤其当字段含 NULL 或需要考虑包含/不包含端点时,tsrange 自动处理:
-
tsrange(start_time, end_time, '[)')显式声明左闭右开(推荐,符合多数业务语义) - NULL 端点会被转为无界范围,
&&仍可安全计算 - 性能上,如果加了
GIST索引(CREATE INDEX ON events USING GIST (tsrange(start_time, end_time));),后续范围查询会快很多
检测自身是否与任何其他记录重叠(全量扫描):
SELECT e1.*
FROM events e1
WHERE EXISTS (
SELECT 1 FROM events e2
WHERE e2.id != e1.id
AND tsrange(e2.start_time, e2.end_time, '[)')
&& tsrange(e1.start_time, e1.end_time, '[)')
);
容易漏掉的边界:NULL、相等时间点、时区混用
实际数据里这三类问题高频引发误报或漏报:
-
start_time或end_time为NULL时,LAG()/LEAD()返回 NULL,参与比较会整个表达式变 NULL(即不满足WHERE条件),必须显式用IS NOT NULL过滤或用COALESCE - 两个时间段完全相等(
start1 = start2 AND end1 = end2)算重叠,但有些业务要排除“完全相同”的记录,这时得额外加id != other_id - 字段存的是
TIMESTAMP WITHOUT TIME ZONE,但应用层按本地时区写入,会导致看似不重叠的时间段在数据库里错位——统一用TIMESTAMP WITH TIME ZONE并确认timezone配置
真正难的不是写出一个能跑的查询,而是想清楚你的业务里“重叠”到底指什么:端点是否可共享?NULL 怎么解释?时区怎么对齐?这些定义一旦模糊,窗口函数再准也没用。










