Skip to content

范围操作符 ​

范围类型的价值一半在存储,另一半在操作符。&& 能判断重叠、@> 能判断包含、-|- 能判断相邻,这三个操作符几乎覆盖了所有区间类业务需求。本节系统梳理全部范围操作符,并说明哪些能走 GiST 索引。

前置知识

阅读本节前,建议先了解:日期时间范围(TSRANGE / TSTZRANGE / DATERANGE)

基础概念 ​

操作符总表 ​

操作符含义示例结果
=相等'[1,3)'::int4range = '[1,3)'::int4rangetrue
<>不等'[1,3)'::int4range <> '[2,5)'::int4rangetrue
<严格小于(下界比较)'[1,3)'::int4range < '[2,5)'::int4rangetrue
<=小于等于'[1,3)'::int4range <= '[1,3)'::int4rangetrue
>严格大于'[2,5)'::int4range > '[1,3)'::int4rangetrue
>=大于等于'[2,5)'::int4range >= '[2,5)'::int4rangetrue
@>包含元素或范围'[1,5)'::int4range @> 3true
<@被包含3 <@ '[1,5)'::int4rangetrue
&&重叠(有交集)'[1,5)'::int4range && '[4,8)'::int4rangetrue
<<严格位于左侧'[1,3)'::int4range << '[4,8)'::int4rangetrue
>>严格位于右侧'[4,8)'::int4range >> '[1,3)'::int4rangetrue
&<不延伸到右侧之外'[1,3)'::int4range &< '[3,8)'::int4rangetrue
&>不延伸到左侧之外'[3,8)'::int4range &> '[1,3)'::int4rangetrue
`--`相邻(刚好接触)`'[1,3)'::int4range -
+并集(可能失败)'[1,3)'::int4range + '[2,8)'::int4range[1,8)
*交集'[1,5)'::int4range * '[4,8)'::int4range[4,5)
-差集'[1,8)'::int4range - '[4,5)'::int4range[1,4)

三类操作符的定位 ​

比较类(= < <= > >= <>):用于 ORDER BY、DISTINCT、唯一约束。比较是按"下界优先、上界其次"进行的。

位置关系类(@> <@ && << >> &< &> -|-):用于查询与约束,是范围类型的核心价值。这类操作符在符号形式上模仿了集合论:@> 读作 "contains",&& 读作 "overlaps"。

集合运算类(+ * -):把范围当作点集做并、交、差。

text
集合论视角(把范围看作点的集合):

  [1,5) @> 3        3 ∈ [1,5)              元素属于集合
  A @> B            B ⊆ A                  B 是 A 的子集
  A && B            A ∩ B ≠ ∅              两个集合有交集
  A + B             A ∪ B                  并集(要求结果仍是单区间)
  A * B             A ∩ B                  交集(结果打包成范围)
  A - B             A \ B                  差集

&& 是所有区间业务的基础 ​

sql
-- 判断两个时段是否冲突(最常见的一行 SQL)
SELECT '[2026-03-01 09:00, 2026-03-01 11:00)'::tsrange
    && '[2026-03-01 10:00, 2026-03-01 12:00)'::tsrange AS conflict;   -- true

用两个独立字段(start_at / end_at)写同样的逻辑需要四个比较条件:

sql
-- 等价写法(无范围类型时)
SELECT NOT (end_at <= '2026-03-01 10:00' OR start_at >= '2026-03-01 12:00') AS conflict;

一个 && 顶四个比较,且不会因为漏掉某个边界条件而写错。

术语提示

包含(Containment)、重叠(Overlap)、相邻(Adjacent)、并集(Union)、交集(Intersection)、差集(Difference)、严格位于左侧(Strictly Left Of)。本节全部操作符都围绕这几组关系展开。

语法与用法 ​

操作符骨架 ​

text
-- 元素判定
range_or_value @> range_or_value
range_or_value <@ range_or_value

-- 集合关系
range && range                     -- 重叠
range << range / range >> range    -- 严格左右
range &< range / range &> range    -- 不越界
range -|- range                    -- 相邻

-- 集合运算
range + range     -- 并集,若结果不连续则报错
range * range     -- 交集,返回 range(可能为 empty)
range - range     -- 差集,返回 range(可能为 empty)

-- 比较
range = range / range < range / range <= range / ...

示例表 ​

sql
DROP TABLE IF EXISTS events;
CREATE TABLE events (
  id       BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  name     TEXT      NOT NULL,
  hall     TEXT      NOT NULL,
  span     TSTZRANGE NOT NULL,          -- 占用时段
  capacity INT       NOT NULL
);

CREATE INDEX idx_events_span ON events USING GIST (span);

INSERT INTO events (name, hall, span, capacity) VALUES
  ('晨会',     'A厅', '[2026-04-01 09:00+08, 2026-04-01 10:00+08)', 30),
  ('产品评审', 'A厅', '[2026-04-01 10:00+08, 2026-04-01 12:00+08)', 20),
  ('技术分享', 'B厅', '[2026-04-01 10:30+08, 2026-04-01 12:30+08)', 80),
  ('客户拜访', 'B厅', '[2026-04-01 14:00+08, 2026-04-01 15:30+08)', 10),
  ('全员大会', 'C厅', '[2026-04-01 13:00+08, 2026-04-01 16:00+08)', 200);
-- INSERT 0 5

详细说明 ​

包含:@> 与 <@ ​

sql
-- 1. 时间点是否落在时段内
SELECT count(*) FROM events
WHERE span @> '2026-04-01 10:00+08'::timestamptz;   -- 2(产品评审的上界 12:00 不含,晨会的 10:00 含)
text
 count
-------
     2
sql
-- 2. 范围是否包含另一个范围
SELECT '[2026-04-01 09:00,2026-04-01 13:00)'::tstzrange
     @> '[2026-04-01 10:00,2026-04-01 12:00)'::tstzrange AS contain;   -- true

-- 3. 边界相等时:包含要求被包含者的两端都在内部
SELECT '[2026-04-01 09:00,2026-04-01 13:00)'::tstzrange
     @> '[2026-04-01 09:00,2026-04-01 13:00)'::tstzrange AS self_contain;  -- true(自身包含自身)

-- 4. 空范围永远不被包含(除了空范围自身)
SELECT '[2026-04-01 09:00,2026-04-01 13:00)'::tstzrange
     @> 'empty'::tstzrange AS contain_empty;   -- false

被包含 <@ 只是 @> 的参数换序:

sql
SELECT '[2026-04-01 10:00,2026-04-01 11:00)'::tstzrange
     <@ '[2026-04-01 09:00,2026-04-01 13:00)'::tstzrange AS contained;   -- true

常见错误

'[1,5)'::int4range @> '[5,9)'::int4range 返回 false,因为 [1,5) 的上界 5 不含,而 [5,9) 的下界 5 含,5 这个点不在 [1,5) 里。判断"是否只是搭上边界"要用 &&(这里也返回 false,因为不重叠)或 -|-(返回 true)。

重叠:&& ​

sql
-- 找出与指定时段冲突的活动
SELECT name, hall, span
FROM events
WHERE span && '[2026-04-01 10:00+08, 2026-04-01 11:00+08)'::tstzrange
ORDER BY lower(span);
text
    name    | hall |                    span
------------+------+-----------------------------------------------
 产品评审  | A厅  | ["2026-04-01 10:00:00+08","2026-04-01 12:00:00+08")
 技术分享  | B厅  | ["2026-04-01 10:30:00+08","2026-04-01 12:30:00+08")

晨会([09:00,10:00))没有出现在结果里——它与查询区间只在 10:00 这一点相接,不共享任何瞬间,&& 返回 false。这正是 [) 写法的价值:晨会 10:00 结束、产品评审 10:00 开始,两者相邻而不冲突,同一会议室可以无缝衔接使用。

如果把查询区间改成 [09:30, 10:30),晨会 就会命中:

sql
SELECT name FROM events
WHERE span && '[2026-04-01 09:30+08, 2026-04-01 10:30+08)'::tstzrange
ORDER BY lower(span);
text
    name
------------
 晨会
 产品评审

技术分享 从 10:30 开始,与查询区间上界 10:30 相接,同样不算重叠。

相邻:-|- ​

sql
SELECT '[2026-04-01 09:00,2026-04-01 10:00)'::tstzrange
    -|- '[2026-04-01 10:00,2026-04-01 12:00)'::tstzrange AS adjacent;  -- true

-|- 与 NOT (a && b) 完全不同:

表达式含义两个相距 10 年的区间
`a -- b`仅仅"刚好接触"
NOT (a && b)只要不重叠就为真true
a << b OR b << a一个完全在另一个左边/右边true

-|- 用于"连续时段检查":校验排班表是否每班无缝衔接、没有空档也没有重叠。

严格左右:<< >> 与 &< &> ​

sql
SELECT '[1,3)'::int4range << '[4,8)'::int4range AS strictly_left,   -- true(1,3 整体在 4 之前)
       '[1,3)'::int4range << '[3,8)'::int4range AS touching_left,   -- false(3 不严格小于 3)
       '[1,3)'::int4range &< '[3,8)'::int4range AS not_right_of;    -- true(未延伸到 3 右侧)

四者的精确定义:

操作符含义边界相等时
<<左操作数的上界 严格小于右操作数的下界false
>>左操作数的下界 严格大于右操作数的上界false
&<左操作数不向右越过右操作数的左端true
&>左操作数不向左越过右操作数的右端true

记忆技巧:<< / >> 是严格的,&< / &> 的 & 表示"允许接触"。

集合运算:+ * - ​

sql
-- 并集:只有结果仍是单个连续区间时才成功
SELECT '[1,3)'::int4range + '[3,8)'::int4range AS u1,     -- [1,8)  相邻可合并
       '[1,3)'::int4range + '[5,8)'::int4range AS u2;     -- ERROR: result of range union would not be contiguous
text
ERROR:  result of range union would not be contiguous
sql
-- 交集:总是成功,无交集时返回 empty
SELECT '[1,5)'::int4range * '[4,8)'::int4range AS i1,     -- [4,5)
       '[1,2)'::int4range * '[4,8)'::int4range AS i2;     -- empty
text
   i1    |  i2
---------+-------
  [4,5)  | empty
sql
-- 差集:从左边移除右边的部分,返回仍是单区间
SELECT '[1,8)'::int4range - '[4,5)'::int4range AS d1,     -- [1,4)  只保留左侧残留
       '[1,8)'::int4range - '[1,2)'::int4range AS d2,     -- [2,8)  只保留右侧残留
       '[1,8)'::int4range - '[3,5)'::int4range AS d3;     -- ERROR: result of range difference would not be contiguous
text
ERROR:  result of range difference would not be contiguous

差集把 [1,8) 从中间挖掉一块后会变成两段 [1,3) 和 [5,8),无法用单区间表示,因此报错。需要保留多段结果时必须用多范围类型:

sql
-- PostgreSQL 14+:多范围支持不连续结果
SELECT multirange(int4range(1,8)) - multirange(int4range(3,5)) AS dm;
-- {[1,3),[5,8)}

在 WHERE 中组合使用 ​

sql
-- 场景一:某会议室在某段时间空着(没有任何活动与之重叠)
SELECT h.hall
FROM (VALUES ('A厅'), ('B厅'), ('C厅')) AS h(hall)
WHERE NOT EXISTS (
  SELECT 1 FROM events e
  WHERE e.hall = h.hall
    AND e.span && '[2026-04-01 15:00+08, 2026-04-01 16:00+08)'::tstzrange
);
text
 hall
------
 A厅
sql
-- 场景二:找出每个会议室内部的冲突对(自查数据质量)
SELECT a.hall, a.name AS event_a, b.name AS event_b
FROM events a
JOIN events b ON a.hall = b.hall AND a.id < b.id
WHERE a.span && b.span;
text
 hall | event_a  | event_b
------+----------+----------
 (0 rows)
sql
-- 场景三:找到跨越午休时段的安排
SELECT name, span
FROM events
WHERE span && '[2026-04-01 12:00+08, 2026-04-01 13:00+08)'::tstzrange;
text
    name    |                    span
------------+-----------------------------------------------
 技术分享  | ["2026-04-01 10:30:00+08","2026-04-01 12:30:00+08")

只有 技术分享 命中。产品评审 的上界 12:00 与午休下界 12:00 相接,晨会 类的边界情形同理——这意味着只用 && 无法查出"刚好在午休前一秒结束"的安排。若业务上把 12:00 整点结束也算"占用到午休开始",应改为闭区间查询:

sql
-- 用 [] 把 12:00 这个瞬间也纳入左侧
SELECT name FROM events
WHERE span && '[2026-04-01 12:00+08, 2026-04-01 13:00+08]'::tstzrange;
text
    name
------------
 产品评审
 技术分享

操作符与索引 ​

操作符GiSTSP-GiSTB-tree
= <> < <= > >=✔✔✔(范围有原生 B-tree 操作符类)
@> <@ && << >> &< &> `--`✔✔
+ * -✘✘✘
sql
CREATE INDEX idx_events_span_gist ON events USING GIST (span);
-- 验证:&& 查询走 Bitmap Index Scan
EXPLAIN (ANALYZE, COSTS OFF, SUMMARY OFF)
SELECT * FROM events WHERE span && '[2026-04-01 10:00+08, 2026-04-01 11:00+08)'::tstzrange;
text
                                  QUERY PLAN
------------------------------------------------------------------------------
 Bitmap Heap Scan on events
   Recheck Cond: (span && '["2026-04-01 10:00:00+08","2026-04-01 11:00:00+08")'::tstzrange)
   ->  Bitmap Index Scan on idx_events_span_gist
         Index Cond: (span && '["2026-04-01 10:00:00+08","2026-04-01 11:00:00+08")'::tstzrange)

操作符与排除约束 ​

排除约束的第二个操作符直接就是范围操作符:

sql
CREATE EXTENSION IF NOT EXISTS btree_gist;

CREATE TABLE hall_reservations (
  id    BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  hall  TEXT      NOT NULL,
  span  TSTZRANGE NOT NULL,
  CONSTRAINT no_hall_overlap
    EXCLUDE USING GIST (hall WITH =, span WITH &&)     -- 同厅堂 && 重叠 → 冲突
);

常用的排除约束搭配:

业务要求排除约束写法
同一资源时段不重叠EXCLUDE USING GIST (resource_id WITH =, span WITH &&)
数值区间不重叠EXCLUDE USING GIST (band WITH &&)
同资源同类型不重复EXCLUDE USING GIST (resource_id WITH =, kind WITH =)

版本提示

范围操作符(含 -|-)自 PostgreSQL 9.2 引入范围类型时即已提供。而多范围类型(Multirange)及其操作符(例如能返回不连续结果的差集)是 PostgreSQL 14 新增的,PostgreSQL 13 及更早版本无法表示由"多段不连续区间"组成的范围。

实战示例 ​

用 events 表演示一套完整的会场冲突排查流程。

第 1 步:准备数据 ​

sql
CREATE EXTENSION IF NOT EXISTS btree_gist;

DROP TABLE IF EXISTS events;
CREATE TABLE events (
  id       BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  name     TEXT      NOT NULL,
  hall     TEXT      NOT NULL,
  span     TSTZRANGE NOT NULL,
  capacity INT       NOT NULL,
  CONSTRAINT chk_span_not_empty CHECK (NOT isempty(span)),
  CONSTRAINT no_hall_overlap
    EXCLUDE USING GIST (hall WITH =, span WITH &&)
);
CREATE INDEX idx_events_span ON events USING GIST (span);

第 2 步:插入合法数据(相邻不冲突) ​

sql
INSERT INTO events (name, hall, span, capacity) VALUES
  ('晨会',     'A厅', '[2026-04-01 09:00+08, 2026-04-01 10:00+08)', 30),
  ('产品评审', 'A厅', '[2026-04-01 10:00+08, 2026-04-01 12:00+08)', 20),
  ('技术分享', 'B厅', '[2026-04-01 10:30+08, 2026-04-01 12:30+08)', 80),
  ('客户拜访', 'B厅', '[2026-04-01 14:00+08, 2026-04-01 15:30+08)', 10),
  ('全员大会', 'C厅', '[2026-04-01 13:00+08, 2026-04-01 16:00+08)', 200);
-- INSERT 0 5

第 3 步:找出某时刻正在进行的活动 ​

sql
SELECT name, hall
FROM events
WHERE span @> '2026-04-01 10:30+08'::timestamptz
ORDER BY hall;
text
    name    | hall
------------+------
 产品评审  | A厅
 技术分享  | B厅

第 4 步:查询重叠关系并计算交叠时长 ​

sql
SELECT a.name AS event_a,
       b.name AS event_b,
       a.hall,
       upper(a.span * b.span) - lower(a.span * b.span) AS overlap_duration
FROM events a
JOIN events b ON a.hall = b.hall AND a.id < b.id
WHERE a.span && b.span
ORDER BY a.hall, a.name;
text
 event_a | event_b | hall | overlap_duration
---------+---------+------+------------------
 (0 rows)

同厅堂内没有重叠(被排除约束保证),因此跨厅堂查询:

sql
SELECT a.name AS event_a, b.name AS event_b,
       a.hall AS hall_a, b.hall AS hall_b,
       upper(a.span * b.span) - lower(a.span * b.span) AS overlap_duration
FROM events a
JOIN events b ON a.id < b.id
WHERE a.hall <> b.hall AND a.span && b.span
ORDER BY overlap_duration DESC;
text
  event_a  |  event_b  | hall_a | hall_b | overlap_duration
-----------+-----------+--------+--------+------------------
 产品评审  | 技术分享  | A厅    | B厅    | 01:30:00
 全员大会  | 客户拜访  | C厅    | B厅    | 01:30:00

第 5 步:检查某厅堂的时段连续性 ​

sql
-- 按时间排序,检查相邻两场之间是否刚好衔接(无空档)
SELECT name,
       span,
       lead(span) OVER (ORDER BY lower(span)) AS next_span,
       span -|- lead(span) OVER (ORDER BY lower(span)) AS is_adjacent
FROM events
WHERE hall = 'A厅'
ORDER BY lower(span);
text
    name    |                       span                        |                     next_span                     | is_adjacent
------------+---------------------------------------------------+---------------------------------------------------+-------------
 晨会      | ["2026-04-01 09:00:00+08","2026-04-01 10:00:00+08") | ["2026-04-01 10:00:00+08","2026-04-01 12:00:00+08") | t
 产品评审  | ["2026-04-01 10:00:00+08","2026-04-01 12:00:00+08") | (null)                                              | (null)

用窗口函数把相邻两行直接拿出来比对。若某行 is_adjacent 为 f,说明两场之间有空档;若为 t,说明排期无缝衔接。

更直接的写法是用连接 + 显式相邻判断:

sql
SELECT a.name AS prev_event, b.name AS next_event,
       a.span -|- b.span AS adjacent
FROM events a
JOIN events b ON a.hall = b.hall AND upper(a.span) = lower(b.span)
WHERE a.hall = 'A厅';
text
 prev_event | next_event | adjacent
------------+------------+----------
 晨会       | 产品评审   | t

第 6 步:用并集合并可衔接的排期 ​

sql
-- 把 A厅 上午的连续安排合并成一个完整占用段
SELECT range_merge(span) AS merged
FROM events
WHERE hall = 'A厅' AND span << '[2026-04-01 12:00+08,)'::tstzrange;
text
                       merged
-----------------------------------------------------
 ["2026-04-01 09:00:00+08","2026-04-01 12:00:00+08")

多行合并时用 range_agg(PostgreSQL 14+):

sql
SELECT hall, range_agg(span) AS all_spans
FROM events
GROUP BY hall
ORDER BY hall;
text
 hall |                            all_spans
------+-------------------------------------------------------------------
 A厅  | {["2026-04-01 09:00:00+08","2026-04-01 12:00:00+08")}
 B厅  | {["2026-04-01 10:30:00+08","2026-04-01 12:30:00+08"),["2026-04-01 14:00:00+08","2026-04-01 15:30:00+08")}
 C厅  | {["2026-04-01 13:00:00+08","2026-04-01 16:00:00+08")}

第 7 步:验证排除约束拦截冲突 ​

sql
INSERT INTO events (name, hall, span, capacity) VALUES
  ('临时加塞', 'A厅', '[2026-04-01 10:30+08, 2026-04-01 11:00+08)', 5);
text
ERROR:  conflicting key value violates exclusion constraint "no_hall_overlap"
DETAIL:  Key (hall, span)=(A厅, ["2026-04-01 10:30:00+08","2026-04-01 11:00:00+08")) conflicts with existing key (hall, span)=(A厅, ["2026-04-01 10:00:00+08","2026-04-01 12:00:00+08")).

第 8 步:清理 ​

sql
DROP TABLE IF EXISTS events;

注意事项 ​

  1. @> 与 <@ 方向极易记反。 记忆法:张开嘴的那个(@>)"吃"下右边的东西。A @> B 表示 A 包含 B。
  2. && 与 -|- 语义完全不同。 && 是"有共同点",-|- 是"只共享边界点"。判定冲突用 &&,判定衔接用 -|-。
  3. + 与 - 可能报错。 结果不再是单区间时返回 ERROR: result of range union/difference would not be contiguous,需要多范围或应用层处理。
  4. 空范围参与运算要小心。 'empty'::int4range @> 'empty'::int4range 为真,但 '[1,5)' @> 'empty' 为假。
  5. * 无交集时返回 empty 而不是 NULL。 判断"有没有交集"用 && 比 (a * b) IS NULL 更直接(后者永远不成立)。
  6. 比较操作符(< >)不等于"位置在前在后"。 '[1,3)' < '[3,8)' 与 '[1,3)' << '[3,8)' 可能给出不同结果,前者比下界,后者比整体位置。
  7. B-tree 索引不能加速 &&。 范围列上的范围查询必须用 GiST 或 SP-GiST。
  8. GiST 索引查询需要 Recheck。 索引给出的是"可能匹配"的候选集,精确判断由 Recheck Cond 完成,这属于 GiST 的正常工作方式。
  9. 排除约束必须用 GiST,不能用 SP-GiST 或 B-tree。 且配合非范围列(如 TEXT)时需要 btree_gist 扩展。
  10. GiST 索引下 && 的实际耗时随匹配行数增长。 若某时段横跨大量记录,索引帮助有限,考虑按时间分区。
  11. 多范围的操作符与范围不完全对称。 多范围之间可用 @> <@ && + * -,但多范围与单个范围的混用需显式转换。
  12. = 比较包含边界开闭属性。 '[1,5)'::int4range = '(0,6)'::int4range 吗?前者规范化为 [1,5),后者仍需规范化,规范化后相等才为真。

最佳实践 ​

  1. 冲突检测统一用 &&。 不要写四个比较条件,出错概率高且无法走索引。
  2. 时段语义统一 [)。 全库一致的开闭约定能消除"边界时刻算谁"的争论。
  3. "相邻且无缝"用 -|-,不要用 NOT (a && b)。 后者会把无关区间也算进来。
  4. 需要防重叠就用排除约束,不要只靠应用层。 并发场景下应用层的"先查再插"必然有竞态。
  5. 范围列一律建 GiST 索引。 即使有排除约束(会自动建索引),普通查询的索引也可能需要独立创建以满足多列查询。
  6. * 求交后立刻解析出时长。 upper(a*b) - lower(a*b) 直接给出重叠时长,比在应用层算更方便。
  7. 差集期望多段结果时改用多范围。 PostgreSQL 14+ 用 multirange() 包装,避免 - 报错。
  8. ORDER BY lower(span) 而非 ORDER BY span。 按业务起始时间排序更符合直觉。
  9. 大表按时间范围分区。 span 上建 GiST 索引在数据量上千万后收益递减,结合 PARTITION BY RANGE 按年/月切分效果更好。
  10. 用 EXPLAIN (ANALYZE, BUFFERS) 验证操作符是否走索引。 写好的 && 查询若退化成 Seq Scan,通常是索引没建或统计信息过期。

下一节

继续学习:范围函数

参考链接 ​