支持的版本: 当前 (17) / 16 / 15 / 14 / 13
开发版本: devel
不支持的版本: 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. 检查约束 #

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

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

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

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

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

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

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

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)
);

应该注意的是,如果检查表达式的计算结果为 true 或 null 值,则满足检查约束。由于如果任何操作数为 null,大多数表达式都将计算为 null 值,因此它们不会阻止受约束的列中的 null 值。为确保列不包含 null 值,可以使用下一节中描述的非空约束。

注意

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

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

注意

PostgreSQL 假定 CHECK 约束的条件是不可变的,也就是说,对于相同的输入行,它们将始终给出相同的结果。这种假设证明了仅在插入或更新行时检查 CHECK 约束,而不是在其他时间进行检查。(上面关于不引用其他表数据的警告实际上是此限制的一个特例。)

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

5.5.2. 非空约束 #

非空约束只是指定列不得采用 null 值。一个语法示例

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

非空约束始终写为列约束。非空约束在功能上等效于创建检查约束 CHECK (column_name IS NOT NULL),但在 PostgreSQL 中,创建显式非空约束效率更高。缺点是您无法为以这种方式创建的非空约束指定显式名称。

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

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

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

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

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

开始,然后在需要的位置插入 NOT 关键字。

提示

在大多数数据库设计中,大多数列都应标记为非空。

5.5.3. 唯一约束 #

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

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

当写为列约束时,和

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

当写为表约束时。

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

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

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

您可以按通常的方式为唯一约束分配自己的名称

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

添加唯一约束将在约束中列出的列或列组上自动创建一个唯一的 B 树索引。仅覆盖某些行的唯一性限制不能写为唯一约束,但可以通过创建唯一的部分索引来强制执行此类限制。

通常,如果表中有多行在约束中包含的所有列的值都相等,则违反唯一约束。默认情况下,在此比较中,两个 null 值不被视为相等。这意味着即使存在唯一约束,也可以存储在至少一个受约束的列中包含 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 标准,唯一约束中的默认 null 处理是实现定义的,并且其他实现具有不同的行为。因此,在开发旨在可移植的应用程序时要小心。

5.5.4. 主键 #

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

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
);

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

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

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

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

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

5.5.5. 外键 #

外键约束指定列(或一组列)中的值必须与另一个表的某些行中出现的值匹配。我们说这维护了两个相关表之间的引用完整性

假设您拥有我们之前多次使用的产品表

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

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

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

现在,不可能创建 product_no 条目为非 NULL 且未出现在产品表中的订单。

在这种情况下,我们说订单表是引用表,产品表是被引用表。类似地,也有引用列和被引用列。

您还可以将以上命令缩短为

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

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

您可以像往常一样,为外键约束指定自己的名称。

外键还可以约束和引用一组列。与往常一样,它需要以表约束的形式编写。这是一个人为的语法示例

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

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

有时,外键约束的另一个表与同一表很有用;这被称为自引用外键。例如,如果您想让表的行表示树结构的节点,您可以编写

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

顶层节点的 parent_id 为 NULL,而非 NULL 的 parent_id 条目将被约束为引用表的有效行。

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

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)
);

限制和级联删除是两个最常见的选择。RESTRICT 阻止删除被引用的行。NO ACTION 表示如果在检查约束时仍存在任何引用行,则会引发错误;如果您不指定任何内容,这是默认行为。(这两个选择之间的本质区别在于 NO ACTION 允许将检查推迟到事务的稍后时间,而 RESTRICT 不允许。)CASCADE 指定当删除被引用的行时,也应自动删除引用它的行。还有两个其他选项:SET NULLSET DEFAULT。当删除被引用的行时,这些选项会导致引用行中的引用列设置为 null 或其默认值。请注意,这些选项并不会免除您遵守任何约束。例如,如果某个操作指定 SET DEFAULT,但默认值不满足外键约束,则该操作将失败。

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

SET NULLSET DEFAULT 操作可以采用列列表来指定要设置的列。通常,设置外键约束的所有列;仅设置子集在某些特殊情况下很有用。考虑以下示例

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)
);

如果没有指定列,外键还会将列 tenant_id 设置为 null,但该列仍然是主键的一部分。

ON DELETE 类似,还有 ON UPDATE,它在更改(更新)被引用列时调用。可能的动作是相同的,只是不能为 SET NULLSET DEFAULT 指定列列表。在这种情况下,CASCADE 表示被引用列的更新值应复制到引用行中。

通常,如果引用行的任何引用列为 null,则该引用行不必满足外键约束。如果将 MATCH FULL 添加到外键声明中,则只有当其所有引用列都为 null 时,引用行才不必满足约束(因此,null 和非 null 值的混合肯定会使 MATCH FULL 约束失败)。如果您不希望引用行能够避免满足外键约束,请将引用列声明为 NOT NULL

外键必须引用作为主键或构成唯一约束的列,或者来自非部分唯一索引的列。这意味着被引用列始终具有索引,以便能够高效查找引用行是否存在匹配项。由于从被引用表 DELETE 行或 UPDATE 被引用列将需要扫描引用表以查找与旧值匹配的行,因此索引引用列通常也是一个好主意。由于并非总是需要这样做,并且在如何编制索引方面有许多选择,因此外键约束的声明不会自动在引用列上创建索引。

有关更新和删除数据的更多信息,请参见 第 6 章。另请参见 CREATE TABLE 的参考文档中有关外键约束语法的描述。

5.5.6. 排除约束 #

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

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

有关详细信息,另请参见 CREATE TABLE ... CONSTRAINT ... EXCLUDE

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

提交更正

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