范围类型是表示某个元素类型(称为范围的
子类型)的一段取值范围的数据类型。
例如,timestamp 的范围可以用来表示会议室被预订的时间段。此时数据类型是
tsrange(“timestamp range” 的缩写),
timestamp 是子类型。子类型必须具有全序,这样元素值是位于某个值范围之内、之前还是之后才有明确的定义。
范围类型之所以有用,是因为它们用单个范围值表示许多元素值,而且像范围重叠这样的概念可以被清晰地表达。把时间与日期范围用于日程安排是最典型的例子;价格区间、仪器测量范围等也同样有用。
每个范围类型都有对应的多重范围类型。多重范围是非连续、非空、非空值范围的有序列表。大多数范围操作符也适用于多重范围,此外它们还有一些自己的函数。
PostgreSQL 自带以下内置范围类型:
int4range — integer 的范围,
int4multirange — 对应的多重范围
int8range — bigint 的范围,
int8multirange — 对应的多重范围
numrange — numeric 的范围,
nummultirange — 对应的多重范围
tsrange — timestamp without time zone 的范围,
tsmultirange — 对应的多重范围
tstzrange — timestamp with time zone 的范围,
tstzmultirange — 对应的多重范围
daterange — date 的范围,
datemultirange — 对应的多重范围
此外,你可以定义自己的范围类型;更多信息请参见 CREATE TYPE。
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));
范围类型的操作符与函数的完整列表请参见 Table 9.58 与 Table 9.60。
每个非空范围都有两个边界:下界与上界。这两个值之间的所有点都包含在范围内。包含边界表示边界点本身也包含在范围内,而排除边界表示边界点不包含在范围内。
在范围的文本形式中,包含下界用
“[” 表示,排除下界用
“(” 表示。类似地,包含上界用
“]” 表示,排除上界用
“)” 表示。
(更多细节参见
Section 8.17.5。)
函数 lower_inc
与 upper_inc 分别测试范围值的下界与上界是否为包含边界。
范围的下界可以省略,表示所有小于上界的值都包含在范围内,例如
(,3]。同样,如果省略范围的上界,则所有大于下界的值都包含在范围内。如果上下界都省略,则元素类型的所有值都被认为在范围之内。把缺失的边界指定为包含边界会被自动转换为排除边界,例如
[,] 会被转换为
(,)。你可以把这些缺失的值看作 +/- 无穷大,但它们是特殊的范围类型值,被认为超出任何范围元素类型的 +/- 无穷大值。
具有 “无穷” 概念的元素类型可以把它们用作显式的边界值。例如,对于 timestamp 范围,
[today,infinity) 排除特殊的
timestamp 值
infinity,而
[today,infinity]、[today,) 与
[today,] 都包含它。
函数 lower_inf
与 upper_inf 分别测试范围的上下界是否为无穷。
范围值的输入必须遵循以下模式之一:
(lower-bound,upper-bound) (lower-bound,upper-bound] [lower-bound,upper-bound) [lower-bound,upper-bound] empty
圆括号或方括号指示上下界是排除的还是包含的,如前所述。注意最后一个模式是
empty,它表示空范围(不包含任何点的范围)。
lower-bound 可以是对子类型有效的输入字符串,也可以留空表示没有下界。同样,upper-bound 可以是对子类型有效的输入字符串,也可以留空表示没有上界。
每个边界值都可以用 "(双引号)字符加引号。如果边界值包含圆括号、方括号、逗号、双引号或反斜杠,这样做是必要的,否则这些字符会被当作范围语法的一部分。要在带引号的边界值中放入双引号或反斜杠,在它前面加一个反斜杠。(此外,带双引号的边界值中的一对双引号被视为一个双引号字符,类似于 SQL 字面量字符串中单引号的规则。)或者,你也可以避免加引号,用反斜杠转义来保护所有原本会被当作范围语法的数据字符。另外,要书写作为空字符串的边界值,请写
"",因为什么都不写表示无穷边界。
范围值前后允许有空白,但圆括号或方括号之间的任何空白都被视为下界或上界值的一部分。(依据元素类型不同,它可能有意义,也可能没有。)
这些规则与在组合类型字面量中书写字段值的规则非常相似。更多说明请参见 Section 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;
每个范围类型都有一个与该范围类型同名的构造函数。使用构造函数通常比书写范围字面量常量更方便,因为它避免了对边界值进行额外加引号的需要。构造函数接受两到三个参数。双参数形式以标准形式构造范围(下界包含、上界排除),而三参数形式按照第三个参数指定的边界形式构造范围。
第三个参数必须是字符串
“()”、
“(]”、
“[)” 或
“[]” 之一。
例如:
-- 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));
离散范围是其元素类型具有明确定义的
“步长” 的范围,例如
integer 或
date。对于这些类型,当两个元素之间没有其他有效值时,可以说它们是相邻的。这与连续范围形成对比,后者在两个给定值之间总是(或几乎总是)可以找出其他元素值。例如,
numeric 类型上的范围是连续的,
timestamp 上的范围也是如此。(尽管
timestamp 精度有限,理论上可以被视为离散,但由于步长通常不是关注点,最好把它视为连续的。)
理解离散范围类型的另一种方式是:每个元素值都有明确的
“下一个” 或 “上一个” 值的概念。知道了这一点,就可以通过选择下一个或上一个元素值来替代最初给定的值,从而在范围的包含与排除表示之间转换。
例如,在整型范围类型中,[4,8] 与
(3,9) 表示同一组值;但对 numeric 上的范围而言并非如此。
离散范围类型应当具有一个 规范化 函数,它了解元素类型期望的步长。规范化函数负责把范围类型的等价值转换为相同的表示,特别是保持一致地包含或排除边界。如果未指定规范化函数,则格式不同的范围将始终被视为不相等,即使它们实际上可能表示同一组值。
内置范围类型
int4range、int8range
与 daterange 都使用包含下界并排除上界的规范形式;也就是
[)。不过,用户定义的范围类型可以使用其他约定。
用户可以定义自己的范围类型。这样做最常见的原因是想在内置范围类型未提供的子类型上使用范围。
例如,要定义一个子类型为
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 索引仍然可以工作,但其效率很可能远低于提供了差值函数的情况。)子类型差值函数接受子类型的两个输入值,并以
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。
可以为范围类型的表列创建 GiST 与 SP-GiST 索引。多重范围类型的表列也可以创建 GiST 索引。 例如,创建一个 GiST 索引:
CREATE INDEX reservation_idx ON reservation USING GIST (during);
范围上的 GiST 或 SP-GiST 索引可以加速涉及以下范围操作符的查询:
=、
&&、
<@、
@>、
<<、
>>、
-|-、
&< 与
&>。
多重范围上的 GiST 索引可以加速涉及同一组多重范围操作符的查询。
范围上的 GiST 索引与多重范围上的 GiST 索引还可以相应地加速涉及以下跨类型的范围到多重范围与多重范围到范围操作符的查询:
&&、
<@、
@>、
<<、
>>、
-|-、
&< 与
&>。
更多信息请参见
Table 9.58。
此外,可以为范围类型的表列创建 B 树与哈希索引。对这些索引类型而言,基本上唯一有用的范围操作是等值。范围值定义了 B 树排序顺序,以及相应的
< 与
> 操作符,但这种排序相当随意,在现实世界中通常没什么用。范围类型的 B 树与哈希支持主要是为了允许在查询内部进行排序与哈希,而非创建实际的索引。
虽然 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