如何使用CREATE TYPE创建范围类型?

来源:AI技术网作者:俊华头衔:草根站长
导读:本期聚焦于俊华创作的《如何使用CREATE TYPE创建范围类型?》,敬请观看详情。想存储温度区间、价格区间或者时间窗口,又希望SQL查询能直接判断值是否落在区间内,PostgreSQL的范围类型提供了一套完整的解决方案。除了内置的int4range、tsrange等类型,CREATE TYPE命令还能基于任意标量类型创建符合业务语义的自定义范围类型。本文将解析CREATE TYPE AS RANGE的语法结构,重点说明SUBTYPE、SUBTYPE_OPCLASS、CANONICAL和SUBTYPE_DIFF等参数的作用,并通过一个完整的温度区间示例演示从定义类型、插入数据到范围查询的全过程。同时还会对比内置范围类型与自定义范围类型的适用场景,帮助你理解规范化函数对区间合并和索引优化的影响。自定义范围类型一旦创建成功,就能像原生类型一样参与表定义、约束和GiST索引,减少应用程序手动拆分区间边界的成本。

PostgreSQL 自 9.2 版本起提供了范围类型,用于表示一段连续的区间,比如订单金额区间、设备运行温度区间或会议时间段。系统内置的 int4range、int8range、numrange、tsrange、tstzrange、daterange 已经能覆盖不少基础场景,但当业务需要特定比较规则或希望规范化边界表示时,使用 CREATE TYPE 的 AS RANGE 子句可以创建自己的范围类型。通过指定子类型、操作符类以及规范化函数,这个自定义范围类型可以像内置类型一样参与列定义、约束检查和 GiST 索引。

如何使用CREATE TYPE创建范围类型?

CREATE TYPE AS RANGE 的语法结构与参数说明

创建范围类型的基本语法并不复杂,核心是 AS RANGE 子句及其中的参数。下面是最完整的语法形式:

CREATE TYPE name AS RANGE (
    SUBTYPE = subtype,
    SUBTYPE_OPCLASS = subtype_operator_class,
    COLLATION = collation,
    CANONICAL = canonical_function,
    SUBTYPE_DIFF = subtype_diff_function,
    MULTIRANGE_TYPE_NAME = multirange_type_name
);

其中 SUBTYPE 是必填项,它指定范围中每一个元素的类型,也就是子类型。子类型可以是任意带排序和等值语义的标量类型,例如 integer、numeric、timestamp 等。SUBTYPE_OPCLASS 指定用于子类型比较的操作符类,默认使用子类型的默认 B-tree 操作符类。对于自定义操作符类或者需要特殊排序规则的场景,这个参数必须显式给出。

COLLATION 只在子类型为可排序类型时有用,用来指定范围边界排序所采用的排序规则;如果子类型不支持排序规则,设置该参数会报错。CANONICAL 和 SUBTYPE_DIFF 是可选的函数参数,分别用于规范化范围值和计算两个子类型值之间的差异。它们对索引效率和数据一致性影响很大,后面会单独展开。最后 MULTIRANGE_TYPE_NAME 用于指定关联的多范围类型名称,PostgreSQL 14 及以上版本支持多范围类型,可以表示不连续的范围集合。

创建命令执行成功后,这个新建的类型会出现在 pg_type 系统目录中,并且系统会自动生成一组范围操作符和函数,例如包含判断、重叠判断、合并、交集等。这意味着我们可以直接使用 @> 运算符来判断一个值是否落在范围内,而不需要手动比较上下界。

自定义 canonical 函数与 subtype_diff 函数

canonical 函数接收一个范围值作为参数,返回一个等价的、规范化的范围值。它最常见的用途是处理离散类型中的开区间边界。以整数范围为例,开区间 (1,5) 和闭区间 [2,4] 表示完全相同的整数集合。如果不进行规范化,数据库可能把这两个范围当成不同的值,导致索引膨胀和比较结果不一致。通过 canonical 函数,我们可以把 (1,5) 转换为 [2,4],保证相同集合始终用同一种形式存储。

写 canonical 函数时需要注意,它必须返回一个合法的范围值,不能改变原始范围的实际集合。PostgreSQL 会在写入和更新范围值时调用它,如果函数返回了错误的结果,查询结果可能出错。下面这个函数演示了离散整数范围的规范化逻辑,它接收内置的 int4range 并返回规范化后的 int4range:

CREATE FUNCTION int4range_canonical(r int4range)
RETURNS int4range
AS $$
DECLARE
    lower_val integer;
    upper_val integer;
    lower_inc boolean;
    upper_inc boolean;
BEGIN
    IF lower(r) IS NULL OR upper(r) IS NULL THEN
        RETURN r;
    END IF;

    lower_val := lower(r)::integer;
    upper_val := upper(r)::integer;
    lower_inc := lower_inc(r);
    upper_inc := upper_inc(r);

    IF NOT lower_inc THEN
        lower_val := lower_val + 1;
        lower_inc := true;
    END IF;

    IF NOT upper_inc THEN
        upper_val := upper_val - 1;
        upper_inc := true;
    END IF;

    RETURN int4range(lower_val, upper_val, '[)');
END;
$$ LANGUAGE plpgsql IMMUTABLE STRICT;

