什麼是約束
在定義表或列時,可以為數據附加校驗或強制規則的,這些規則稱為約束。
數據類型本身只能提供較粗粒度的限制,例如 numeric 無法限定只能為正數。更具體的規則(如唯一性、取值範圍等)需要通過約束來實現。
約束用於保障數據完整性。當插入或默認值違反約束時,PostgreSQL 會直接報錯。
本質上,約束是數據庫層面強制執行的數據規則。一旦缺失或使用不當,數據問題往往會悄然積累,並最終演變為難以排查的缺陷。
pg_constraint 系統目錄
從內部實現來看,PostgreSQL 中的所有約束,都會以記錄的形式存儲在 pg_constraint 系統目錄中。
🗄️ 什麼是系統目錄(Catalog)
系統目錄是 PostgreSQL 用來保存元數據的系統表。用户表存儲業務數據,而系統目錄則記錄“數據庫自身的信息”,例如表、列、索引、約束等。
除
pg_constraint之外,常見的系統目錄還包括:
pg_class:所有關係對象(表、索引、視圖等)pg_attribute:表的列信息pg_type:數據類型(含域和自定義類型)pg_namespace:模式(schema)pg_index:索引相關信息(其餘信息主要在pg_class中)pg_proc:函數、過程及聚合函數這些表都位於
pg_catalog模式中,該模式在search_path中默認優先,因此通常無需顯式指定。
pg_constraint用於存儲表上的 CHECK、NOT NULL、主鍵、唯一、外鍵和排他約束。
需要注意的是,在 PostgreSQL 18 之前,表上的 NOT NULL 約束並不存儲在 pg_constraint 中,而是記錄在 pg_attribute;從 PostgreSQL 18 開始,NOT NULL 才在 pg_constraint 中擁有獨立記錄。
PostgreSQL 17:
pg_constraint目錄用於存儲 CHECK、主鍵、唯一、外鍵、排他約束,以及定義在域上的 NOT NULL 約束。表上的 NOT NULL 約束仍然記錄在
pg_attribute中,而非pg_constraint。
因此,每一個約束都會在 pg_constraint 中以一條記錄存在,並通過 contype 字段標識約束類型。後文將對這些類型逐一説明,其中也包括一個較為特殊的類型:t。
列約束與表約束
pg_constraint 文檔中明確指出:
列約束不會被特殊處理,每個列約束在內部都等價於某種表約束。
SQL 層面上,約束可以寫在列定義後,也可以寫成表約束,例如:
CREATE TABLE products_oct (
price numeric CHECK (price > 0)
);
CREATE TABLE products_nov (
price numeric,
CHECK (price > 0)
);
第一種寫法只作用於單列,第二種寫法可以作用於多列。但在 PostgreSQL 內部,這兩種方式最終都會被統一記錄為 pg_constraint 中的一行數據。
因此,無論約束以哪種形式定義,都可以通過 ALTER TABLE .. DROP CONSTRAINT .. 刪除。系統目錄中並不存在“列約束”的特殊標識,它只是作用於單列的表約束。
下面的查詢用於查看兩個示例表中的約束定義:
SELECT
rel.relname AS table_name,
c.conname,
c.contype,
c.conrelid::regclass AS table_ref,
c.conkey,
pg_get_constraintdef(c.oid, true) AS constraint_def
FROM pg_constraint c
JOIN pg_class rel ON rel.oid = c.conrelid
WHERE rel.relname IN ('products_oct', 'products_nov');
⚡ 查詢要點説明
pg_class用於存儲所有關係對象的元數據。relname為表的名稱,由於pg_constraint中僅保存表的 OID,需要通過rel.oid = c.conrelid進行關聯。conrelid表示約束所屬表的 OID。conname為約束名稱,約束名稱在單表內唯一,可由系統自動生成,也可在 DDL 中顯式指定。contype表示約束類型(c、f、n、p、u、x、t)。conkey為屬性編號數組,用於標識約束涉及的列(如{1}表示第一列,{1,3}表示第一和第三列)。pg_get_constraintdef()為系統函數,用於獲取約束定義文本。
查詢結果如下所示。兩種約束在內部表示上幾乎完全一致,僅約束名稱和所屬表不同。
-[ RECORD 1 ]--+---------------------------
table_name | products_nov
conname | products_nov_price_check
contype | c
table_ref | products_nov
conkey | {1}
constraint_def | CHECK (price > 0::numeric)
-[ RECORD 2 ]--+---------------------------
table_name | products_oct
conname | products_oct_price_check
contype | c
table_ref | products_oct
conkey | {1}
constraint_def | CHECK (price > 0::numeric)
約束觸發器(Constraint Trigger)
在 pg_constraint 中,使用 CREATE CONSTRAINT TRIGGER 創建的約束觸發器同樣會生成記錄,其 contype 標記為 t。常見約束如 UNIQUE 為 u,CHECK 為 c。
約束觸發器是一種將觸發器機制與約束系統結合的特殊形式,主要用於數據一致性校驗。
可延遲觸發器(Deferrable Triggers)
約束觸發器通過 CREATE CONSTRAINT TRIGGER 創建,語法與普通觸發器類似,但指定 CONSTRAINT 後生成的是約束觸發器。其核心區別在於,約束觸發器可以通過 SET CONSTRAINTS 控制觸發執行時機。
其執行時機可通過 SET CONSTRAINTS 控制:
IMMEDIATE:語句結束時檢查DEFERRED:事務提交時檢查
與普通觸發器不同,約束觸發器允許在事務級別延遲執行,並在運行時動態調整。
⚠️ WHEN 條件始終立即評估
即使觸發器本身是延遲執行的,
WHEN子句仍在語句執行時立即判斷,用於決定是否進入執行隊列。
AFTER 觸發器
在創建觸發器時,需要指定觸發函數的執行時機:BEFORE、AFTER 或 INSTEAD OF。約束觸發器只能定義為 AFTER 觸發器。
約束觸發器並不用於改變數據處理流程,而是在數據操作完成後進行條件校驗。約束的核心目標是數據驗證,而普通觸發器通常用於數據修改。約束觸發器屬於校驗機制的一部分,當其所實現的約束條件被違反時,應當拋出異常。
FOR EACH ROW 觸發器
創建觸發器時,還需要指定觸發粒度:
FOR EACH ROW:對受影響的每一行執行一次FOR EACH STATEMENT:每條 SQL 語句只執行一次
約束觸發器只能定義為 FOR EACH ROW,這是因為約束校驗依賴於單行數據的具體取值。
需要注意的是,約束觸發器不支持 OR REPLACE 選項,因此只能通過刪除後重新創建的方式進行修改。
為什麼需要約束觸發器
在《Triggers to enforce constraints in PostgreSQL》一文中,Laurenz Albe 指出,某些需要在表級別強制執行的規則,無法通過常規約束直接表達,此時可藉助觸發器機制實現。文中結合示例説明了適用場景,並分析了約束與觸發器在 MVCC 行為上的差異。
在實際系統中,約束觸發器很少由用户顯式創建。PostgreSQL 更多將其作為約束實現的內部基礎機制使用,尤其是在外鍵約束中。外鍵依賴系統自動生成的約束觸發器實現,這一設計也使外鍵能夠支持 DEFERRABLE 和 INITIALLY DEFERRED 等特性。
什麼是域
域可以理解為“帶約束的數據類型”。它基於已有類型(如 text、integer),但可以附加 NOT NULL、CHECK 約束或默認值,用於集中定義數據規則。
示例如下:
CREATE DOMAIN email_address AS text
CHECK (VALUE ~* '^[^@]+@[^@]+\.[^@]+$');
CREATE TABLE users (
id serial PRIMARY KEY,
email email_address NOT NULL
);
-- This will fail
INSERT INTO users(email) VALUES ('not-an-email');
-- This will be successful
INSERT INTO users(email) VALUES ('ok@example.com');
上述示例中定義了一個名為 email_address 的新類型。所有使用該類型的列,在插入或更新數據時都會自動校驗正則表達式。即使表本身未顯式定義 CHECK 約束,非法值仍會被拒絕。
通常情況下,約束是附加在表上的,但 PostgreSQL 同樣支持在域上定義約束。以下查詢演示瞭如何從 pg_constraint 中查詢定義在域上的約束:
SELECT c.conname,
pg_get_constraintdef(c.oid, true) AS definition,
t.typname AS domain_name
FROM pg_constraint c
JOIN pg_type t ON t.oid = c.contypid
WHERE c.contype = 'c'
AND c.contypid <> 0;
⚡ 查詢要點説明
pg_constraint存儲所有類型的約束,包括表約束和域約束pg_type存儲數據類型信息,包括域contypid表示約束所屬域的 OID。當contypid非 0 時,約束附加在域上;當為 0 時,約束附加在表上,此時使用conrelid- 通過
JOIN pg_type t ON t.oid = c.contypid獲取域名稱- 域僅支持
CHECK約束,因此篩選條件為c.contype = 'c'pg_get_constraintdef()用於獲取約束定義文本,與CREATE DOMAIN中的定義一致
查詢結果如下,展示了約束名稱、定義內容以及所屬域:
conname | definition | domain_name
-------------------+---------------------------------------+-------------
email_address_check|CHECK (VALUE ~* '^[^@]+@[^@]+\.[^@]+$')| email_address
總結
通過 pg_constraint 系統目錄,可以系統理解 PostgreSQL 中各類約束的內部表示方式。無論是列約束、表約束、約束觸發器,還是域上的約束,本質上都通過同一套機制進行管理,這是 PostgreSQL 約束體系設計上的關鍵特點。
原文鏈接:
https://xata.io/blog/constraints-in-postgres
作者:Gulcin Yildirim Jelinek