选择 打开 改范围 完整检索页
受支持版本: 当前版本 (18) / 17 / 16 / 15 / 14
开发版本: 19 / devel
不受支持的版本: 13 / 12 / 11 / 10
当前 PostgreSQL 版本不在支持生命周期内。
您可以参阅当前版本的对应页面,或其他在上面列出的活跃大版本。

5.4. 约束 #

数据类型是一种限制能够存储在表中数据类别的方法。但是对于很多应用来说,它们提供的约束太粗糙。例如,一个包含产品价格的列应该只接受正值。但是没有任何一种标准数据类型只接受正值。另一个问题是我们可能需要根据其他列或行来约束一个列中的数据。例如,在一个包含产品信息的表中,对于每个产品编号应该只有一行。

为此,SQL 允许我们在列和表上定义约束。约束让我们能够按照需要控制表中的数据。如果用户试图在列中存储违反约束的数据,就会报错。即使该值来自默认值定义,这条规则也同样适用。

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

需要注意的是,当检查表达式的值为真或空值时,检查约束就被视为满足。由于当任一操作数为空时,大多数表达式都会计算为空值,所以检查约束不会阻止受约束列中出现空值。要确保某列不包含空值,可以使用下一节介绍的非空约束。

Note

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

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

Note

PostgreSQL假定CHECK约束的条件是不可变的,也就是说,对于同一输入行它们始终会给出相同的结果。 正是这个假设,才使得只需要在插入或更新行时检查CHECK约束,而不必在其他时间检查。 (上面关于不引用其他表数据的警告实际上是此限制的特殊情况。)

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

5.4.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关键字。

Tip

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

5.4.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-树索引。只覆盖某些行的唯一性限制不能写成唯一约束,但可以通过创建唯一的部分索引来强制这种限制。

通常情况下,如果表中有多行,且约束所包含的所有列的值都相等,就违反了唯一约束。不过,在这种比较中,两个空值从不被视为相等。这意味着即使存在唯一约束,也可以存储重复行,只要其中至少一个受约束列含有空值。此行为符合 SQL 标准,但我们听说其他 SQL 数据库可能不遵循此规则。因此,开发需要可移植性的应用时要小心。

5.4.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.4.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值(非空)的订单。

我们说在这种情况下,订单表是引用表而产品表是被引用表。相应地,也有引用和被引用列的说法。

我们也可以把上述命令简写为:

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。它们会在被引用行删除时,分别把引用行中的引用列设为空值或其默认值。注意,这并不能让你免于遵守其他约束。例如,如果某个动作指定了SET DEFAULT,但默认值本身不满足外键约束,则该操作仍会失败。

ON DELETE类似,还有ON UPDATE,当被引用列发生变化(更新)时就会触发。可用动作相同。在这种情况下,CASCADE表示应把被引用列更新后的值复制到引用行中。

正常情况下,如果引用行的任意一个引用列为空,它就不需要满足外键约束。如果在外键声明中加入MATCH FULL,引用行只有在所有引用列都为空时才不需要满足约束(因此空值和非空值混合的情况必定不满足MATCH FULL约束)。如果不希望引用行能够避开外键约束,应将引用列声明为NOT NULL

外键必须引用这样的列:这些列要么是主键,要么形成唯一约束,要么来自一个非部分唯一索引。这意味着被引用列总是有索引,从而能高效地查找引用行是否存在匹配项。由于从被引用表中DELETE行或UPDATE被引用列时,需要扫描引用表以查找匹配旧值的行,因此通常也建议为引用列建立索引。由于这并不总是必需,而且索引方式也有很多种,因此声明外键约束时不会自动在引用列上创建索引。

更多关于更新和删除数据的信息请见Chapter 6。外键约束的语法描述请参考CREATE TABLE

5.4.6. 排他约束 #

排他约束保证:对任意两行,若对指定列或表达式使用指定操作符进行比较,则这些操作符比较中至少有一个会返回假或空值。语法如下:

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

详见CREATE TABLE ... CONSTRAINT ... EXCLUDE

增加一个排他约束会自动创建约束声明中指定类型的索引。