这个函数里使用了 lower、upper、lower_inc 和 upper_inc 几个系统函数来拆解范围边界。注意 lower_inc 返回布尔值,表示下界是否为闭区间。处理时先把开区间转换为闭区间,再用 int4range 构造标准半开区间。虽然这里最后统一成了 [) 形式,但它仍然保持了原始整数集合不变。实际创建自定义范围类型时,如果子类型是离散的,也可以参考这段逻辑编写自己的规范化函数。

subtype_diff 函数的参数是两个子类型值,返回值是一个双精度浮点数,表示它们之间的差值。这个函数对 GiST 索引的精度很重要。如果没有提供 subtype_diff,范围索引仍然可以工作,但无法准确估计某些范围谓词的选择性,可能导致查询计划偏向全表扫描。对于 numeric 类型,可以直接使用内置的 numeric_sub 函数;对于 timestamp 类型,可以用 timestamp_sub。自定义离散类型如果没有合适的差异函数,也可以返回一个粗略的距离值。

完整示例:创建温度范围类型并进行查询

接下来创建一个基于 numeric 的温度范围类型,用于存储设备允许运行的温度区间。温度是连续量,不需要像整数那样做离散开闭区间的步长转换,因此 canonical 函数可以省略。但为了优化范围索引,我们提供 numeric_sub 作为 SUBTYPE_DIFF。

CREATE TYPE temprange AS RANGE (
    SUBTYPE = numeric,
    SUBTYPE_OPCLASS = numeric_ops,
    SUBTYPE_DIFF = numeric_sub
);

创建完成后,可以在建表时直接使用这个类型。下面建立一个传感器读数表,其中 reading_range 列存放温度范围,并创建一个 GiST 索引来加速范围查询:

CREATE TABLE temp_readings (
    id bigserial PRIMARY KEY,
    device_id integer NOT NULL,
    reading_range temprange NOT NULL,
    recorded_at timestamptz DEFAULT now()
);

CREATE INDEX idx_temp_readings_range
ON temp_readings
USING GiST (reading_range);

插入数据时,范围值可以用字符串形式表示,再通过 ::temprange 转换。边界使用方括号表示包含,圆括号表示不包含,例如 [18.5,24.0) 表示大于等于 18.5 且小于 24.0。查询某个温度是否落在范围内,使用包含运算符 @>;查询两个范围是否重叠,使用重叠运算符 &&。

INSERT INTO temp_readings (device_id, reading_range)
VALUES
    (1, '[18.5,24.0)'::temprange),
    (2, '[15.0,20.0)'::temprange),
    (3, '[22.0,30.0)'::temprange);

SELECT * FROM temp_readings
WHERE reading_range @> 22.5::numeric;

SELECT device_id FROM temp_readings
WHERE reading_range && '[19.0,23.0)'::temprange;

第一条查询返回设备 3,因为 22.5 落在 [22.0,30.0) 内;第二条查询返回设备 1 和 3,因为它们的范围与 [19.0,23.0) 相交。GiST 索引能够高效处理这类范围谓词,即使表数据量很大,也能避免顺序扫描。

如果希望保证范围不为空,可以在表约束中使用范围函数 isempty。例如添加检查约束:

ALTER TABLE temp_readings
ADD CONSTRAINT temp_range_not_empty
CHECK (NOT isempty(reading_range));

还可以使用 lower 和 upper 函数抽取边界值,或者使用 lower_inc 和 upper_inc 判断边界是否包含。这些函数让自定义范围类型在应用层处理起来非常灵活。

与内置范围类型的对比及使用建议

内置范围类型和自定义范围类型并不是对立关系。如果业务区间只是普通的整数、日期或时间戳区间,直接使用内置类型即可,它们已经配置了合适的 canonical 函数和 subtype_diff 函数,开箱即用。自定义范围类型的价值在于当子类型是自定义枚举、自定义复合类型或者需要特殊排序规则时,可以通过 AS RANGE 精确控制比较逻辑。

创建自定义范围类型后,建议至少考虑两个问题。第一,子类型的等值语义是否清晰。范围类型的很多操作依赖子类型等值判断,如果子类型没有稳定的等值规则,范围类型的行为可能出现意外。第二,是否提供 canonical 函数和 subtype_diff 函数。前者避免同一个集合出现多种表示,后者提升索引查询的统计信息质量,对性能敏感的系统尤为重要。

另外,范围类型虽然支持 B-tree 索引,但真正发挥区间查询优势的是 GiST 和 SP-GiST 索引。建表时如果经常执行包含、重叠、左/右相邻等范围谓词,应该优先创建 GiST 索引。对于不连续的范围集合,还可以考虑使用多范围类型,但多范围类型同样需要在创建时通过 MULTIRANGE_TYPE_NAME 指定关联名称。

总的来说,CREATE TYPE 的 AS RANGE 子句让 PostgreSQL 的范围抽象可以延伸到任意标量类型。理解它的参数含义,尤其是 canonical 和 subtype_diff,能够帮助你在数据一致性和查询性能之间找到合适的平衡点。

PostgreSQLCREATE TYPE范围类型修改时间:2026-10-01 23:57:23

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/1001/64430.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。