在设计数据库表结构时,我们经常会遇到同一个字段规范在十几张表里重复出现的情况。比如手机号格式、订单状态、金额精度,这些规则如果只靠每次手写CHECK约束来保证,一旦规则变了就要挨个表去改,遗漏一张表就是数据隐患。PostgreSQL提供的DOMAIN(域)正是解决这个问题的利器:它允许你基于内置类型创建一个带约束的"子类型",一处定义,处处复用。

什么是DOMAIN,它和普通类型有什么区别
DOMAIN可以理解为"带规则的类型别名"。它底层仍然基于某个已有的类型(比如varchar、numeric),但可以在其上附加若干约束条件,包括CHECK约束、DEFAULT默认值和NOT NULL限制。当某个列声明为这个DOMAIN时,所有约束会自动生效,不需要再单独写一遍。
举个例子,系统里到处都要存手机号,规则是必须为11位数字。不用DOMAIN时,你可能要在每张表上写CHECK (phone ~ '^1[0-9]{10}$')。而定义一个DOMAIN之后,这个规则就跟着类型走了:
CREATE DOMAIN chinese_phone AS varchar(11)
CHECK (VALUE ~ '^1[0-9]{10}$');
-- 使用时和普通类型没有区别
CREATE TABLE customer (
id bigserial PRIMARY KEY,
name varchar(50) NOT NULL,
phone chinese_phone NOT NULL
);
-- 违反约束会被数据库直接拒绝
INSERT INTO customer (name, phone) VALUES ('张三', '12345');
-- ERROR: value for domain chinese_phone violates check constraint注意约束里用的关键字是VALUE,它代表正在被校验的那个值,这是DOMAIN约束特有的写法。一个DOMAIN可以同时挂多个约束,写多个CHECK子句或者用AND连接都可以。
DOMAIN与CHECK约束、枚举类型的取舍
既然CHECK约束也能实现校验,为什么还要用DOMAIN?核心区别在于复用粒度。CHECK约束属于某张表的某一列,而DOMAIN是数据库级别的对象,可以被任意表引用。当规则需要变更时,DOMAIN只需要修改一次,所有引用它的表自动继承新规则,维护成本大幅降低。
另一个经常被拿来对比的是枚举类型ENUM。比如订单状态这种固定取值,用ENUM和DOMAIN都能实现:
-- 方式一:枚举类型
CREATE TYPE order_status AS ENUM ('pending', 'paid', 'shipped', 'done');
-- 方式二:域
CREATE DOMAIN order_status_d AS varchar(20)
CHECK (VALUE IN ('pending', 'paid', 'shipped', 'done'));两者的差异值得细看。ENUM是真正独立的类型,存储紧凑、比较速度快,但新增值要用ALTER TYPE ... ADD VALUE,且在旧版本中不能在事务里执行,灵活性差。DOMAIN基于varchar,新增一个合法值只需改约束条件,配合默认值和长度限制也更自由。一般来说,取值极度稳定、需要参与排序语义的场景适合ENUM;规则可能演化、需要跨项目复用的场景适合DOMAIN。
DIRECT修改与删除DOMAIN的注意事项
DIRECT语法上DOMAIN支持ALTER操作,可以添加或删除约束。需要注意的是,添加约束时PostgreSQL默认会扫描所有引用该DOMAIN的现有数据,如果存量数据不满足新约束,操作会直接失败:
-- 给金额域增加非负约束
ALTER DOMAIN amount_dom ADD CONSTRAINT positive_check
CHECK (VALUE >= 0);
-- 如果已有负数数据,会报错并回滚
-- 宽松方案:先不校验存量数据,只对新增数据生效
ALTER DOMAIN amount_dom ADD CONSTRAINT positive_check
CHECK (VALUE >= 0) NOT VALID;
-- 后台修完数据后再补验证
ALTER DOMAIN amount_dom VALIDATE CONSTRAINT positive_check;
-- 删除约束
ALTER DOMAIN amount_dom DROP CONSTRAINT positive_check;NOT VALID加VALIDATE CONSTRAINT的组合是生产环境改约束的标准套路,能避免长时间锁表。另外要记住,删除DOMAIN之前必须先删除或修改所有引用它的列,否则会报依赖错误。可以先查系统视图确认引用情况:
SELECT n.nspname AS schema_name,
c.relname AS table_name,
a.attname AS column_name
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
JOIN pg_attribute a ON a.attrelid = c.oid AND a.atttypid = 'chinese_phone'::regtype
WHERE c.relkind = 'r';实际业务中的封装实践
在真实项目里,建议把领域内常用的DOMAIN统一放到一个独立的schema中管理,比如建一个commonschema,存放手机号、邮箱、金额、状态等公共类型。这样迁移脚本清晰,团队成员一眼就能知道哪些类型是可以直接复用的。
CREATE SCHEMA common;
CREATE DOMAIN common.email_addr AS varchar(254)
CHECK (VALUE ~ '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$');
CREATE DOMAIN common.amount AS numeric(12,2)
DEFAULT 0
CHECK (VALUE >= 0);
CREATE TABLE payment (
id bigserial PRIMARY KEY,
amount common.amount NOT NULL,
contact common.email_addr
);这样封装还有个隐性好处:应用层的ORM大多支持映射自定义类型,一旦DOMAIN定义变更,数据库层是唯一的事实来源,避免了应用层校验和数据库校验不一致的尴尬。当然,DOMAIN不能完全替代应用层校验(比如唯一性、跨表关联规则仍需另行处理),但作为数据完整性的最后一道防线,它比散落各处的CHECK约束可靠得多。合理使用DOMAIN,能让你的表结构设计更规范、更易维护。
PostgreSQLDOMAIN约束数据类型修改时间:2026-09-07 07:08:34