2025年9月25日: PostgreSQL 18 发布!
支持的版本: 当前 (18) / 17 / 16 / 15 / 14 / 13
开发版本: 开发版
不支持的版本: 12 / 11 / 10 / 9.6 / 9.5 / 9.4 / 9.3 / 9.2 / 9.1 / 9.0 / 8.4 / 8.3 / 8.2 / 8.1 / 8.0 / 7.4 / 7.3

5.5. 约束 #

数据类型是限制表中可以存储的数据种类的方式。然而,对于许多应用程序来说,它们提供的约束过于粗糙。例如,包含产品价格的列可能应该只接受正数。但没有标准数据类型只接受正数。另一个问题是,您可能希望根据其他列或行来约束列数据。例如,在包含产品信息的表中,每个产品编号应该只有一行。

为此,SQL 允许您在列和表上定义约束。约束让您对表中的数据拥有您想要的控制权。如果用户尝试在违反约束的列中存储数据,将引发错误。即使值来自默认值定义,此规则也适用。

5.5.1. CHECK 约束 #

CHECK 约束是最通用的约束类型。它允许您指定某一列中的值必须满足布尔(真值)表达式。例如,要要求产品价格为正数,您可以使用

CREATE TABLE products (
    product_no integer,
    name text,
    price numeric CHECK (price > 0)
);

如您所见,约束定义出现在数据类型之后,就像默认值定义一样。默认值和约束可以按任何顺序排列。CHECK 约束由关键字 CHECK 后跟括号中的表达式组成。CHECK 约束表达式应涉及被约束的列,否则约束就没有太大意义。

您也可以为约束指定单独的名称。这可以使错误消息更清晰,并允许您在需要更改约束时引用它。语法是

CREATE TABLE products (
    product_no integer,
    name text,
    price numeric CONSTRAINT positive_price CHECK (price > 0)
);

因此,要指定命名约束,请使用关键字 CONSTRAINT 后跟标识符,然后是约束定义。(如果您没有以这种方式指定约束名称,系统会为您选择一个名称。)

CHECK 约束也可以引用多个列。假设您存储了常规价格和折扣价格,并且您想确保折扣价格低于常规价格

CREATE TABLE products (
    product_no integer,
    name text,
    price numeric CHECK (price > 0),
    discounted_price numeric CHECK (discounted_price > 0),
    CHECK (price > discounted_price)
);

前两个约束应该看起来很熟悉。第三个使用了一个新的语法。它没有附加到特定的列,而是作为逗号分隔的列列表中的一个独立项出现。列定义和这些约束定义可以混合排序。

我们称前两个约束为列约束,而第三个约束是表约束,因为它与任何一个列定义分开书写。列约束也可以写成表约束,但反之不一定可行,因为列约束应该只引用它所附加的列。(PostgreSQL 不强制执行此规则,但如果您希望您的表定义与其他数据库系统一起工作,您应该遵循它。)上面的例子也可以这样写

CREATE TABLE products (
    product_no integer,
    name text,
    price numeric,
    CHECK (price > 0),
    discounted_price numeric,
    CHECK (discounted_price > 0),
    CHECK (price > discounted_price)
);

甚至

CREATE TABLE products (
    product_no integer,
    name text,
    price numeric CHECK (price > 0),
    discounted_price numeric,
    CHECK (discounted_price > 0 AND price > discounted_price)
);

这取决于个人喜好。

可以像列约束一样为表约束命名

CREATE TABLE products (
    product_no integer,
    name text,
    price numeric,
    CHECK (price > 0),
    discounted_price numeric,
    CHECK (discounted_price > 0),
    CONSTRAINT valid_discount CHECK (price > discounted_price)
);

应该注意的是,如果 CHECK 约束表达式评估为 true 或 null 值,则该约束得到满足。由于大多数表达式在任何操作数为 null 时都会评估为 null 值,因此它们不会阻止被约束列中的 null 值。要确保列不包含 null 值,可以使用下一节介绍的 NOT NULL 约束。

