2025年9月25日: PostgreSQL 18 发布!
支持的版本: 当前 (18) / 17 / 16 / 15 / 14 / 13
开发版本: devel
不支持的版本: 12 / 11 / 10 / 9.6 / 9.5 / 9.4 / 9.3 / 9.2

8.17. 范围类型 #

范围类型是表示某种元素类型(称为范围的子类型)的值的范围的数据类型。例如,timestamp 的范围可能用于表示会议室预订的时间范围。在这种情况下,数据类型是 tsrange(“timestamp range”的缩写),而 timestamp 是子类型。子类型必须具有全序关系,以便明确元素值是否在某个值范围内、在此范围之前还是在此范围之后。

范围类型很有用,因为它们用单个范围值表示许多元素值,并且像“重叠范围”这样的概念可以清晰地表达。将时间和日期范围用于调度目的就是最清晰的例子;但价格范围、仪器的测量范围等也可能很有用。

每个范围类型都有一个对应的多范围类型。多范围是连续的、非空、非 NULL 范围的有序列表。大多数范围运算符也适用于多范围,并且它们有自己的一些函数。

8.17.1. 内置范围类型和多范围类型 #

PostgreSQL 提供了以下内置范围类型:

  • int4rangeinteger 的范围,int4multirange — 对应的多范围

  • int8rangebigint 的范围,int8multirange — 对应的多范围

  • numrangenumeric 的范围,nummultirange — 对应的多范围

  • tsrangetimestamp without time zone 的范围,tsmultirange — 对应的多范围

  • tstzrangetimestamp with time zone 的范围,tstzmultirange — 对应的多范围

  • daterangedate 的范围,datemultirange — 对应的多范围

此外,您还可以定义自己的范围类型;有关更多信息,请参阅 CREATE TYPE

8.17.2. 示例 #

CREATE TABLE reservation (room int, during tsrange);
INSERT INTO reservation VALUES
    (1108, '[2010-01-01 14:30, 2010-01-01 15:30)');

-- Containment
SELECT int4range(10, 20) @> 3;

-- Overlaps
SELECT numrange(11.1, 22.2) && numrange(20.0, 30.0);

-- Extract the upper bound
SELECT upper(int8range(15, 25));

-- Compute the intersection
SELECT int4range(10, 20) * int4range(15, 25);

-- Is the range empty?
SELECT isempty(numrange(1, 5));

请参阅 表 9.58表 9.60 以获取范围类型的运算符和函数的完整列表。

8.17.3. 包含和排除边界 #

每个非空范围都有两个边界:下边界和上边界。这两个值之间的所有点都包含在范围内。包含边界意味着边界点本身也包含在范围内,而排除边界意味着边界点不包含在范围内。

在范围的文本形式中,包含的下边界用“[” 表示,而排除的下边界用“(” 表示。同样,包含的上边界用“]” 表示,而排除的上边界用“)” 表示。(有关更多详细信息,请参阅 第 8.17.5 节。)

lower_incupper_inc 函数分别测试范围值下边界和上边界的包含性。

8.17.4. 无限(无界)范围 #

范围的下边界可以省略,这意味着小于上边界的所有值都包含在范围内,例如 (,3]。同样,如果范围的上边界被省略,则大于下边界的所有值都包含在范围内。如果下边界和上边界都被省略,则元素类型的所有值都将被视为在范围内。将缺失的边界指定为包含是自动转换为排除的,例如 [,] 会被转换为 (,)。您可以将这些缺失值视为 +/- 无穷大,但它们是特殊的范围类型值,被认为超出了任何范围元素类型的 +/- 无穷大值。

具有“无穷大”概念的元素类型可以使用它们作为显式边界值。例如,对于时间戳范围,[today,infinity) 排除特殊 timestampinfinity,而 [today,infinity] 包含它,就像 [today,)[today,] 一样。

lower_infupper_inf 函数分别测试范围的无限下边界和上边界。

8.17.5. 范围的输入/输出 #

范围值的输入必须遵循以下模式之一:

(lower-bound,upper-bound)
(lower-bound,upper-bound]
[lower-bound,upper-bound)
[lower-bound,upper-bound]
empty

括号或方括号表示下边界和上边界是排除的还是包含的,如前所述。请注意,最后一个模式是 empty,它表示一个空范围(不包含任何点的范围)。

lower-bound 可以是子类型有效的输入字符串,或者为空以表示没有下边界。同样,upper-bound 可以是子类型有效的输入字符串,或者为空以表示没有上边界。

每个边界值都可以使用“"”(双引号)进行引用。如果边界值包含括号、方括号、逗号、双引号或反斜杠,则必须这样做,因为否则这些字符将被视为范围语法的一部分。要在引用的边界值中放置双引号或反斜杠,请在前面加上反斜杠。(此外,双引号内的双引号对被视为一个双引号字符,这类似于 SQL 字面字符串中的单引号规则。)或者,您可以避免引用,并使用反斜杠转义来保护所有将被视为范围语法的字符。另外,要写入空字符串的边界值,请写 "",因为什么都不写意味着无限边界。

