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

头两个约束看上去应该很面熟。第三个使用了一个新的语法。 它没有附着在某个字段上,它在逗号分隔的字段列表中是以一个独立行的形式出现的。 字段定义和这些约束定义可以以混合的顺序列出。

我们说头两个约束是字段约束,而第三个是表约束,因为它和字段定义分开写。 字段约束也可以写成表约束,而反过来很可能不行。上面的例子也可以这么写

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

这只是风格的不同。

我们还要知道一个检查约束在表达式计算出真或者空值的时候是得到满足的。 因为大多数表达式在其中一个操作数是空的时候都会得出空值, 所以这些约束不能在受约数字段上禁止空值。要确保一个字段不包含空值,我们可以使用下一节介绍的非空约束。

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 约束。这个约束并不意味着该字段必须是空,因为这样的字段也没啥用。 它只是定义了该字段可以为空的这个简单行为。在 SQL 标准里没有定义 NULL 约束, 因此不应该在可移植的应用中使用它。 (我们在 PostgreSQL 里面增加这个约束只是为了和其它数据库系统兼容。) 不过,有些用户喜欢它,因为这个约束可以让他们很容易在脚本文件里切换约束。比如,你可以从下面这样开始

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

然后在需要的时候插入 NOT 关键字。

提示: 在大多数数据库设计里,主要的字段都应该标记为非空。

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

通常,如果在表中有两行或更多行,而这些行中包含在唯一约束里面的那几个字段都相等,那么就算违反了唯一约束。 但是在这种比较中,空值是认为不相等的。这就意味着,在多字段唯一约束的情况下, 如果在至少一个字段上存在空值,那么这样的行我们可以存储无限多个。 这种行为遵循 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)
);

主键表示一个字段或者是若干个字段的组合可以用于表中的数据行的唯一标识。 (这是定义一个主键的直接结果。请注意一个唯一约束实际上并不能提供一个唯一表示,因为它不排除空值。) 这个功能对文档目的和客户应用都很有用。比如,一个可以修改行数值的 GUI 应用可能需要知道一个表的主键才能唯一地标识一个行。

一个表最多可以有一个主键(但是它可以有多个唯一和非空约束)。 关系型数据库理论告诉我们,每个表都必须有一个主键。PostgreSQL 并不强制这个规则,但我们最好还是遵循它。

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 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 选项,它是在被引用字段修改(更新)的时候调用的。可用的动作是一样的。

有关更新和删除数据的更多信息可以在 Chapter 6 里找到。

最后,我们应该说明的是,一个外键必须要么引用一个主键,要么引用一个唯一约束。 如果外键引用了一个唯一约束,那么在如何匹配空值这个问题上还有一些其它的可能性。 这些东西都在 CREATE TABLE 里的 CREATE TABLE 中解释。