注意

PostgreSQL 不支持引用新行或更新行以外的表数据的 CHECK 约束。虽然违反此规则的 CHECK 约束在简单测试中可能看起来有效,但它不能保证数据库不会达到约束条件为 false 的状态(由于涉及的其他行的后续更改)。这会导致数据库转储和恢复失败。即使数据库的完整状态与约束一致,恢复也可能失败,因为行的加载顺序不满足约束。如果可能,请使用 UNIQUEEXCLUDEFOREIGN KEY 约束来表达跨行和跨表限制。

如果您想要在插入行时对其他行进行一次性检查,而不是持续维护一致性保证,可以使用自定义的 触发器 来实现。(这种方法避免了转储/恢复问题,因为 pg_dump 不会在恢复数据之前重新安装触发器,因此在转储/恢复期间不会强制执行检查。)

注意

PostgreSQL 假定 CHECK 约束的条件是不可变的,也就是说,对于相同的输入行,它们总是产生相同的结果。这个假设证明了为什么只在插入或更新行时检查 CHECK 约束,而不是在其他时候。(上面关于不引用其他表数据的警告实际上是此限制的一个特殊情况。)

一个常见的破坏此假设的方法是在 CHECK 表达式中引用用户定义的函数,然后更改该函数的行为。PostgreSQL 不禁止这样做,但它不会注意到表中是否存在违反 CHECK 约束的行。这会导致后续的数据库转储和恢复失败。处理此类更改的推荐方法是删除约束(使用 ALTER TABLE),调整函数定义,然后重新添加约束,从而针对所有表行重新检查它。

5.5.2. NOT NULL 约束 #

NOT NULL 约束只是指定列不得为 null 值。语法示例

CREATE TABLE products (
    product_no integer NOT NULL,
    name text NOT NULL,
    price numeric
);

也可以指定显式的约束名称,例如

CREATE TABLE products (
    product_no integer NOT NULL,
    name text CONSTRAINT products_name_not_null NOT NULL,
    price numeric
);

NOT NULL 约束通常写成列约束。将其写成表约束的语法是

CREATE TABLE products (
    product_no integer,
    name text,
    price numeric,
    NOT NULL product_no,
    NOT NULL name
);

但这种语法不是标准的,主要供 pg_dump 使用。

NOT NULL 约束在功能上等同于创建 CHECK 约束 CHECK (column_name IS NOT NULL),但在 PostgreSQL 中,创建显式的 NOT NULL 约束效率更高。

当然,一列可以有多个约束。只需将约束一个接一个地写即可

CREATE TABLE products (
    product_no integer NOT NULL,
    name text NOT NULL,
    price numeric NOT NULL CHECK (price > 0)
);

顺序无关紧要。它不一定决定约束检查的顺序。

但是,一列最多只能有一个显式的 NOT NULL 约束。

NOT NULL 约束有一个反义:NULL 约束。这并不意味着列必须为 null,这肯定是没有用的。相反,它只是选择列可以为 null 的默认行为。NULL 约束不在 SQL 标准中,不应在可移植应用程序中使用。(它仅在 PostgreSQL 中添加以兼容某些其他数据库系统。)然而,一些用户喜欢它,因为它使得在脚本文件中切换约束变得容易。例如,您可以从以下开始

CREATE TABLE products (
    product_no integer NULL,
    name text NULL,
    price numeric NULL
);

然后将 NOT 关键字插入到需要的地方。

提示

在大多数数据库设计中,大多数列都应该标记为 NOT NULL。

5.5.3. UNIQUE 约束 #

UNIQUE 约束确保列或一组列中包含的数据在表中的所有行中都是唯一的。语法是

CREATE TABLE products (
    product_no integer UNIQUE,
    name text,
    price numeric
);

当写成列约束时,以及

CREATE TABLE products (
    product_no integer,
    name text,
    price numeric,
    UNIQUE (product_no)
);

当写成表约束时。

