2025年9月25日: PostgreSQL 18 釋出!
支援的版本: 當前 (18) / 17 / 16 / 15 / 14 / 13
開發版本: 開發版
不支援的版本: 12 / 11 / 10 / 9.6 / 9.5 / 9.4 / 9.3 / 9.2 / 9.1 / 9.0 / 8.4 / 8.3 / 8.2 / 8.1 / 8.0 / 7.4 / 7.3

5.5. 約束 #

資料型別是限制表中可以儲存的資料種類的方式。然而,對於許多應用程式來說,它們提供的約束過於粗糙。例如,包含產品價格的列可能應該只接受正數。但沒有標準資料型別只接受正數。另一個問題是,您可能希望根據其他列或行來約束列資料。例如,在包含產品資訊的表中,每個產品編號應該只有一行。

為此,SQL 允許您在列和表上定義約束。約束讓您對錶中的資料擁有您想要的控制權。如果使用者嘗試在違反約束的列中儲存資料,將引發錯誤。即使值來自預設值定義,此規則也適用。

5.5.1. CHECK 約束 #

CHECK 約束是最通用的約束型別。它允許您指定某一列中的值必須滿足布林(真值)表示式。例如,要要求產品價格為正數,您可以使用

CREATE TABLE products (
    product_no integer,
    name text,
    price numeric CHECK (price > 0)
);

如您所見,約束定義出現在資料型別之後,就像預設值定義一樣。預設值和約束可以按任何順序排列。CHECK 約束由關鍵字 CHECK 後跟括號中的表示式組成。CHECK 約束表示式應涉及被約束的列,否則約束就沒有太大意義。

您也可以為約束指定單獨的名稱。這可以使錯誤訊息更清晰,並允許您在需要更改約束時引用它。語法是

CREATE TABLE products (
    product_no integer,
    name text,
    price numeric CONSTRAINT positive_price CHECK (price > 0)
);

因此,要指定命名約束,請使用關鍵字 CONSTRAINT 後跟識別符號,然後是約束定義。(如果您沒有以這種方式指定約束名稱,系統會為您選擇一個名稱。)

CHECK 約束也可以引用多個列。假設您儲存了常規價格和折扣價格,並且您想確保折扣價格低於常規價格

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

應該注意的是,如果 CHECK 約束表示式評估為 true 或 null 值,則該約束得到滿足。由於大多數表示式在任何運算元為 null 時都會評估為 null 值,因此它們不會阻止被約束列中的 null 值。要確保列不包含 null 值,可以使用下一節介紹的 NOT NULL 約束。

注意

PostgreSQL 不支援引用新行或更新行以外的表資料的 CHECK 約束。雖然違反此規則的 CHECK 約束在簡單測試中可能看起來有效,但它不能保證資料庫不會達到約束條件為 false 的狀態(由於涉及的其他行的後續更改)。這會導致資料庫轉儲和恢復失敗。即使資料庫的完整狀態與約束一致,恢復也可能失敗,因為行的載入順序不滿足約束。如果可能,請使用 UNIQUEEXCLUDEFOREIGN KEY 約束來表達跨行和跨表限制。

如果您想要在插入行時對其他行進行一次性檢查,而不是持續維護一致性保證,可以使用自定義的 觸發器 來實現。(這種方法避免了轉儲/恢復問題,因為 pg_dump 不會在恢復資料之前重新安裝觸發器,因此在轉儲/恢復期間不會強制執行檢查。)

注意

PostgreSQL 假定 CHECK 約束的條件是不可變的,也就是說,對於相同的輸入行,它們總是產生相同的結果。這個假設證明了為什麼只在插入或更新行時檢查 CHECK 約束,而不是在其他時候。(上面關於不引用其他表資料的警告實際上是此限制的一個特殊情況。)

一個常見的破壞此假設的方法是在 CHECK 表示式中引用使用者定義的函式,然後更改該函式的行為。PostgreSQL 不禁止這樣做,但它不會注意到表中是否存在違反 CHECK 約束的行。這會導致後續的資料庫轉儲和恢復失敗。處理此類更改的推薦方法是刪除約束(使用 ALTER TABLE),調整函式定義,然後重新新增約束,從而針對所有錶行重新檢查它。

5.5.2. NOT NULL 約束 #

NOT NULL 約束只是指定列不得為 null 值。語法示例

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

也可以指定顯式的約束名稱,例如

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

NOT NULL 約束通常寫成列約束。將其寫成表約束的語法是

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

但這種語法不是標準的,主要供 pg_dump 使用。

NOT NULL 約束在功能上等同於建立 CHECK 約束 CHECK (column_name IS NOT NULL),但在 PostgreSQL 中,建立顯式的 NOT NULL 約束效率更高。

當然,一列可以有多個約束。只需將約束一個接一個地寫即可

CREATE TABLE products (
    product_no integer NOT NULL,
    name text NOT NULL,
    price numeric NOT NULL CHECK (price > 0)
);

