資料類型是一種限制可以儲存在表格中的資料種類的方法。但是,對於許多應用程式來說,它們提供的約束太過粗略。例如,包含產品價格的欄位可能只應接受正數值。但是沒有接受正數值的標準資料類型。另一個問題是您可能希望針對其他欄位或列約束欄位資料。例如,在包含產品資訊的表格中,每個產品編號應該只有一列。
為此,SQL 允許您在欄位和表格上定義約束。約束讓您可以隨心所欲地控制表格中的資料。如果使用者嘗試將資料儲存在違反約束的欄位中,則會引發錯誤。即使該值來自預設值定義,也適用此規則。
檢查約束條件是最通用的約束條件類型。它允許您指定特定欄位中的值必須滿足布林(真值)運算式。例如,要要求正數產品價格,您可以使用
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)
);
應注意的是,如果檢查運算式評估為 true 或 null 值,則滿足檢查約束條件。由於如果任何運算元為 null,則大多數運算式將評估為 null 值,因此它們不會阻止受約束欄位中的 null 值。要確保欄位不包含 null 值,可以使用下一節中描述的 not-null 約束條件。
PostgreSQL 不支援參考表格資料的 CHECK
約束條件,但正在檢查的新增或更新列除外。雖然違反此規則的 CHECK
約束條件可能在簡單的測試中看起來有效,但它不能保證資料庫不會達到約束條件為 false 的狀態(由於涉及的其他列的後續變更)。這將導致資料庫轉儲和還原失敗。即使完整資料庫狀態與約束條件一致,還原也可能失敗,因為列未以滿足約束條件的順序載入。如果可能,請使用 UNIQUE
、EXCLUDE
或 FOREIGN KEY
約束條件來表達跨列和跨表格的限制。
如果您希望的是在新增列時,針對其他列進行一次性的檢查,而不是持續維護一致性的保證,則可以使用自訂的觸發器來實作。(這種方法避免了傾印/還原的問題,因為 pg_dump 在還原資料之後才會重新安裝觸發器,因此在傾印/還原期間不會強制執行檢查。)
PostgreSQL 假設 CHECK
約束的條件是不可變的,也就是說,對於相同的輸入列,它們總是會給出相同的結果。這個假設是允許僅在插入或更新列時檢查 CHECK
約束,而不是在其他時間檢查的原因。(上面關於不要引用其他表格資料的警告實際上是這個限制的一個特殊情況。)
打破這個假設的一個常見方法是在 CHECK
表達式中引用使用者定義的函數,然後更改該函數的行為。PostgreSQL 不禁止這樣做,但如果表格中現在有違反 CHECK
約束的列,它不會注意到。這會導致後續的資料庫傾印和還原失敗。建議處理此類變更的方法是刪除約束 (使用 ALTER TABLE
),調整函數定義,然後重新新增約束,從而針對所有表格列重新檢查它。
非空值約束僅僅指定一個欄位不能採用空值。語法範例:
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
關鍵字。
在大多數資料庫設計中,大多數欄位都應該標記為非空值。
唯一性約束確保欄位或一組欄位中包含的資料在表格的所有列中都是唯一的。語法是:
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 標準,唯一性約束中的預設空值處理是實現定義的,並且其他實現具有不同的行為。因此,在開發旨在可移植的應用程式時要小心。
主鍵約束指示欄位或一組欄位可以用作表格中列的唯一識別符。這要求值既是唯一的又是非空的。因此,以下兩個表格定義接受相同的資料:
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 應用程式可能需要知道表格的主鍵,才能唯一地識別列。資料庫系統還以各種方式利用已宣告的主鍵;例如,主鍵定義了引用其表格的外鍵的預設目標欄位。
外鍵約束指定欄位(或一組欄位)中的值必須與另一個表格的某些列中出現的值相符。我們說這維護了兩個相關表格之間的參考完整性。
假設您有我們已經多次使用的 product 表格:
CREATE TABLE products ( product_no integer PRIMARY KEY, name text, price numeric );
我們也假設您有一個表格儲存這些產品的訂單。我們想要確保 orders 表格僅包含實際存在的產品的訂單。因此,我們在 orders 表格中定義一個外鍵約束,該約束引用 products 表格:
CREATE TABLE orders (
order_id integer PRIMARY KEY,
product_no integer REFERENCES products (product_no),
quantity integer
);
現在不可能建立在 products 表格中沒有出現的非 NULL product_no
條目的訂單。
我們說在這種情況下,orders 表格是參考表格,而 products 表格是被參考表格。同樣,存在參考欄位和被參考欄位。
您也可以將上述命令縮短為:
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, ... );
頂層節點會有 NULL 的 parent_id
,而非 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
。這些會導致參照列中的參照欄位被設定為 null 或它們的預設值(分別),當被參照的列被刪除時。請注意,這些並不能免除你遵守任何約束。例如,如果一個動作指定 SET DEFAULT
但預設值不滿足外鍵約束,則操作將失敗。
選擇適當的 ON DELETE
動作取決於相關表格代表的物件種類。當參照表格代表被參照表格所代表物件的組件,且無法獨立存在時,CASCADE
可能適合。如果這兩個表格代表獨立的物件,那麼 RESTRICT
或 NO ACTION
更合適;實際上想要刪除兩個物件的應用程式必須明確地執行此操作並運行兩個刪除命令。在上面的範例中,訂單項目是訂單的一部分,如果它們在刪除訂單時自動刪除,那就很方便。但產品和訂單是不同的東西,因此讓刪除產品自動導致刪除某些訂單項目可能被認為是有問題的。如果外鍵關係代表可選資訊,則動作 SET NULL
或 SET DEFAULT
可能適用。例如,如果產品表格包含對產品經理的參照,並且產品經理項目被刪除,那麼將產品的產品經理設定為 null 或預設值可能很有用。
動作 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
設定為 null,但該欄位仍然是主鍵的一部分,因此是必需的。
與 ON DELETE
類似,還有 ON UPDATE
,它在被參照的欄位被更改(更新)時被呼叫。可能的動作是相同的,除了無法為 SET NULL
和 SET DEFAULT
指定欄位列表。在這種情況下,CASCADE
意味著被參照欄位的更新值應複製到參照列中。
通常,如果任何參照欄位為 null,則參照列不需要滿足外鍵約束。如果將 MATCH FULL
添加到外鍵宣告中,則只有當其所有參照欄位都為 null 時,參照列才能避免滿足約束(因此,null 和非 null 值的混合保證會使 MATCH FULL
約束失敗)。如果你不希望參照列能夠避免滿足外鍵約束,請將參照欄位宣告為 NOT NULL
。
外鍵必須參照作為主鍵或形成唯一約束的欄位,或者是非部分唯一索引中的欄位。這意味著被參照的欄位始終具有索引,以便可以有效地查找參照列是否具有匹配項。由於從被參照表格中 DELETE
一列或 UPDATE
被參照欄位將需要掃描參照表格以查找與舊值匹配的列,因此索引參照欄位通常是一個好主意。由於這並非總是需要,並且有很多關於如何索引的選擇,因此外鍵約束的宣告不會自動在參照欄位上建立索引。
有關更新和刪除資料的更多資訊,請參閱 第 6 章。另請參閱 CREATE TABLE 的參考文件中外鍵約束語法的描述。
排除約束確保如果使用指定的運算符在指定的欄位或表達式上比較任何兩列,則至少其中一個運算符比較將返回 false 或 null。語法是:
CREATE TABLE circles ( c circle, EXCLUDE USING gist (c WITH &&) );
另請參閱 CREATE TABLE ... CONSTRAINT ... EXCLUDE
以了解詳細資訊。
新增排除約束將自動建立約束宣告中指定的類型的索引。
如果您在文件中發現任何不正確、與您使用特定功能的經驗不符或需要進一步澄清的地方,請使用此表單報告文件問題。