Redrock Postgres 搜索 英文
版本: 9.3 / 9.4 / 9.5 / 9.6 / 10 / 11 / 12 / 13 / 14 / 15 / 16 / 17

8.17. 范围类型 #

8.17.1. 内置范围和多范围类型
8.17.2. 示例
8.17.3. 包含和不包含的边界
8.17.4. 无穷(无界)范围
8.17.5. 范围输入/输出
8.17.6. 构建范围和多范围
8.17.7. 离散范围类型
8.17.8. 定义新的范围类型
8.17.9. 索引
8.17.10. 对范围的约束

范围类型是代表某种元素类型(称为该范围的子类型)值范围的数据类型。例如,timestamp 范围可能用来表示会议室预订的时间范围。在这种情况下,数据类型为 tsrange(即“时间戳范围”的缩写),而 timestamp 是子类型。子类型必须有整体顺序,以便明确定义元素值是位于某个值范围之内、之前还是之后。

范围类型很有用,因为它们以一个范围值表示多个元素值,而且范围重叠等概念可以用清晰的方式表达。为计划目的使用时间和日期范围是最清晰的例子;但是,价格范围、仪器测量范围等也很有用。

每个范围类型都有一个相对应的多范围类型。多范围是由互不包含、非空、非空范围组成的有序列表。大多数范围运算符也适用于多范围,并且它们都有自己的几个函数。

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.56表 9.58

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 没有有意义的 step,所以我们在这个示例中未定义规范函数。

当定义你自己的范围时,将自动获得相应的多分段范围类型。

定义你自己的范围类型也允许你指定不同的子类型 B 树操作符类或整理规则以进行使用,从而更改确定哪些值属于给定范围的排序顺序。

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

此外,任何旨在与 GiST 或 SP-GiST 索引一起使用的范围类型均应定义一个子类型差函数或 subtype_diff 函数。(即使没有 subtype_diff,索引仍然能正常工作,但如果提供了差函数,其效率可能会大大高于前者。)子类型差函数需要两个输入值(其子类型),并以 float8 值的形式返回它们的差值(即 X 减去 Y)。在上面的示例中,基础常规 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. 索引 #

可以创建表的 range 类型的 GiST 和 SP-GiST 索引。还可以在表的 multirange 类型的列中创建 GiST 索引。例如,要创建一个 GiST 索引

CREATE INDEX reservation_idx ON reservation USING GIST (during);

对范围使用 GiST 或 SP-GiST 索引可以加速涉及以下范围运算符的查询:=&&<@@><<>>-|-&<&>。对 multirange 使用 GiST 索引可以加速涉及相同 multirange 运算符集的查询。对范围使用 GiST 索引和对 mirange 使用 GiST 索引还可以分别加速涉及这些跨类型范围至 multirange 和 multirange 至范围运算符的查询:&&<@@><<>>-|-&<&>。有关更多信息,请参见 表 9.56

此外,对于范围类型的表列,可以创建 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