要为一组列定义 UNIQUE 约束,请将其写成表约束,并将列名用逗号分隔

CREATE TABLE example (
    a integer,
    b integer,
    c integer,
    UNIQUE (a, c)
);

这指定了所指示列中的值组合在整个表中是唯一的,尽管任何一个列都不需要(通常也不是)是唯一的。

您可以像往常一样为 UNIQUE 约束分配自己的名称

CREATE TABLE products (
    product_no integer CONSTRAINT must_be_different UNIQUE,
    name text,
    price numeric
);

添加 UNIQUE 约束将自动在约束中列出的列或列组上创建一个唯一的 B-tree 索引。无法通过 UNIQUE 约束来编写仅覆盖部分行的唯一性限制,但可以通过创建唯一的 部分索引 来强制执行此类限制。

通常,如果表中有多行中约束中包含的所有列的值都相等,则 UNIQUE 约束将违反。默认情况下,在此比较中,两个 null 值不被视为相等。这意味着即使存在 UNIQUE 约束,也可以存储包含至少一个约束列中的 null 值的重复行。可以通过添加 NULLS NOT DISTINCT 子句来更改此行为,例如

CREATE TABLE products (
    product_no integer UNIQUE NULLS NOT DISTINCT,
    name text,
    price numeric
);

CREATE TABLE products (
    product_no integer,
    name text,
    price numeric,
    UNIQUE NULLS NOT DISTINCT (product_no)
);

可以使用 NULLS DISTINCT 显式指定默认行为。SQL 标准规定 UNIQUE 约束中的默认 null 处理是实现定义的,并且其他实现具有不同的行为。因此,在开发旨在可移植的应用程序时要小心。

5.5.4. 主键 #

PRIMARY KEY 约束表示列或一组列可以用作表中行的唯一标识符。这要求值既是唯一的又是 NOT NULL 的。因此,以下两个表定义接受相同的数据

CREATE TABLE products (
    product_no integer UNIQUE NOT NULL,
    name text,
    price numeric
);
CREATE TABLE products (
    product_no integer PRIMARY KEY,
    name text,
    price numeric
);

主键可以跨越多列;语法类似于 UNIQUE 约束

CREATE TABLE example (
    a integer,
    b integer,
    c integer,
    PRIMARY KEY (a, c)
);

添加 PRIMARY KEY 将自动在主键中列出的列或列组上创建唯一的 B-tree 索引,并将强制将列标记为 NOT NULL

一个表最多只能有一个主键。(可以有任意数量的 UNIQUE 约束,它们与 NOT NULL 约束结合在功能上几乎是相同的,但只有一个可以被标识为主键。)关系数据库理论规定每个表都必须有一个主键。 PostgreSQL 不强制执行此规则,但通常最好遵循它。

主键对于文档目的和客户端应用程序都很有用。例如,允许修改行值的 GUI 应用程序可能需要知道表的主键才能唯一地标识行。数据库系统在声明主键后,也有各种利用它的方式;例如,主键定义了引用其表的 FOREIGN KEY 的默认目标列。

5.5.5. 外键 #

FOREIGN KEY 约束指定列(或一组列)中的值必须匹配另一个表中出现的行中的值。我们说这在两个相关表之间维护了参照完整性

假设您有前面多次使用的产品表

CREATE TABLE products (
    product_no integer PRIMARY KEY,
    name text,
    price numeric
);

我们还假设您有一个存储这些产品订单的表。我们想确保订单表只包含实际存在的产品订单。因此,我们在订单表中定义一个引用产品表的 FOREIGN KEY 约束

CREATE TABLE orders (
    order_id integer PRIMARY KEY,
    product_no integer REFERENCES products (product_no),
    quantity integer
);

现在,不可能创建 product_no 条目不为空且不存在于产品表中的订单。

在这种情况下,我们称订单表为引用表,产品表为被引用表。同样,有引用列和被引用列。

您也可以缩短上述命令

CREATE TABLE orders (
    order_id integer PRIMARY KEY,
    product_no integer REFERENCES products,
    quantity integer
);

