Redrock Postgres 搜索 英文
版本: 9.3 / 9.4 / 9.5 / 9.6 / 10 / 11 / 12 / 13 / 14 / 15 / 16 / 17

5.5. 约束 #

5.5.1. 检查约束
5.5.2. 非空约束
5.5.3. 唯一约束
5.5.4. 主键
5.5.5. 外键
5.5.6. 排他约束

数据类型是一种限制可以存储在表中的数据类型的途径。不过,对于许多应用程序而言,它们提供的约束条件过于粗略。例如,包含产品价格的列可能只接受正值。但是没有接受仅正数的标准数据类型。另一个问题是,您可能想要相对于其他列或行来限制列数据。例如,在包含产品信息的表中,针对每个产品编号只应有一行。

为此,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)
);

需要注意的是,如果检查表达式求值为真或空值,则表示满足检查约束。由于大多数表达式如果任何操作数为 null 都会求值为 null,所以它们不会阻止约束列中的空值。确保列不包含 null 值的方法是使用下一部分中描述的非空约束。

PostgreSQL 不支持引用除正在检查的新行或已更新行之外的其他表数据的 CHECK 约束。尽管违反此规则的 CHECK 约束在简单测试中似乎可以正常工作,但它无法确保数据库不会达到约束条件为 false 的状态(由于所涉及的其他行的后续更改)。这会导致数据库下载和恢复失败。即使数据库的完整状态与约束一致,但由于行未按满足约束的顺序加载,恢复也可能会失败。如果可能,请使用 UNIQUEEXCLUDEFOREIGN KEY 约束来表示跨行和跨表的限制。

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

PostgreSQL 假设 CHECK 约束的条件是不可变的,也就是说,它们对于相同的输入行始终会给出相同的结果。这个假设正是仅在插入或更新行时检查 CHECK 约束的理由,而在其他时间不检查的道理。(上面关于不引用其他表数据是该限制的特殊情况。)

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

5.5.2. 非空约束 #

非空约束仅指定一列不得采用空值。语法示例

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 树索引。无法将仅覆盖部分行的唯一性限制写为唯一约束,但可以通过创建部分索引来强制执行此类限制。

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

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

or

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

可以使用 NULLS DISTINCT 显式指定默认行为。根据 SQL 标准,唯一约束中的默认空值处理由实现定义,其他实现有不同的行为。因此,在开发旨在实现可移植性的应用程序时要小心。

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 不会强制执行此规则,但通常最好遵循它。

主键对文档目的和客户端应用程序都很有用。例如,一个允许修改行值的用户界面应用程序可能需要知道表的主键,以便能够唯一地标识行。数据库系统还将利用已声明的主键,例如,主键定义引用其表的外部键的默认目标列。

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

现在,无法创建具有未出现于产品表中的非 NULL product_no 条目的订单。

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

你还可以将上述命令缩短为

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

顶级节点将有 NULL parent_id,而非 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,则该引用行无需满足外键约束。如果在 foreign key 声明中添加了 MATCH FULL,则仅当引用行中所有引用列都为 null 时,引用行才满足约束(因此,null 值和非 null 值的混合必然不满足 MATCH FULL 约束)。如果您不想让引用行能够避开外键约束,请将引用列声明为 NOT NULL

外键必须引用是主键或形成唯一约束的列,或者是非 partial 唯一索引中的列。这意味着引用列始终有一个索引,允许高效查找引用行是否具有匹配项。由于从引用表 DELETE 一行或 UPDATE 引用列将需要扫描引用表的行,以匹配旧值,因此通常最好也为引用列编制索引。由于并不总是需要这样做,并且有很多可用于索引的方法,因此外键约束的声明不会自动在引用列中创建索引。

有关更新和删除数据的详细信息,请参阅 第 6 章。此外,请参阅 CREATE TABLE 的参考文档中对 foreign key 约束语法的描述。

5.5.6. 排除约束 #

排除约束确保如果针对指定列或表达式使用指定操作符对任意两行进行比较,那么这些操作符比较中的至少一个将返回 false 或 null。语法为

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

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

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