什么是约束
在定义表或列时,可以为数据附加校验或强制规则的,这些规则称为约束。
数据类型本身只能提供较粗粒度的限制,例如 numeric 无法限定只能为正数。更具体的规则(如唯一性、取值范围等)需要通过约束来实现。
约束用于保障数据完整性。当插入或默认值违反约束时,PostgreSQL 会直接报错。
本质上,约束是数据库层面强制执行的数据规则。一旦缺失或使用不当,数据问题往往会悄然积累,并最终演变为难以排查的缺陷。
pg_constraint 系统目录
从内部实现来看,PostgreSQL 中的所有约束,都会以记录的形式存储在 pg_constraint 系统目录中。
🗄️ 什么是系统目录(Catalog)
系统目录是 PostgreSQL 用来保存元数据的系统表。用户表存储业务数据,而系统目录则记录"数据库自身的信息",例如表、列、索引、约束等。
除
pg_constraint之外,常见的系统目录还包括:
pg_class:所有关系对象(表、索引、视图等)pg_attribute:表的列信息pg_type:数据类型(含域和自定义类型)pg_namespace:模式(schema)pg_index:索引相关信息(其余信息主要在pg_class中)pg_proc:函数、过程及聚合函数这些表都位于
pg_catalog模式中,该模式在search_path中默认优先,因此通常无需显式指定。
pg_constraint用于存储表上的 CHECK、NOT NULL、主键、唯一、外键和排他约束。
需要注意的是,在 PostgreSQL 18 之前,表上的 NOT NULL 约束并不存储在 pg_constraint 中,而是记录在 pg_attribute;从 PostgreSQL 18 开始,NOT NULL 才在 pg_constraint 中拥有独立记录。
PostgreSQL 17:
pg_constraint目录用于存储 CHECK、主键、唯一、外键、排他约束,以及定义在域上的 NOT NULL 约束。表上的 NOT NULL 约束仍然记录在
pg_attribute中,而非pg_constraint。
因此,每一个约束都会在 pg_constraint 中以一条记录存在,并通过 contype 字段标识约束类型。后文将对这些类型逐一说明,其中也包括一个较为特殊的类型:t。 
列约束与表约束
pg_constraint 文档中明确指出:
列约束不会被特殊处理,每个列约束在内部都等价于某种表约束。
SQL 层面上,约束可以写在列定义后,也可以写成表约束,例如:
sql
CREATE TABLE products_oct (
price numeric CHECK (price > 0)
);
CREATE TABLE products_nov (
price numeric,
CHECK (price > 0)
);
第一种写法只作用于单列,第二种写法可以作用于多列。但在 PostgreSQL 内部,这两种方式最终都会被统一记录为 pg_constraint 中的一行数据。
因此,无论约束以哪种形式定义,都可以通过 ALTER TABLE .. DROP CONSTRAINT .. 删除。系统目录中并不存在"列约束"的特殊标识,它只是作用于单列的表约束。
下面的查询用于查看两个示例表中的约束定义:
vbnet
SELECT
rel.relname AS table_name,
c.conname,
c.contype,
c.conrelid::regclass AS table_ref,
c.conkey,
pg_get_constraintdef(c.oid, true) AS constraint_def
FROM pg_constraint c
JOIN pg_class rel ON rel.oid = c.conrelid
WHERE rel.relname IN ('products_oct', 'products_nov');
⚡ 查询要点说明
pg_class用于存储所有关系对象的元数据。relname为表的名称,由于pg_constraint中仅保存表的 OID,需要通过rel.oid = c.conrelid进行关联。conrelid表示约束所属表的 OID。conname为约束名称,约束名称在单表内唯一,可由系统自动生成,也可在 DDL 中显式指定。contype表示约束类型(c、f、n、p、u、x、t)。conkey为属性编号数组,用于标识约束涉及的列(如{1}表示第一列,{1,3}表示第一和第三列)。pg_get_constraintdef()为系统函数,用于获取约束定义文本。
查询结果如下所示。两种约束在内部表示上几乎完全一致,仅约束名称和所属表不同。
sql
-[ RECORD 1 ]--+---------------------------
table_name | products_nov
conname | products_nov_price_check
contype | c
table_ref | products_nov
conkey | {1}
constraint_def | CHECK (price > 0::numeric)
-[ RECORD 2 ]--+---------------------------
table_name | products_oct
conname | products_oct_price_check
contype | c
table_ref | products_oct
conkey | {1}
constraint_def | CHECK (price > 0::numeric)
约束触发器(Constraint Trigger)
在 pg_constraint 中,使用 CREATE CONSTRAINT TRIGGER 创建的约束触发器同样会生成记录,其 contype 标记为 t。常见约束如 UNIQUE 为 u,CHECK 为 c。
约束触发器是一种将触发器机制与约束系统结合的特殊形式,主要用于数据一致性校验。
可延迟触发器(Deferrable Triggers)
约束触发器通过 CREATE CONSTRAINT TRIGGER 创建,语法与普通触发器类似,但指定 CONSTRAINT 后生成的是约束触发器。其核心区别在于,约束触发器可以通过 SET CONSTRAINTS 控制触发执行时机。
其执行时机可通过 SET CONSTRAINTS 控制:
IMMEDIATE:语句结束时检查DEFERRED:事务提交时检查
与普通触发器不同,约束触发器允许在事务级别延迟执行,并在运行时动态调整。
⚠️ WHEN 条件始终立即评估
即使触发器本身是延迟执行的,
WHEN子句仍在语句执行时立即判断,用于决定是否进入执行队列。
AFTER 触发器
在创建触发器时,需要指定触发函数的执行时机:BEFORE、AFTER 或 INSTEAD OF。约束触发器只能定义为 AFTER 触发器。
约束触发器并不用于改变数据处理流程,而是在数据操作完成后进行条件校验。约束的核心目标是数据验证,而普通触发器通常用于数据修改。约束触发器属于校验机制的一部分,当其所实现的约束条件被违反时,应当抛出异常。
FOR EACH ROW 触发器
创建触发器时,还需要指定触发粒度:
FOR EACH ROW:对受影响的每一行执行一次FOR EACH STATEMENT:每条 SQL 语句只执行一次
约束触发器只能定义为 FOR EACH ROW,这是因为约束校验依赖于单行数据的具体取值。
需要注意的是,约束触发器不支持 OR REPLACE 选项,因此只能通过删除后重新创建的方式进行修改。
为什么需要约束触发器
在《Triggers to enforce constraints in PostgreSQL》一文中,Laurenz Albe 指出,某些需要在表级别强制执行的规则,无法通过常规约束直接表达,此时可借助触发器机制实现。文中结合示例说明了适用场景,并分析了约束与触发器在 MVCC 行为上的差异。
在实际系统中,约束触发器很少由用户显式创建。PostgreSQL 更多将其作为约束实现的内部基础机制使用,尤其是在外键约束中。外键依赖系统自动生成的约束触发器实现,这一设计也使外键能够支持 DEFERRABLE 和 INITIALLY DEFERRED 等特性。
什么是域
域可以理解为"带约束的数据类型"。它基于已有类型(如 text、integer),但可以附加 NOT NULL、CHECK 约束或默认值,用于集中定义数据规则。
示例如下:
sql
CREATE DOMAIN email_address AS text
CHECK (VALUE ~* '^[^@]+@[^@]+\.[^@]+$');
CREATE TABLE users (
id serial PRIMARY KEY,
email email_address NOT NULL
);
-- This will fail
INSERT INTO users(email) VALUES ('not-an-email');
-- This will be successful
INSERT INTO users(email) VALUES ('ok@example.com');
上述示例中定义了一个名为 email_address 的新类型。所有使用该类型的列,在插入或更新数据时都会自动校验正则表达式。即使表本身未显式定义 CHECK 约束,非法值仍会被拒绝。
通常情况下,约束是附加在表上的,但 PostgreSQL 同样支持在域上定义约束。以下查询演示了如何从 pg_constraint 中查询定义在域上的约束:
vbnet
SELECT c.conname,
pg_get_constraintdef(c.oid, true) AS definition,
t.typname AS domain_name
FROM pg_constraint c
JOIN pg_type t ON t.oid = c.contypid
WHERE c.contype = 'c'
AND c.contypid <> 0;
⚡ 查询要点说明
pg_constraint存储所有类型的约束,包括表约束和域约束pg_type存储数据类型信息,包括域contypid表示约束所属域的 OID。当contypid非 0 时,约束附加在域上;当为 0 时,约束附加在表上,此时使用conrelid- 通过
JOIN pg_type t ON t.oid = c.contypid获取域名称- 域仅支持
CHECK约束,因此筛选条件为c.contype = 'c'pg_get_constraintdef()用于获取约束定义文本,与CREATE DOMAIN中的定义一致
查询结果如下,展示了约束名称、定义内容以及所属域:
ini
conname | definition | domain_name
-------------------+---------------------------------------+-------------
email_address_check|CHECK (VALUE ~* '^[^@]+@[^@]+\.[^@]+$')| email_address
总结
通过 pg_constraint 系统目录,可以系统理解 PostgreSQL 中各类约束的内部表示方式。无论是列约束、表约束、约束触发器,还是域上的约束,本质上都通过同一套机制进行管理,这是 PostgreSQL 约束体系设计上的关键特点。
原文链接:
作者:Gulcin Yildirim Jelinek