因为在没有列列表的情况下,被引用表的主键被用作被引用列。

您可以像往常一样为 FOREIGN KEY 约束分配自己的名称。

FOREIGN KEY 也可以约束和引用一组列。像往常一样,它需要写成表约束的形式。这是一个牵强的语法示例

CREATE TABLE t1 (
  a integer PRIMARY KEY,
  b integer,
  c integer,
  FOREIGN KEY (b, c) REFERENCES other_table (c1, c2)
);

当然,约束列的数量和类型需要与被引用列的数量和类型相匹配。

有时, FOREIGN KEY 约束的另一个表与自身相同是有用的;这称为自引用 FOREIGN KEY。例如,如果您希望表中的行代表树形结构中的节点,您可以这样写

CREATE TABLE tree (
    node_id integer PRIMARY KEY,
    parent_id integer REFERENCES tree,
    name text,
    ...
);

顶层节点将具有 NULL parent_id,而非 NULL parent_id 条目将受约束以引用表中有效的行。

一个表可以有多个 FOREIGN KEY 约束。这用于实现表之间的多对多关系。假设您有关于产品和订单的表,但现在您想允许一个订单可能包含多个产品(上面的结构不允许)。您可以使用此表结构

CREATE TABLE products (
    product_no integer PRIMARY KEY,
    name text,
    price numeric
);

CREATE TABLE orders (
    order_id integer PRIMARY KEY,
    shipping_address text,
    ...
);

CREATE TABLE order_items (
    product_no integer REFERENCES products,
    order_id integer REFERENCES orders,
    quantity integer,
    PRIMARY KEY (product_no, order_id)
);

请注意,主键与最后一个表中的外键重叠。

我们知道外键不允许创建与任何产品无关的订单。但是,如果一个产品在创建了引用它的订单后被删除会怎样?SQL 也允许您处理这种情况。直观地说,我们有几个选择

  • 禁止删除被引用的产品

  • 同时删除订单

  • 其他?

为了说明这一点,让我们在上面的多对多关系示例中实施以下策略:当有人想删除仍被订单(通过 order_items)引用的产品时,我们禁止这样做。如果有人删除订单,订单项也会被删除

CREATE TABLE products (
    product_no integer PRIMARY KEY,
    name text,
    price numeric
);

CREATE TABLE orders (
    order_id integer PRIMARY KEY,
    shipping_address text,
    ...
);

CREATE TABLE order_items (
    product_no integer REFERENCES products ON DELETE RESTRICT,
    order_id integer REFERENCES orders ON DELETE CASCADE,
    quantity integer,
    PRIMARY KEY (product_no, order_id)
);

ON DELETE 动作的默认值是 ON DELETE NO ACTION;这不需要指定。这意味着允许删除被引用表中的删除操作继续进行。但是,FOREIGN KEY 约束仍然必须满足,因此此操作通常会导致错误。但是,FOREIGN KEY 约束的检查也可以延迟到事务的稍后阶段(本章未涵盖)。在这种情况下,NO ACTION 设置将允许其他命令在检查约束之前修复情况,例如通过在被引用表中插入另一个合适的行或从引用表中删除现在悬空的行。

RESTRICTNO ACTION 更严格。它阻止删除被引用的行。RESTRICT 不允许将检查推迟到事务稍后。

CASCADE 指定当被引用的行被删除时,引用它的行也将被自动删除。

还有另外两个选项:SET NULLSET DEFAULT。当被引用的行被删除时,这些会将引用行中的引用列设置为 null 或其默认值。请注意,这些不会让您免于遵守任何约束。例如,如果一个操作指定 SET DEFAULT 但默认值不满足 FOREIGN KEY 约束,操作将失败。