允许在范围值前后有空白字符,但括号或方括号之间的任何空白字符都将作为下边界值或上边界值的一部分。(根据元素类型,这可能具有意义,也可能没有。)

注意

这些规则与复合类型字面值中字段值的编写规则非常相似。有关更多注释,请参阅 第 8.16.6 节

示例

-- includes 3, does not include 7, and does include all points in between
SELECT '[3,7)'::int4range;

-- does not include either 3 or 7, but includes all points in between
SELECT '(3,7)'::int4range;

-- includes only the single point 4
SELECT '[4,4]'::int4range;

-- includes no points (and will be normalized to 'empty')
SELECT '[4,4)'::int4range;

多范围的输入是花括号({}),其中包含零个或多个有效范围,用逗号分隔。允许在括号和逗号周围添加空白字符。这旨在让人联想到数组语法,尽管多范围要简单得多:它们只有一维,并且不需要引用其内容。(不过,其范围的边界可以像上面那样引用。)

示例

SELECT '{}'::int4multirange;
SELECT '{[3,7)}'::int4multirange;
SELECT '{[3,7), [8,9)}'::int4multirange;

8.17.6. 构建范围和多范围 #

每个范围类型都有一个与范围类型同名的构造函数。使用构造函数通常比编写范围字面常量更方便,因为它避免了对边界值进行额外引用的需要。构造函数接受两个或三个参数。两参数形式构建标准形式的范围(下边界包含,上边界排除),而三参数形式构建具有由第三个参数指定的边界形式的范围。第三个参数必须是字符串 ()(][)[] 之一。例如:

-- The full form is: lower bound, upper bound, and text argument indicating
-- inclusivity/exclusivity of bounds.
SELECT numrange(1.0, 14.0, '(]');

-- If the third argument is omitted, '[)' is assumed.
SELECT numrange(1.0, 14.0);

-- Although '(]' is specified here, on display the value will be converted to
-- canonical form, since int8range is a discrete range type (see below).
SELECT int8range(1, 14, '(]');

-- Using NULL for either bound causes the range to be unbounded on that side.
SELECT numrange(NULL, 2.2);

每个范围类型还有一个与多范围类型同名的多范围构造函数。构造函数接受零个或多个参数,这些参数都是相应类型的范围。例如:

SELECT nummultirange();
SELECT nummultirange(numrange(1.0, 14.0));
SELECT nummultirange(numrange(1.0, 14.0), numrange(20.0, 25.0));

8.17.7. 离散范围类型 #

离散范围是指其元素类型具有明确定义的“步长”的范围,例如 integerdate。在这些类型中,两个元素可以称为相邻的,当它们之间没有有效值时。这与连续范围形成对比,在连续范围中,总(或几乎总是)可以在两个给定值之间识别其他元素值。例如,numeric 类型的范围是连续的,timestamp 的范围也是如此。(尽管 timestamp 的精度有限,因此理论上可以被视为离散的,但最好将其视为连续的,因为步长通常不被关注。)

另一种思考离散范围类型的方式是,每个元素值都有一个清晰的“下一个”或“上一个”值。知道这一点,就可以通过选择下一个或上一个元素值而不是原始给定的值,在范围边界的包含和排除表示之间进行转换。例如,在整数范围类型中,[4,8](3,9) 表示相同的数值集合;但对于 numeric 范围则不是这样。

离散范围类型应具有一个规范化函数,该函数应了解元素类型的所需步长。规范化函数负责将范围类型的等效值转换为具有相同表示形式,特别是始终包含或排除边界。如果未指定规范化函数,则格式不同的范围将始终被视为不等,即使它们在现实中可能表示相同的值集。

内置范围类型 int4rangeint8rangedaterange 都使用包含下边界并排除上边界的规范形式;即 [)。然而,用户定义的范围类型可以使用其他约定。

8.17.8. 定义新的范围类型 #

用户可以定义自己的范围类型。最常见的原因是使用内置范围类型未提供的子类型的范围。例如,要定义 float8 子类型的新范围类型:

CREATE TYPE floatrange AS RANGE (
    subtype = float8,
    subtype_diff = float8mi
);

SELECT '[1.234, 5.678]'::floatrange;

由于 float8 没有有意义的“步长”,因此在此示例中我们不定义规范化函数。

当您定义自己的范围时,会自动获得一个对应的多范围类型。

定义自己的范围类型还可以让您指定其他子类型 B 树操作符类或排序规则,以更改确定哪些值属于给定范围的排序顺序。

如果子类型被认为具有离散值而不是连续值,则 CREATE TYPE 命令应指定一个 canonical 函数。规范化函数接收输入范围值,并且必须返回一个等效的范围值,该值可能具有不同的边界和格式。两个表示相同值集的范围的规范输出,例如整数范围 [1, 7][1, 8),必须是相同的。选择哪种表示形式作为规范形式并不重要,只要两个具有不同格式的等效值始终映射到具有相同格式的相同值即可。除了调整包含/排除边界格式外,规范化函数还可以四舍五入边界值,以防所需步长大于子类型能存储的。例如,timestamp 上的范围类型可以定义为具有一小时的步长,在这种情况下,规范化函数需要将不是一小时倍数的边界四舍五入,或者可能抛出错误。

