PostgreSQL中enable、disable和validate外來鍵約束
postgres=# create table t1(a int primary key,b text,c date); CREATE TABLE postgres=# create table t2(a int primary key,b int references t1(a),c text); CREATE TABLE postgres=# insert into t1 (a,b,c) values(1,'aa',now()); INSERT 0 1 postgres=# insert into t1 (a,b,c) values(2,'bb',now()); INSERT 0 1 postgres=# insert into t2 (a,b,c) values (1,1,'aa'); INSERT 0 1 postgres=# insert into t2 (a,b,c) values (2,2,'aa'); INSERT 0 1 postgres=# \d t1 Table "public.t1" Column | Type | Collation | Nullable | Default --------+---------+-----------+----------+--------- a | integer | | not null | b | text | | | c | date | | | Indexes: "t1_pkey" PRIMARY KEY, btree (a) Referenced by: TABLE "t2" CONSTRAINT "t2_b_fkey" FOREIGN KEY (b) REFERENCES t1(a) postgres=# \d t2 Table "public.t2" Column | Type | Collation | Nullable | Default --------+---------+-----------+----------+--------- a | integer | | not null | b | integer | | | c | text | | | Indexes: "t2_pkey" PRIMARY KEY, btree (a) Foreign-key constraints: "t2_b_fkey" FOREIGN KEY (b) REFERENCES t1(a) postgres=#
假設我們想通過指令碼向表中載入一些資料。因為我們不知道指令碼中載入的順序,我們決定將表t2上的外來鍵約束禁用掉,在資料載入之後載開啟外來鍵約束:
postgres=# alter table t2 disable trigger all; ALTER TABLE postgres=#
這裡看起來可能有點奇怪,但是它的確禁用了外來鍵約束。如果有其他外來鍵約束,當然也是被禁用了。
我們再來看看錶t2:
postgres=# \d t2 Table "public.t2" Column | Type | Collation | Nullable | Default --------+---------+-----------+----------+--------- a | integer | | not null | b | integer | | | c | text | | | Indexes: "t2_pkey" PRIMARY KEY, btree (a) Foreign-key constraints: "t2_b_fkey" FOREIGN KEY (b) REFERENCES t1(a) Disabled internal triggers: "RI_ConstraintTrigger_c_75213" AFTER INSERT ON t2 FROM t1 NOT DEFERRABLE INITIALLY IMMEDIATE FOR EACH ROW EXECUTE PROCEDURE "RI_FKey_check_ins"() "RI_ConstraintTrigger_c_75214" AFTER UPDATE ON t2 FROM t1 NOT DEFERRABLE INITIALLY IMMEDIATE FOR EACH ROW EXECUTE PROCEDURE "RI_FKey_check_upd"() postgres=#
關鍵字all將表上的其他內部觸發器也禁用了,需要superser才可以執行成功。
postgres=# create user abce with login password 'abce'; CREATE ROLE postgres=# \c postgres abce You are now connected to database "postgres" as user "abce". postgres=> create table t3 ( a int primary key, b text, c date); CREATE TABLE postgres=> create table t4 ( a int primary key, b int references t3(a), c text); CREATE TABLE postgres=> alter table t4 disable trigger all; ERROR: permission denied: "RI_ConstraintTrigger_c_75235" is a system trigger postgres=>
那作為普通使用者,該如何禁用觸發器呢?
postgres=> alter table t4 disable trigger user;
具體語法為:
DISABLE TRIGGER [ trigger_name | ALL | USER ]
回到t1、t2表。
postgres=# select * from t1; a | b | c ---+----+------------ 1 | aa | 2020-11-04 2 | bb | 2020-11-04 (2 rows) postgres=# select * from t2; a | b | c ---+---+---- 1 | 1 | aa 2 | 2 | aa (2 rows) postgres=# insert into t2 (a,b,c) values (3,3,'cc'); INSERT 0 1 postgres=#
這裡插入了一條在t1中不匹配的記錄,但是插入成功了。
postgres=# alter table t2 enable trigger all; ALTER TABLE postgres=# \d t2 Table "public.t2" Column | Type | Collation | Nullable | Default --------+---------+-----------+----------+--------- a | integer | | not null | b | integer | | | c | text | | | Indexes: "t2_pkey" PRIMARY KEY, btree (a) Foreign-key constraints: "t2_b_fkey" FOREIGN KEY (b) REFERENCES t1(a) postgres=# alter table t2 validate constraint t2_b_fkey; ALTER TABLE postgres=#
是不是很驚訝,PostgreSQL沒有報告不匹配的記錄。為什麼呢?
檢視一個pg_constraint:
postgres=# select * from pg_constraint where conname='t2_b_fkey' and conrelid='t2'::regclass; -[ RECORD 1 ]-+---------- conname | t2_b_fkey connamespace | 2200 contype | f condeferrable | f condeferred | f convalidated | t conrelid | 75202 contypid | 0 conindid | 75200 conparentid | 0 confrelid | 75194 confupdtype | a confdeltype | a confmatchtype | s conislocal | t coninhcount | 0 connoinherit | t conkey | {2} confkey | {1} conpfeqop | {96} conppeqop | {96} conffeqop | {96} conexclop | conbin | consrc | postgres=#
convalidated欄位的值為t,表明該外來鍵約束還是有效的。
哪怕是我們再次將其disable,仍然會顯示是有效的:
postgres=# alter table t2 disable trigger all; ALTER TABLE postgres=# select * from pg_constraint where conname='t2_b_fkey' and conrelid='t2'::regclass; -[ RECORD 1 ]-+---------- conname | t2_b_fkey connamespace | 2200 contype | f condeferrable | f condeferred | f convalidated | t conrelid | 75202 contypid | 0 conindid | 75200 conparentid | 0 confrelid | 75194 confupdtype | a confdeltype | a confmatchtype | s conislocal | t coninhcount | 0 connoinherit | t conkey | {2} confkey | {1} conpfeqop | {96} conppeqop | {96} conffeqop | {96} conexclop | conbin | consrc | postgres=#
這表明當我們開啟(enable)內部觸發器的時候,PostgreSQL不會驗證(validate)約束,因此也不會驗證資料是否會有衝突,因為外來鍵約束的狀態始終是有效的。
我們需要做的是先將其變成無效的:
postgres=# alter table t2 alter CONSTRAINT t2_b_fkey not valid; ERROR: ALTER CONSTRAINT statement constraints cannot be marked NOT VALID ## 需要先將外來鍵刪掉,然後重建外來鍵約束並將其狀態設定成無效 postgres=# alter table t2 drop constraint t2_b_fkey; ALTER TABLE postgres=# delete from t2 where a in (3); DELETE 1 postgres=# alter table t2 add constraint t2_b_fkey foreign key (b) references t1(a) not valid; ALTER TABLE postgres=# \d t2 Table "public.t2" Column | Type | Collation | Nullable | Default --------+---------+-----------+----------+--------- a | integer | | not null | b | integer | | | c | text | | | Indexes: "t2_pkey" PRIMARY KEY, btree (a) Foreign-key constraints: "t2_b_fkey" FOREIGN KEY (b) REFERENCES t1(a) NOT VALID
現在,可以看到狀態是無效的了:
postgres=# select * from pg_constraint where conname='t2_b_fkey' and conrelid='t2'::regclass; -[ RECORD 1 ]-+---------- conname | t2_b_fkey connamespace | 2200 contype | f condeferrable | f condeferred | f convalidated | f conrelid | 75202 contypid | 0 conindid | 75200 conparentid | 0 confrelid | 75194 confupdtype | a confdeltype | a confmatchtype | s conislocal | t coninhcount | 0 connoinherit | t conkey | {2} confkey | {1} conpfeqop | {96} conppeqop | {96} conffeqop | {96} conexclop | conbin | consrc | postgres=#
繼續插入資料:
postgres=# insert into t2(a,b,c) values (3,3,'cc'); ERROR: insert or update on table "t2" violates foreign key constraint "t2_b_fkey" DETAIL: Key (b)=(3) is not present in table "t1". postgres=#
是不是更驚訝了?建立了一個無效的約束,只是通知PostgreSQL
不要掃描整個表去驗證所有的行記錄是否有效。對於新插入或更新的行,仍然會檢查是否滿足約束條件,這就是為什麼上面插入失敗了。
我們該怎麼做呢?
1.刪除所有的外來鍵
2.載入資料
3.重新建立外來鍵,但是將其狀態設定成無效的,從而避免掃描整個表。之後,新的資料會被驗證了
4.在系統負載低的時候開啟約束驗證(validate the constraints)
另一種方法是:
postgres=# alter table t2 alter constraint t2_b_fkey deferrable; ALTER TABLE postgres=# begin; BEGIN postgres=# set constraints all deferred; SET CONSTRAINTS postgres=# insert into t2 (a,b,c) values (3,3,'cc'); INSERT 0 1 postgres=# insert into t2 (a,b,c) values (4,4,'dd'); INSERT 0 1 postgres=# insert into t1 (a,b,c) values (3,'cc',now()); INSERT 0 1 postgres=# insert into t1 (a,b,c) values (4,'dd',now()); INSERT 0 1 postgres=# commit; COMMIT
這樣做不好的方面是,在下一次提交時才起作用,因此,你需要將所有的工作放到一個事務中。
本文的關鍵點是,下面的假設將驗證你的資料是錯誤的:
postgres=# alter table t2 disable trigger all; ALTER TABLE postgres=# insert into t2 (a,b,c) values (5,5,'ee'); INSERT 0 1 postgres=# alter table t2 enable trigger all; ALTER TABLE postgres=#
這隻會驗證新的資料,但是並不保證所有的資料都滿足約束:
postgres = # insert into t2 (a,b,c) values (6,6,'ff'); ERROR: insert or update on table "t2" violates foreign key constraint "t2_b_fkey" DETAIL: Key(b) = (6) is not present in table "t1". postgres = # select * from t2 where b = 5; a | b | c ---+---+---- 5 | 5 | ee (1 row) postgres = # select * from t1 where a = 5; a | b | c ---+---+--- (0 rows)
最終,還有一種方式來解決,直接修改pg_constraint目錄表。但是並建議使用者這麼做!
postgres=# delete from t2 where b = 5; DELETE 1 postgres=# delete from t2 where b = 5; DELETE 1 postgres=# alter table t2 disable trigger all; ALTER TABLE postgres=# insert into t2 values (5,5,'ee'); INSERT 0 1 postgres=# alter table t2 enable trigger all; ALTER TABLE postgres=# update pg_constraint set convalidated = false where conname = 't2_b_fkey' and conrelid = 't2'::regclass; UPDATE 1 postgres=# alter table t2 validate constraint t2_b_fkey; ERROR: insert or update on table "t2" violates foreign key constraint "t2_b_fkey" DETAIL: Key (b)=(5) is not present in table "t1". postgres=#