順序無關緊要。它不一定決定約束檢查的順序。

但是,一列最多隻能有一個顯式的 NOT NULL 約束。

NOT NULL 約束有一個反義:NULL 約束。這並不意味著列必須為 null,這肯定是沒有用的。相反,它只是選擇列可以為 null 的預設行為。NULL 約束不在 SQL 標準中,不應在可移植應用程式中使用。(它僅在 PostgreSQL 中新增以相容某些其他資料庫系統。)然而,一些使用者喜歡它,因為它使得在指令碼檔案中切換約束變得容易。例如,您可以從以下開始

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

然後將 NOT 關鍵字插入到需要的地方。

提示

在大多數資料庫設計中,大多數列都應該標記為 NOT NULL。

5.5.3. UNIQUE 約束 #

UNIQUE 約束確保列或一組列中包含的資料在表中的所有行中都是唯一的。語法是

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

當寫成列約束時,以及

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

當寫成表約束時。

要為一組列定義 UNIQUE 約束,請將其寫成表約束,並將列名用逗號分隔

CREATE TABLE example (
    a integer,
    b integer,
    c integer,
    UNIQUE (a, c)
);

這指定了所指示列中的值組合在整個表中是唯一的,儘管任何一個列都不需要(通常也不是)是唯一的。

您可以像往常一樣為 UNIQUE 約束分配自己的名稱

CREATE TABLE products (
    product_no integer CONSTRAINT must_be_different UNIQUE,
    name text,
    price numeric
);

新增 UNIQUE 約束將自動在約束中列出的列或列組上建立一個唯一的 B-tree 索引。無法透過 UNIQUE 約束來編寫僅覆蓋部分行的唯一性限制,但可以透過建立唯一的 部分索引 來強制執行此類限制。

通常,如果表中有多行中約束中包含的所有列的值都相等,則 UNIQUE 約束將違反。預設情況下,在此比較中,兩個 null 值不被視為相等。這意味著即使存在 UNIQUE 約束,也可以儲存包含至少一個約束列中的 null 值的重複行。可以透過新增 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 標準規定 UNIQUE 約束中的預設 null 處理是實現定義的,並且其他實現具有不同的行為。因此,在開發旨在可移植的應用程式時要小心。

5.5.4. 主鍵 #

PRIMARY KEY 約束表示列或一組列可以用作表中行的唯一識別符號。這要求值既是唯一的又是 NOT NULL 的。因此,以下兩個表定義接受相同的資料

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

主鍵可以跨越多列;語法類似於 UNIQUE 約束

CREATE TABLE example (
    a integer,
    b integer,
    c integer,
    PRIMARY KEY (a, c)
);

新增 PRIMARY KEY 將自動在主鍵中列出的列或列組上建立唯一的 B-tree 索引,並將強制將列標記為 NOT NULL

一個表最多隻能有一個主鍵。(可以有任意數量的 UNIQUE 約束,它們與 NOT NULL 約束結合在功能上幾乎是相同的,但只有一個可以被標識為主鍵。)關係資料庫理論規定每個表都必須有一個主鍵。 PostgreSQL 不強制執行此規則,但通常最好遵循它。

主鍵對於文件目的和客戶端應用程式都很有用。例如,允許修改行值的 GUI 應用程式可能需要知道表的主鍵才能唯一地標識行。資料庫系統在宣告主鍵後,也有各種利用它的方式;例如,主鍵定義了引用其表的 FOREIGN KEY 的預設目標列。

5.5.5. 外部索引鍵 #

FOREIGN KEY 約束指定列(或一組列)中的值必須匹配另一個表中出現的行中的值。我們說這在兩個相關表之間維護了參照完整性

假設您有前面多次使用的產品表

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

我們還假設您有一個儲存這些產品訂單的表。我們想確保訂單表只包含實際存在的產品訂單。因此,我們在訂單表中定義一個引用產品表的 FOREIGN KEY 約束

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

因為在沒有列列表的情況下,被引用表的主鍵被用作被引用列。

您可以像往常一樣為 FOREIGN KEY 約束分配自己的名稱。

FOREIGN KEY 也可以約束和引用一組列。像往常一樣,它需要寫成表約束的形式。這是一個牽強的語法示例

CREATE TABLE t1 (
  a integer PRIMARY KEY,
  b integer,
  c integer,
  FOREIGN KEY (b, c) REFERENCES other_table (c1, c2)
);

當然,約束列的數量和型別需要與被引用列的數量和型別相匹配。

有時, FOREIGN KEY 約束的另一個表與自身相同是有用的;這稱為自引用 FOREIGN KEY。例如,如果您希望表中的行代表樹形結構中的節點,您可以這樣寫

CREATE TABLE tree (
    node_id integer PRIMARY KEY,
    parent_id integer REFERENCES tree,
    name text,
    ...
);

頂層節點將具有 NULL parent_id,而非 NULL parent_id 條目將受約束以引用表中有效的行。

