在事务中删除然后创建外键约束是否安全?
Is it safe to drop and then create the foreign key constraints inside a transaction?
我有一个 table A 引用了一个 table B。Table B 需要填充来自外部源的更新数据,为了提高效率,我使用 TRUNCATE 后跟一个复制。即使应用程序处于活动状态,也会这样做。
为了进一步提高效率,as suggested in the documentation,我想删除然后重新创建外键。
不过我有些疑惑。
如果我删除 FK,COPY 然后在同一事务中重新创建 FK ,我能否确保即使在 table 中插入的数据上也保留了约束A在交易期间?我问这个是因为理论上事务是原子的,但在文档中,关于临时删除 FK 的说法是:
there is a trade-off between data load speed and loss of error checking while the constraint is missing.
如果同时插入了错误的引用,当您尝试重新创建 FK 约束时会发生什么情况?
你可以使外键约束deferrable(最初延迟)。这样它只会在交易结束时被检查一次。
ALTER TABLE
xxx
ADD CONSTRAINT
xxx_yyy_id_fk FOREIGN KEY (yyy_id)
REFERENCES
yyy
DEFERRABLE INITIALLY DEFERRED;
在所有情况下,事务在PostgreSQL中都是完全原子的(不仅仅是理论上),包括DDL语句(例如CREATE/DROP约束),所以即使你删除一个外键,然后插入数据,然后创建外键并在一个事务中完成所有操作,那么你就安全了——如果重新创建外键约束失败,那么插入的数据也将被忽略。
不过,最好切换到延迟外键,而不是先删除再创建它们。
外键引用的任何 table 都不允许 TRUNCATE
,除非您使用 TRUNCATE CASCADE
,这也会截断引用 table。约束的 DEFERRABLE
状态对此没有影响。我认为没有办法解决这个问题;您将需要删除约束。
但是,这样做没有违反完整性的风险。 ALTER TABLE ... ADD CONSTRAINT
锁定了有问题的 table(TRUNCATE
也是如此),因此您的导入过程保证在其事务期间对 table 具有独占访问权。并发插入的任何尝试都将挂起,直到导入已提交,并且当它们被允许继续时,约束将回到原位。
解析答案:测量new/same/updated/deleted条记录的数量。
有四种情况:
B
table 中的密钥不存在于 b_import 中:删除
b_import
中的键在旧 B 上不存在:insert
- key在旧B和新B中都有,但内容相同:忽略
- 键相同,属性值不同:更新
-- some test data for `A`, `B` and `B_import`:
CREATE TABLE b
( id INTEGER NOT NULL PRIMARY KEY
, payload varchar
);
INSERT INTO b(id,payload) SELECT gs, 'bb_' || gs::varchar
FROM generate_series(1,20) gs;
CREATE TABLE b_import
( id INTEGER NOT NULL PRIMARY KEY
, payload varchar
);
INSERT INTO b_import(id,payload) SELECT gs, 'bb_' || gs::varchar
FROM generate_series(10,15) gs;
-- In real life this table will be filled by a `COPY b_import FROM ...`
INSERT INTO b_import(id,payload) SELECT gs, 'b2_' || gs::varchar
FROM generate_series(16,25) gs;
CREATE TABLE a
( id SERIAL NOT NULL PRIMARY KEY
, b_id INTEGER references b(id) ON DELETE SET NULL
, aaaaa varchar
);
INSERT INTO a(b_id,aaaaa)
SELECT gs,'aaaaa_' || gs::text FROM generate_series(1,20) gs;
CREATE INDEX ON a(b_id); -- index supporting the FK
-- show it
SELECT a.id, a.aaaaa
,b.id, b.payload AS oldpayload
FROM a
FULL JOIN b ON a.b_id=b.id
ORDER BY a.id;
-- Do the actual I/U/D and report the numbers of affected rows
-- EXPLAIN
WITH ins AS ( -- INSERTS
INSERT INTO b(id, payload)
SELECT b_import.id, b_import.payload
FROM b_import
WHERE NOT EXISTS (
SELECT 1 FROM b
WHERE b.id = b_import.id
)
RETURNING b.id
)
, del AS ( -- DELETES
DELETE FROM b
WHERE NOT EXISTS (
SELECT 2 FROM b_import
WHERE b_import.id = b.id
)
RETURNING b.id
)
, upd AS ( -- UPDATES
UPDATE b
SET payload=b_import.payload
FROM b_import
WHERE b_import.id = b.id
AND b_import.payload IS DISTINCT FROM b.payload -- exclude idempotent updates
-- AND NOT EXISTS ( -- exclude deleted records
-- SELECT 3 FROM del
-- WHERE del.id = b_import.id
-- )
-- AND NOT EXISTS ( -- avoid touching freshly inserted rows
-- SELECT 4 FROM ins
-- WHERE ins.id = b_import.id
-- )
RETURNING b.id
)
SELECT COUNT(*) AS orgb
, (SELECT COUNT(*) FROM b_import) AS newb
, (SELECT COUNT(*) FROM ins) AS ninserted
, (SELECT COUNT(*) FROM del) AS ndeleted
, (SELECT COUNT(*) FROM upd) AS nupdated
FROM b
;
- 删除约束并在导入后重建它代价高昂:全部涉及
A
和B
中的记录。
- 暂时忽略约束是 危险的:新的
B
table 可能会遗漏某些仍被 A
引用的行FK。
- ergo:你可能会得到一个残缺的模型,你必须重建
A
s 参考(这基本上是不可能的,没有额外的信息(这将是多余的,顺便说一句))
我有一个 table A 引用了一个 table B。Table B 需要填充来自外部源的更新数据,为了提高效率,我使用 TRUNCATE 后跟一个复制。即使应用程序处于活动状态,也会这样做。
为了进一步提高效率,as suggested in the documentation,我想删除然后重新创建外键。
不过我有些疑惑。
如果我删除 FK,COPY 然后在同一事务中重新创建 FK ,我能否确保即使在 table 中插入的数据上也保留了约束A在交易期间?我问这个是因为理论上事务是原子的,但在文档中,关于临时删除 FK 的说法是:
there is a trade-off between data load speed and loss of error checking while the constraint is missing.
如果同时插入了错误的引用,当您尝试重新创建 FK 约束时会发生什么情况?
你可以使外键约束deferrable(最初延迟)。这样它只会在交易结束时被检查一次。
ALTER TABLE
xxx
ADD CONSTRAINT
xxx_yyy_id_fk FOREIGN KEY (yyy_id)
REFERENCES
yyy
DEFERRABLE INITIALLY DEFERRED;
在所有情况下,事务在PostgreSQL中都是完全原子的(不仅仅是理论上),包括DDL语句(例如CREATE/DROP约束),所以即使你删除一个外键,然后插入数据,然后创建外键并在一个事务中完成所有操作,那么你就安全了——如果重新创建外键约束失败,那么插入的数据也将被忽略。
不过,最好切换到延迟外键,而不是先删除再创建它们。
TRUNCATE
,除非您使用 TRUNCATE CASCADE
,这也会截断引用 table。约束的 DEFERRABLE
状态对此没有影响。我认为没有办法解决这个问题;您将需要删除约束。
但是,这样做没有违反完整性的风险。 ALTER TABLE ... ADD CONSTRAINT
锁定了有问题的 table(TRUNCATE
也是如此),因此您的导入过程保证在其事务期间对 table 具有独占访问权。并发插入的任何尝试都将挂起,直到导入已提交,并且当它们被允许继续时,约束将回到原位。
解析答案:测量new/same/updated/deleted条记录的数量。 有四种情况:
B
table 中的密钥不存在于 b_import 中:删除b_import
中的键在旧 B 上不存在:insert- key在旧B和新B中都有,但内容相同:忽略
- 键相同,属性值不同:更新
-- some test data for `A`, `B` and `B_import`:
CREATE TABLE b
( id INTEGER NOT NULL PRIMARY KEY
, payload varchar
);
INSERT INTO b(id,payload) SELECT gs, 'bb_' || gs::varchar
FROM generate_series(1,20) gs;
CREATE TABLE b_import
( id INTEGER NOT NULL PRIMARY KEY
, payload varchar
);
INSERT INTO b_import(id,payload) SELECT gs, 'bb_' || gs::varchar
FROM generate_series(10,15) gs;
-- In real life this table will be filled by a `COPY b_import FROM ...`
INSERT INTO b_import(id,payload) SELECT gs, 'b2_' || gs::varchar
FROM generate_series(16,25) gs;
CREATE TABLE a
( id SERIAL NOT NULL PRIMARY KEY
, b_id INTEGER references b(id) ON DELETE SET NULL
, aaaaa varchar
);
INSERT INTO a(b_id,aaaaa)
SELECT gs,'aaaaa_' || gs::text FROM generate_series(1,20) gs;
CREATE INDEX ON a(b_id); -- index supporting the FK
-- show it
SELECT a.id, a.aaaaa
,b.id, b.payload AS oldpayload
FROM a
FULL JOIN b ON a.b_id=b.id
ORDER BY a.id;
-- Do the actual I/U/D and report the numbers of affected rows
-- EXPLAIN
WITH ins AS ( -- INSERTS
INSERT INTO b(id, payload)
SELECT b_import.id, b_import.payload
FROM b_import
WHERE NOT EXISTS (
SELECT 1 FROM b
WHERE b.id = b_import.id
)
RETURNING b.id
)
, del AS ( -- DELETES
DELETE FROM b
WHERE NOT EXISTS (
SELECT 2 FROM b_import
WHERE b_import.id = b.id
)
RETURNING b.id
)
, upd AS ( -- UPDATES
UPDATE b
SET payload=b_import.payload
FROM b_import
WHERE b_import.id = b.id
AND b_import.payload IS DISTINCT FROM b.payload -- exclude idempotent updates
-- AND NOT EXISTS ( -- exclude deleted records
-- SELECT 3 FROM del
-- WHERE del.id = b_import.id
-- )
-- AND NOT EXISTS ( -- avoid touching freshly inserted rows
-- SELECT 4 FROM ins
-- WHERE ins.id = b_import.id
-- )
RETURNING b.id
)
SELECT COUNT(*) AS orgb
, (SELECT COUNT(*) FROM b_import) AS newb
, (SELECT COUNT(*) FROM ins) AS ninserted
, (SELECT COUNT(*) FROM del) AS ndeleted
, (SELECT COUNT(*) FROM upd) AS nupdated
FROM b
;
- 删除约束并在导入后重建它代价高昂:全部涉及
A
和B
中的记录。 - 暂时忽略约束是 危险的:新的
B
table 可能会遗漏某些仍被A
引用的行FK。 - ergo:你可能会得到一个残缺的模型,你必须重建
A
s 参考(这基本上是不可能的,没有额外的信息(这将是多余的,顺便说一句))