外键的本质竟然是触发器?深入解析 PostgreSQL 约束底层

来源:这里教程网 时间:2026-03-14 23:48:09 作者:

什么是约束

在定义表或列时,可以为数据附加校验或强制规则的,这些规则称为约束。

数据类型本身只能提供较粗粒度的限制,例如 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。 1.png

    列约束与表约束

    pg_constraint 文档中明确指出:

    列约束不会被特殊处理,每个列约束在内部都等价于某种表约束。

    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 .. 删除。系统目录中并不存在“列约束”的特殊标识,它只是作用于单列的表约束。

    下面的查询用于查看两个示例表中的约束定义:

    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() 为系统函数,用于获取约束定义文本。
  • 查询结果如下所示。两种约束在内部表示上几乎完全一致,仅约束名称和所属表不同。

    -[ 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 约束或默认值,用于集中定义数据规则。

    示例如下:

    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 中查询定义在域上的约束:

    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 中的定义一致
  • 查询结果如下,展示了约束名称、定义内容以及所属域:

    conname            | definition                            | domain_name
    -------------------+---------------------------------------+-------------
    email_address_check|CHECK (VALUE ~* '^[^@]+@[^@]+\.[^@]+$')| email_address

    总结

    通过  pg_constraint 系统目录,可以系统理解 PostgreSQL 中各类约束的内部表示方式。无论是列约束、表约束、约束触发器,还是域上的约束,本质上都通过同一套机制进行管理,这是 PostgreSQL 约束体系设计上的关键特点。

    原文链接:

    https://xata.io/blog/constraints-in-postgres

    作者:Gulcin Yildirim Jelinek

  • 相关推荐