一個表可以有多個 FOREIGN KEY 約束。這用於實現表之間的多對多關係。假設您有關於產品和訂單的表,但現在您想允許一個訂單可能包含多個產品(上面的結構不允許)。您可以使用此表結構

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

ON DELETE 動作的預設值是 ON DELETE NO ACTION;這不需要指定。這意味著允許刪除被引用表中的刪除操作繼續進行。但是,FOREIGN KEY 約束仍然必須滿足,因此此操作通常會導致錯誤。但是,FOREIGN KEY 約束的檢查也可以延遲到事務的稍後階段(本章未涵蓋)。在這種情況下,NO ACTION 設定將允許其他命令在檢查約束之前修復情況,例如透過在被引用表中插入另一個合適的行或從引用表中刪除現在懸空的行。

RESTRICTNO ACTION 更嚴格。它阻止刪除被引用的行。RESTRICT 不允許將檢查推遲到事務稍後。

CASCADE 指定當被引用的行被刪除時,引用它的行也將被自動刪除。

還有另外兩個選項:SET NULLSET DEFAULT。當被引用的行被刪除時,這些會將引用行中的引用列設定為 null 或其預設值。請注意,這些不會讓您免於遵守任何約束。例如,如果一個操作指定 SET DEFAULT 但預設值不滿足 FOREIGN KEY 約束,操作將失敗。

選擇合適的 ON DELETE 動作取決於相關表代表什麼型別的物件。當引用表代表被引用表所代表事物的組成部分且不能獨立存在時,CASCADE 可能是合適的。如果兩個表代表獨立的物體,那麼 RESTRICTNO ACTION 更合適;實際上想要刪除兩個物體的應用程式必須明確這一點並執行兩個刪除命令。在上面的例子中,訂單項是訂單的一部分,如果刪除訂單,它們也會被自動刪除會很方便。但是產品和訂單是不同的東西,因此自動刪除產品可能會導致刪除某些訂單項可能被認為是有問題的。如果外部索引鍵關係代表可選資訊,則 SET NULLSET DEFAULT 操作可能合適。例如,如果產品表包含對產品經理的引用,並且產品經理條目被刪除,那麼將產品的產品經理設定為 null 或預設值可能很有用。

操作 SET NULLSET DEFAULT 可以接受列列表來指定要設定的列。通常, FOREIGN KEY 約束的所有列都會被設定;僅設定一個子集在某些特殊情況下很有用。考慮以下示例

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

如果沒有指定列,FOREIGN KEY 也會將 tenant_id 列設定為 null,但該列仍是主鍵的一部分,是必需的。

ON DELETE 類似,還有一個 ON UPDATE,它在被引用列被更改(更新)時呼叫。可能的操作是相同的,除了 SET NULLSET DEFAULT 不能指定列列表。在這種情況下,CASCADE 意味著應該將引用列的更新值複製到引用行中。 ON UPDATE NO ACTION(預設)和 ON UPDATE RESTRICT 之間也有明顯的區別。前者允許更新繼續進行,並且 FOREIGN KEY 約束將在更新後的狀態下進行檢查。後者將阻止更新執行,即使更新後的狀態仍然滿足約束。這可以防止將被引用的行更新為不同但比較相等的值(例如,使用不區分大小寫的排序規則的字串型別,具有不同大小寫變體的字串)。

通常,如果引用列中的任何一個為 null,引用行就不需要滿足 FOREIGN KEY 約束。如果將 MATCH FULL 新增到 FOREIGN KEY 宣告中,引用行只有在所有引用列都為 null 時才能逃避滿足約束(因此 null 值和非 null 值的混合肯定會使 MATCH FULL 約束失敗)。如果您不希望引用行能夠避免滿足 FOREIGN KEY 約束,請將引用列宣告為 NOT NULL

FOREIGN KEY 必須引用是主鍵或形成唯一約束的列,或者來自非部分唯一索引的列。這意味著被引用列始終有一個索引,以便高效地查詢引用行是否匹配。由於從被引用表中刪除行或更新被引用列將需要掃描引用表以查詢與舊值匹配的行,因此通常最好也為引用列建立索引。因為這並非總是必需的,並且有許多可用的索引選擇,所以 FOREIGN KEY 約束的宣告不會自動為引用列建立索引。

有關更新和刪除資料的更多資訊,請參閱 第 6 章。另請參閱 CREATE TABLE 的參考文件中關於 FOREIGN KEY 約束語法的說明。

5.5.6. 排斥約束 #

排斥約束確保,如果使用指定的運算子比較指定的列或表示式中的任何兩行,則至少有一個運算子比較將返回 false 或 null。語法是

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

另請參閱 CREATE TABLE ... CONSTRAINT ... EXCLUDE 以獲取詳細資訊。

新增排斥約束將自動建立約束宣告中指定的型別的索引。

提交更正

如果您在文件中看到任何不正確、與您對特定功能的體驗不符或需要進一步闡明的內容,請使用 此表單 報告文件問題。