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)
);
需要注意的是,当检查表达式的值为真或空值时,检查约束就被视为满足。由于当任一操作数为空时,大多数表达式都会计算为空值,所以检查约束不会阻止受约束列中出现空值。要确保某列不包含空值,可以使用下一节介绍的非空约束。
注意
PostgreSQL 不支持引用除了正在检查的新行或更新行之外的表数据的 CHECK 约束。虽然违反此规则的 CHECK 约束在简单测试中可能有效,但无法保证数据库不会达到约束条件为假的状态(由于其他行的后续更改)。这将导致数据库转储和恢复失败。即使完整的数据库状态与约束一致,恢复也可能失败,因为行未按满足约束的顺序加载。如果可能的话,使用 UNIQUE、EXCLUDE 或
FOREIGN 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 (,但在 PostgreSQL 中创建显式的非空约束效率更高。缺点是无法为以这种方式创建的非空约束显式指定名称。
column_name IS NOT NULL)
当然,一个列可以同时拥有多个约束;只要把它们依次写出来即可:
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
);
或者
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 并未强制要求这一点,但通常最好遵循它。
主键对于文档和客户端应用都是有用的。例如,一个允许修改行值的 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 值(非空)的订单。
我们说在这种情况下,订单表是引用表而产品表是被引用表。相应地,也有引用和被引用列的说法。
我们也可以把上述命令简写为:
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 NULL 和 SET DEFAULT。它们会在被引用行删除时,分别把引用行中的引用列设为空值或其默认值。注意,这并不能让你免于遵守其他约束。例如,如果某个动作指定了 SET DEFAULT,但默认值本身不满足外键约束,则该操作仍会失败。
适当选择 ON DELETE 操作取决于相关表所代表的对象类型。当引用表所表示的对象是被引用表所表示对象的组成部分,并且不能独立存在时,CASCADE 可能是合适的选择。如果这两个表代表独立的对象,则 RESTRICT 或 NO ACTION 更合适;实际上想要删除这两个对象的应用程序必须明确这一点,并运行两个删除命令。在上面的示例中,订单项目是订单的一部分,如果删除订单,则自动删除它们是方便的。但产品和订单是不同的东西,因此使产品的删除自动导致某些订单项目的删除可能被认为是有问题的。当外键关系表示可选信息时,SET NULL 或 SET DEFAULT 操作可能是合适的。例如,如果产品表包含对产品经理的引用,而产品经理条目被删除,那么把产品的产品经理设置为空值或默认值可能是有用的。
操作 SET NULL 和 SET 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 列设为空值,但该列仍然是主键的一部分。
与 ON DELETE 类似,还有 ON UPDATE,当被引用列发生变化(更新)时就会触发。可用动作与 ON DELETE 相同,只是 SET NULL 和 SET DEFAULT 不能指定列列表。在这种情况下,CASCADE 表示应把被引用列更新后的值复制到引用行中。
正常情况下,如果一个引用行中至少有一个引用列为空,则它不需要满足外键约束。如果在外键定义中加入了 MATCH FULL,一个引用行只有在它的所有引用列为空时才不需要满足外键约束(因此空和非空值的混合肯定会导致 MATCH FULL 约束失败)。如果不希望引用行能够避开外键约束,将引用列声明为 NOT NULL。
外键必须引用这样的列:这些列要么是主键,要么形成唯一约束,要么来自一个非部分唯一索引。这意味着被引用列总是有索引,从而能高效地查找引用行是否存在匹配项。由于从被引用表中 DELETE 行或 UPDATE 被引用列时,需要扫描引用表以查找匹配旧值的行,因此通常也建议为引用列建立索引。由于这并不总是必需,而且索引方式也有很多种,因此声明外键约束时不会自动在引用列上创建索引。
更多关于更新和删除数据的信息请见第 6 章。外键约束的语法描述请参考 CREATE TABLE。
5.5.6. 排他约束 #
排他约束保证:对任意两行,若对指定列或表达式使用指定操作符进行比较,则这些操作符比较中至少有一个会返回假或空值。语法如下:
CREATE TABLE circles (
c circle,
EXCLUDE USING gist (c WITH &&)
);
详见 CREATE
TABLE ... CONSTRAINT ... EXCLUDE。
增加一个排他约束会自动创建约束声明中指定类型的索引。
报告文档问题
阅读 上游文档. 通过 PostgreSQL 文档反馈表单.