正确条件是A ≤ D且B ≥ C;漏一者将漏数据或引入误匹配,须加NULL校验、禁用BETWEEN、建(start_time,end_time)联合索引。

重叠区间JOIN的正确条件必须同时写两个不等式
只写 a.start_time 或只写 <code>a.end_time >= b.start_time 都会漏数据。数学上,两个区间 [A, B] 和 [C, D] 有交集,当且仅当 A ≤ D 且 B ≥ C。漏掉任一,要么漏掉被完全包含的区间(如 [10,20] 被 [5,25] 包住),要么引入端点相接但实际无重叠的记录(取决于业务是否把“相接”算作重叠)。
常见错误现象包括:返回行数爆炸、同一主表记录反复出现不同副表记录、查询变慢甚至超时。如果业务明确要求端点相接不算重叠(例如 [2025-01-01, 2025-01-10] 和 [2025-01-10, 2025-01-20] 不算重叠),就把 改成 <code>、<code>>= 改成 >。
- 务必加
AND a.start_time IS NOT NULL AND a.end_time IS NOT NULL,NULL 会让整个条件短路为 UNKNOWN,导致匹配失败 - 避免用
BETWEEN:它在边界相接时行为不稳定,且多数优化器无法将其识别为可索引的范围谓词 - PostgreSQL 的
OVERLAPS操作符不能跨库使用,MySQL/SQL Server 不支持;实测在百万级数据上比手写双不等式慢 8 倍以上
联合索引顺序必须是 (start_time, end_time)
单列索引(比如只在 start_time 上建索引)对重叠判断几乎没用。数据库无法靠一个有序序列高效判断“任意起点 ≤ 右端 + 任意终点 ≥ 左端”。只有复合索引 (start_time, end_time) 才能让优化器在扫描 start_time 范围时,顺便利用 end_time 做剪枝。
如果把顺序写反成 (end_time, start_time),范围扫描效率会暴跌——因为前导列 end_time 无法支撑 a.start_time 这类条件的快速定位。
- 建索引语句示例:
CREATE INDEX idx_period ON events (start_time, end_time) - 字段类型必须严格一致:若
start_time是TIMESTAMP WITH TIME ZONE,end_time就不能是DATE,否则隐式转换会让索引失效 - 别在 JOIN 条件里做计算,例如
DATE(a.start_time) = DATE(b.start_time)会彻底禁用索引
LEFT JOIN 查“无重叠记录”时不能依赖 WHERE b.id IS NULL
很多人写 SELECT a.* FROM orders a LEFT JOIN events b ON a.start = b.start WHERE b.id IS NULL 想找“没被任何 event 覆盖的 order”,但这是错的。当 events 表为空时,b.id IS NULL 对所有 a 行都成立,结果返回全部 orders——显然不符合业务预期。
根本原因是:LEFT JOIN 的 ON 只控制匹配逻辑,不改变左表行为;而 WHERE b.id IS NULL 在语义上只是筛选“右表未匹配到的行”,并不等价于“不存在任何重叠”。
- 正确写法是用
NOT EXISTS:WHERE NOT EXISTS (SELECT 1 FROM events b WHERE a.start = b.start) - 如果一定要用 JOIN,可在子查询中先聚合右表:例如
LEFT JOIN (SELECT order_id, COUNT(*) AS overlap_cnt FROM ... GROUP BY order_id) b USING (order_id),再查b.overlap_cnt = 0 - 注意:
NOT EXISTS在大多数数据库中能更好利用索引,且语义更贴近“不存在”
跨时区时间字段必须先转 UTC 再 JOIN
JOIN 不处理时区转换。如果你直接拿存纽约本地时间的 order_time 和存东京本地时间的 event_time 做重叠判断,数值上永远不等,结果必然为空或错乱。
正确做法是在 JOIN 前统一转为 UTC。不要在 ON 子句里实时转换(如 CONVERT_TZ(t1.time, 'US/Eastern', '+00:00') = CONVERT_TZ(t2.time, 'Asia/Tokyo', '+00:00')),这会导致无法走索引、每次 JOIN 都重复计算。
- 推荐用 CTE 预处理:
WITH t1_utc AS (SELECT ..., event_time AT TIME ZONE 'US/Eastern' AT TIME ZONE 'UTC' AS utc_time FROM t1) - 确保
utc_time字段上有索引,而不是只给原始字段建索引 - 注意夏令时:'US/Eastern' 在每年 3 月和 11 月跳变,数据库处理方式不同(PostgreSQL 依赖 IANA,MySQL 依赖系统时区表),建议生产环境用固定偏移(如 '+05:00')或显式校验 DST 边界
真正难的不是写出两个不等式,而是让它们在百万行数据上稳定走索引——这取决于字段是否非空、类型是否一致、索引是否按 (start, end) 顺序建立,以及有没有在 JOIN 前把时区搅成一锅粥。

















