PostgreSQL 的类型系统并不局限于整数、字符和日期这几类内置标量。数据库允许开发人员通过 CREATE TYPE 命令定义自己的数据类型,这种能力在构建更贴近业务语义的表结构时非常关键。自定义类型可以减少冗余字段、强化取值约束,并让函数签名更清晰。接下来从类型分类、创建语法、实际应用和后期维护几个角度展开说明。

一、PostgreSQL 支持哪些自定义数据类型
PostgreSQL 的自定义数据类型大致可以分为复合类型、枚举类型、域类型、范围类型和基础类型。复合类型用于把多个字段打包成一个独立结构,枚举类型用于限制某列只能取预定义值,域类型是在已有类型上附加约束而不创建新的底层表示,范围类型则可以表达区间数据。对于大多数日常业务场景,复合类型和枚举类型使用最多,域类型也能有效提升数据质量。
复合类型的典型例子是地址。一个收货地址通常包含街道、城市、邮政编码等多个字段,如果这些字段分散在每一张需要地址的表中,会出现大量重复定义。将地址定义成一个复合类型后,任意表只需要一个 address 字段即可保存全部信息。查询时可以通过括号语法访问内部属性,例如 (address).city。这种设计让数据结构更接近领域模型,也便于统一维护。
枚举类型适合状态、级别等取值固定的字段。以前开发者常使用整型或文本表示状态,例如用0、1、2表示草稿、发布、归档,但整型无法阻止写入99这样的非法值,文本又容易因为大小写或拼写不一致产生脏数据。枚举类型可以从数据库层面约束取值范围,同时保留可读性。PostgreSQL 会把枚举值按声明顺序排序,因此还可以直接使用比较运算符。
二、使用 CREATE TYPE 创建复合类型与枚举类型
复合类型通过 CREATE TYPE ... AS 语法创建。创建后,类型名称可以像内置类型一样用于表字段、函数参数或函数返回值。下面先定义一个 address_type 复合类型,它包含 street、city 和 postal_code 三个属性。
CREATE TYPE address_type AS (
street text,
city text,
postal_code varchar(20)
);
这段代码执行后,可以在建表时直接引用 address_type。插入数据时需要把整个复合值写成 ROW 表达式,或者用字符串字面量转换。查询内部字段时使用括号语法,因为类型名和字段名之间存在优先级问题,不写括号会被解析成表名或列名。下面代码展示建表、插入和查询的完整流程。
CREATE TABLE customer_address (
id serial PRIMARY KEY,
customer_id integer NOT NULL,
address address_type
);
INSERT INTO customer_address (customer_id, address)
VALUES (1, ROW('科技路88号', '西安', '710000'));
SELECT id,
(address).street AS street,
(address).city AS city
FROM customer_address
WHERE (address).city = '西安';
注意 WHERE 条件中同样需要用括号包裹 address,否则 PostgreSQL 可能将 city 解析为当前查询中的独立列,导致错误。复合类型也支持嵌套,例如可以在 address_type 中放入另一个复合类型,但嵌套过深会让 SQL 语句变得难以阅读,需要权衡。
枚举类型的创建使用 CREATE TYPE ... AS ENUM 语法。枚举值以单引号逐个列出,顺序就是排序顺序。例如创建一个文章状态枚举类型,并把它用于博客表。
CREATE TYPE article_status AS ENUM ('draft', 'published', 'archived');
CREATE TABLE article (
id serial PRIMARY KEY,
title text NOT NULL,
status article_status NOT NULL DEFAULT 'draft'
);
INSERT INTO article (title, status)
VALUES ('PostgreSQL 自定义类型', 'published');
如果尝试插入枚举值之外的文本,例如 'deleted',PostgreSQL 会抛出错误,提醒该值不是 article_status 类型的合法成员。这种约束比使用简单的 CHECK 约束更直观,也不容易出现重复定义不一致的问题。枚举值之间可以比较大小,例如 'draft' < 'published' 在布尔判断中成立,因为 draft 在声明时位于 published 之前。
三、域类型与范围类型的使用
域类型使用 CREATE DOMAIN 创建,它不生成新的底层存储结构,而是给已有类型增加约束和默认值。比如要确保所有邮箱字段格式正确,可以定义一个 email_domain 域,只在创建时维护一次正则校验即可。
CREATE DOMAIN email_domain AS text
CHECK (VALUE ~ '^[^@]+@[^@]+$');
CREATE TABLE user_account (
id serial PRIMARY KEY,
email email_domain NOT NULL
);
域类型非常适合统一业务约束。如果多个表都需要保存手机号、邮政编码、百分比等字段,可以通过域类型保证规则一致。当约束发生变化时,只需修改域定义,而无需逐表调整。但域类型本质上是文本或数值,查询时并不区分来源,因此它更多用于数据完整性而非语义建模。
PostgreSQL 还支持范围类型,可以表达如会议时间、价格区间等连续或离散范围。虽然系统已经提供了 int4range、numrange、tsrange 等内置范围类型,但你也可以基于自己的基础类型创建范围类型。下面的例子基于 numeric 创建了一个价格范围类型。
CREATE TYPE price_range AS RANGE (
subtype = numeric,
subtype_diff = numeric_sub_diff
);
范围类型可以自动处理包含、重叠、相邻等关系,例如使用 && 运算符判断两个范围是否重叠,使用 @> 判断一个范围是否包含另一个范围。范围类型在某些排期系统或价格模型中非常有用,但复杂度较高,如果不是明确的区间建模需求,不必优先使用。
四、修改与删除自定义数据类型
自定义类型创建后并非一成不变。复合类型可以通过 ALTER TYPE 增加或删除属性,枚举类型可以增加新的枚举值。例如给 address_type 增加一个 country 字段,可以用以下命令。
ALTER TYPE address_type ADD ATTRIBUTE country text;
枚举类型增加值的语法是 ALTER TYPE article_status ADD VALUE 'deleted'。需要注意的是,在 PostgreSQL 的某些版本中,新增枚举值不能放在同一个事务中立即使用,需要先提交事务再读取,否则可能出现错误。删除枚举值则长期没有直接支持,只能通过重建类型的间接方式完成,因此在设计枚举值时尽量预留一定的扩展空间。
删除自定义类型使用 DROP TYPE 命令。如果类型已经被表、函数或视图引用,直接删除会失败。这时可以指定 CASCADE 选项,让数据库自动删除依赖该类型的对象,但操作前必须确认影响范围,避免误删重要数据。
DROP TYPE address_type CASCADE;
依赖管理是自定义类型维护中的核心风险。例如一个复合类型被多张表引用,修改其属性可能导致查询列名变化,进而影响应用程序的 SQL 语句。如果业务模型经常变化,自定义复合类型会降低迁移灵活性,此时可以考虑改用单独的关联表或 JSONB 列。但对于结构稳定、语义明确的字段集合,自定义类型仍然是比分散字段更优的选择。
总结来说,PostgreSQL 自定义数据类型是一把强大的建模工具。复合类型适合封装固定结构,枚举类型适合约束有限取值,域类型适合统一约束规则,范围类型适合表达区间。使用时要结合业务稳定性、查询复杂度和维护成本来权衡,避免为了抽象而引入不必要的复杂度。
PostgreSQL自定义数据类型CREATE TYPE复合类型修改时间:2026-08-26 23:24:10