【问题标题】:Is it safe to drop and then create the foreign key constraints inside a transaction?在事务中删除然后创建外键约束是否安全?
【发布时间】:2015-07-02 19:42:36
【问题描述】:

我有一个引用表 B 的表 A。表 B 需要使用来自外部源的更新数据填充,为了提高效率,我使用 TRUNCATE 后跟一个 COPY。即使应用程序处于活动状态,也会这样做。

为了进一步提高效率,as suggested in the documentation,我想删除然后重新创建外键。

但是我有一些疑问。

如果我删除 FK、COPY 然后重新创建 FK在同一个事务中,我是否可以确保即使在事务期间插入到表 A 中的数据上也会保留约束?我问这个是因为理论上事务是原子的,但在文档中,关于临时删除 FK 说:

在缺少约束时,需要在数据加载速度和错误检查丢失之间进行权衡。

如果在此期间插入了错误的引用,当您尝试重新创建 FK 约束时会发生什么?

【问题讨论】:

  • B表中有多少行被导入实际改变了?插入了多少?删了多少? (提示:如果 A 引用 B,则导入后 B 中所有引用的键必须仍然存在;可能会出现新的键,但 A 不必引用)
  • @joop 在 B 中大约有 13M 行需要刷新。如果在 B 中有一些被删除的引用行,我添加了一个 UPDATE 以在同一事务中将引用设置为 NULL,就在重新创建 FK 之前。
  • 这不是关于要删除/更新插入的实际行,而是关于。添加/删除的键的比例是多少? (次要:如果 B 和 new_B 中的键 is 相同:属性实际改变值的记录的比例是多少?)
  • @wildplasser 好问题:我不知道,但我可以告诉你,我正在尝试从每周的 Wikidata 转储中刷新数据。我导入了大部分项目,并应用了一些更改以使它们适应我的架构。
  • 如果您不知道:测量。 (对于 Wikipedia 转储,我认为大多数记录实际上并没有改变,插入将相当罕见)密钥是否稳定? (否则:考虑一个类似数据保险库的模型)我会添加一个答案(或多或少是分析性的)

标签: sql postgresql transactions foreign-keys populate


【解决方案1】:

您可以使外键约束@​​987654321@(最初延迟)。这样在交易结束时只会检查一次。

ALTER TABLE
  xxx
ADD CONSTRAINT
  xxx_yyy_id_fk FOREIGN KEY (yyy_id)
REFERENCES
  yyy
DEFERRABLE INITIALLY DEFERRED;

在所有情况下,事务在 PostgreSQL 中都是完全原子的(不仅在理论上),包括 DDL 语句(例如 CREATE/DROP 约束),所以即使你删除了外键,然后插入数据,然后创建外键key 并在一个事务中完成所有操作,那么您是安全的 - 如果重新创建外键约束失败,则插入的数据也将被取消。

不过,最好切换到延迟外键,而不是删除然后创建它们。

【讨论】:

  • 由于事务大约需要 20 分钟(因为复制的数据中涉及一些 ETL),我想知道在此期间尝试插入带有引用的行的人会发生什么...
  • 那我不知道你建议的方法对百万行是否可以接受。我认为 drop 和 recreate 与推迟检查不同:你读过paragraph 14.4.4吗?
  • 好吧,这取决于您当前的情况和需求。删除外键将在 both 表(引用和被引用)上创建 Access Exclusive Lock。这意味着当您的事务正在处理时,即使是单个 SELECT 也会在两个表上被阻止。因此,如果您不关心并发性,您可以安全地使用 drop-insert-create 方案(在一个事务中)。否则推迟约束会好很多,因为它只会在引用表上创建Row Exclusive Lock
  • 关于挂起的触发事件列表和可能的内存不足问题,我不确定延迟约束如何与之交互,即正常和延迟约束之间是否存在差异。也许您可以通过测试这两种方法并分享结果来监控内存使用情况?
【解决方案2】:

TRUNCATE 不允许在外键引用的任何表上,除非您使用 TRUNCATE CASCADE,这也会截断引用表。约束的DEFERRABLE 状态不影响这一点。我认为没有办法解决这个问题。您将需要删除约束。

但是,这样做没有违反完整性的风险。 ALTER TABLE ... ADD CONSTRAINT 锁定有问题的表(TRUNCATE 也是如此),因此您的导入过程保证在其事务期间对表具有独占访问权。任何并发插入尝试都将挂起,直到导入提交,当它们被允许继续时,约束将恢复原状。

【讨论】:

    【解决方案3】:

    分析答案:测量新/相同/更新/删除记录的数量。 有四种情况:

    • B 表中的键在 b_import 中不存在:删除
    • b_import 中的键在旧 B 上不存在:插入
    • 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 表可能会丢失一些仍被A 的FK 引用的行。
    • ergo:您最终可能会得到一个残缺的模型,并且您必须重建 As 引用(这基本上是不可能的,没有额外的信息(这将是多余的,顺便说一句))

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-06-11
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-09-07
      • 1970-01-01
      相关资源
      最近更新 更多