此外,任何打算与 GiST 或 SP-GiST 索引一起使用的范围类型都应定义子类型差值或 subtype_diff 函数。(如果没有 subtype_diff,索引仍然可以工作,但可能效率会大大降低。)子类型差值函数接收子类型的两个输入值,并将其差值(即 X 减去 Y)表示为 float8 值。在我们上面的示例中,可以使用底层普通 float8 减法运算符的 float8mi 函数;但对于任何其他子类型,则需要进行一些类型转换。有时也需要创造性地思考如何将差值表示为数字。在最大可能范围内,subtype_diff 函数应与所选操作符类和排序规则所隐含的排序一致;也就是说,当第一个参数根据排序大于第二个参数时,其结果应为正。

一个不太简化的 subtype_diff 函数示例是:

CREATE FUNCTION time_subtype_diff(x time, y time) RETURNS float8 AS
'SELECT EXTRACT(EPOCH FROM (x - y))' LANGUAGE sql STRICT IMMUTABLE;

CREATE TYPE timerange AS RANGE (
    subtype = time,
    subtype_diff = time_subtype_diff
);

SELECT '[11:10, 23:00]'::timerange;

有关创建范围类型的更多信息,请参阅 CREATE TYPE

8.17.9. 索引 #

可以为范围类型的表列创建 GiST 和 SP-GiST 索引。还可以为多范围类型的表列创建 GiST 索引。例如,要创建一个 GiST 索引:

CREATE INDEX reservation_idx ON reservation USING GIST (during);

范围上的 GiST 或 SP-GiST 索引可以加速涉及以下范围运算符的查询:=&&<@@><<>>-|-&<&>。多范围上的 GiST 索引可以加速涉及相同多范围运算符集的查询。范围上的 GiST 索引和多范围上的 GiST 索引还可以加速涉及这些跨类型范围到多范围以及多范围到范围的运算符的查询:&&<@@><<>>-|-&<&>。有关更多信息,请参阅 表 9.58

此外,还可以为范围类型的表列创建 B 树和哈希索引。对于这些索引类型,基本上唯一有用的范围操作是相等性。范围值有一个 B 树排序顺序,带有相应的 <> 运算符,但这种排序相当随意,在实际世界中通常没有用。范围类型的 B 树和哈希支持主要用于允许查询内部进行排序和哈希,而不是创建实际索引。

8.17.10. 范围约束 #

虽然 UNIQUE 是标量值的自然约束,但通常不适用于范围类型。相反,排除约束通常更合适(请参阅 CREATE TABLE ... CONSTRAINT ... EXCLUDE)。排除约束允许对范围类型指定诸如“不重叠”之类的约束。例如:

CREATE TABLE reservation (
    during tsrange,
    EXCLUDE USING GIST (during WITH &&)
);

该约束将阻止任何重叠值同时存在于表中。

INSERT INTO reservation VALUES
    ('[2010-01-01 11:30, 2010-01-01 15:00)');
INSERT 0 1

INSERT INTO reservation VALUES
    ('[2010-01-01 14:45, 2010-01-01 15:45)');
ERROR:  conflicting key value violates exclusion constraint "reservation_during_excl"
DETAIL:  Key (during)=(["2010-01-01 14:45:00","2010-01-01 15:45:00")) conflicts
with existing key (during)=(["2010-01-01 11:30:00","2010-01-01 15:00:00")).

您可以使用 btree_gist 扩展来定义普通标量数据类型的排除约束,然后可以将这些约束与范围排除结合使用以获得最大的灵活性。例如,在安装 btree_gist 之后,以下约束仅在会议室编号相同时才会拒绝重叠范围:

CREATE EXTENSION btree_gist;
CREATE TABLE room_reservation (
    room text,
    during tsrange,
    EXCLUDE USING GIST (room WITH =, during WITH &&)
);

INSERT INTO room_reservation VALUES
    ('123A', '[2010-01-01 14:00, 2010-01-01 15:00)');
INSERT 0 1

INSERT INTO room_reservation VALUES
    ('123A', '[2010-01-01 14:30, 2010-01-01 15:30)');
ERROR:  conflicting key value violates exclusion constraint "room_reservation_room_during_excl"
DETAIL:  Key (room, during)=(123A, ["2010-01-01 14:30:00","2010-01-01 15:30:00")) conflicts
with existing key (room, during)=(123A, ["2010-01-01 14:00:00","2010-01-01 15:00:00")).

INSERT INTO room_reservation VALUES
    ('123B', '[2010-01-01 14:30, 2010-01-01 15:30)');
INSERT 0 1

提交更正

如果您在文档中发现任何不正确、与您对特定功能的体验不符或需要进一步澄清的内容,请使用 此表单 来报告文档问题。