选择合适的 ON DELETE 动作取决于相关表代表什么类型的对象。当引用表代表被引用表所代表事物的组成部分且不能独立存在时,CASCADE 可能是合适的。如果两个表代表独立的物体,那么 RESTRICTNO ACTION 更合适;实际上想要删除两个物体的应用程序必须明确这一点并运行两个删除命令。在上面的例子中,订单项是订单的一部分,如果删除订单,它们也会被自动删除会很方便。但是产品和订单是不同的东西,因此自动删除产品可能会导致删除某些订单项可能被认为是有问题的。如果外键关系代表可选信息,则 SET NULLSET DEFAULT 操作可能合适。例如,如果产品表包含对产品经理的引用,并且产品经理条目被删除,那么将产品的产品经理设置为 null 或默认值可能很有用。

操作 SET NULLSET DEFAULT 可以接受列列表来指定要设置的列。通常, FOREIGN KEY 约束的所有列都会被设置;仅设置一个子集在某些特殊情况下很有用。考虑以下示例

CREATE TABLE tenants (
    tenant_id integer PRIMARY KEY
);

CREATE TABLE users (
    tenant_id integer REFERENCES tenants ON DELETE CASCADE,
    user_id integer NOT NULL,
    PRIMARY KEY (tenant_id, user_id)
);

CREATE TABLE posts (
    tenant_id integer REFERENCES tenants ON DELETE CASCADE,
    post_id integer NOT NULL,
    author_id integer,
    PRIMARY KEY (tenant_id, post_id),
    FOREIGN KEY (tenant_id, author_id) REFERENCES users ON DELETE SET NULL (author_id)
);

如果没有指定列,FOREIGN KEY 也会将 tenant_id 列设置为 null,但该列仍是主键的一部分,是必需的。

ON DELETE 类似,还有一个 ON UPDATE,它在被引用列被更改(更新)时调用。可能的操作是相同的,除了 SET NULLSET DEFAULT 不能指定列列表。在这种情况下,CASCADE 意味着应该将引用列的更新值复制到引用行中。 ON UPDATE NO ACTION(默认)和 ON UPDATE RESTRICT 之间也有明显的区别。前者允许更新继续进行,并且 FOREIGN KEY 约束将在更新后的状态下进行检查。后者将阻止更新运行,即使更新后的状态仍然满足约束。这可以防止将被引用的行更新为不同但比较相等的值(例如,使用不区分大小写的排序规则的字符串类型,具有不同大小写变体的字符串)。

通常,如果引用列中的任何一个为 null,引用行就不需要满足 FOREIGN KEY 约束。如果将 MATCH FULL 添加到 FOREIGN KEY 声明中,引用行只有在所有引用列都为 null 时才能逃避满足约束(因此 null 值和非 null 值的混合肯定会使 MATCH FULL 约束失败)。如果您不希望引用行能够避免满足 FOREIGN KEY 约束,请将引用列声明为 NOT NULL

FOREIGN KEY 必须引用是主键或形成唯一约束的列,或者来自非部分唯一索引的列。这意味着被引用列始终有一个索引,以便高效地查找引用行是否匹配。由于从被引用表中删除行或更新被引用列将需要扫描引用表以查找与旧值匹配的行,因此通常最好也为引用列创建索引。因为这并非总是必需的,并且有许多可用的索引选择,所以 FOREIGN KEY 约束的声明不会自动为引用列创建索引。

有关更新和删除数据的更多信息,请参阅 第 6 章。另请参阅 CREATE TABLE 的参考文档中关于 FOREIGN KEY 约束语法的说明。

5.5.6. 排斥约束 #

排斥约束确保,如果使用指定的运算符比较指定的列或表达式中的任何两行,则至少有一个运算符比较将返回 false 或 null。语法是

CREATE TABLE circles (
    c circle,
    EXCLUDE USING gist (c WITH &&)
);

另请参阅 CREATE TABLE ... CONSTRAINT ... EXCLUDE 以获取详细信息。

添加排斥约束将自动创建约束声明中指定的类型的索引。

提交更正

如果您在文档中看到任何不正确、与您对特定功能的体验不符或需要进一步阐明的内容,请使用 此表单 报告文档问题。