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,如果你不聲明任何東西,那麼它就是 缺省的。在刪除主鍵的時候,在外鍵字段上的動作還有兩個選項: SET NULLSET DEFAULT。 請注意這些選項並不能讓你逃脫被觀察和約束的境地。 比如,如果一個動作聲明 SET DEFAULT,但是缺省值 並不能滿足外鍵,那麼主鍵的刪除動作就會失敗。

類似 ON DELETE,還有 ON UPDATE 選項,它是在主鍵被修改(更新)的時候調用的。可用的動作是一樣的。

有關更新和刪除數據的更多信息可以在 Chapter 6 裡找到。

最後,我們應該說明的是,一個外鍵必須要麼引用一個主鍵,要麼引用 一個唯一約束。如果外鍵引用了一個唯一約束,那麼在如何匹配空值這個 問題上還有一些其它的可能性。這些東西都在 CREATE TABLE 裡的 CREATE TABLE 中解釋。