在事务中删除然后创建外键约束是否安全?

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
        ;

  • 删除约束并在导入后重建它代价高昂:全部涉及AB中的记录。
  • 暂时忽略约束是 危险的:新的 B table 可能会遗漏某些仍被 A 引用的行FK。
  • ergo:你可能会得到一个残缺的模型,你必须重建 As 参考(这基本上是不可能的,没有额外的信息(这将是多余的,顺便说一句))