在大表中填充新列的最佳方法?


33

我们在Postgres中有一个2.2 GB的表,其中有7,801,611行。我们正在向其中添加一个uuid / guid列,我想知道填充该列的最佳方法是什么(因为我们想向其添加NOT NULL约束)。

如果我正确理解Postgres,从技术上讲,更新就是删除和插入,因此这基本上是在重建整个2.2 GB表。另外,我们有一个正在运行的奴隶,所以我们不想让它落后。

有什么方法比编写随时间推移缓慢填充脚本的方法更好?


2
您是否已经运行过ALTER TABLE .. ADD COLUMN ...或者该部分也要回答?
ypercubeᵀᴹ

只是在计划阶段,尚未运行任何表修改。我之前通过添加列,填充它,然后添加约束或索引来做到这一点。但是,此表明显更大,我担心负载,锁定,复制等…
Collin Peters 2013年

Answers:


45

这在很大程度上取决于您的要求的细节。

如果你有足够的可用空间(至少110%pg_size_pretty((pg_total_relation_size(tbl))的磁盘)和可以承受的一段时间共享锁一个很短的时间内独占锁,然后创建一个新表,包括uuid使用的列CREATE TABLE AS。为什么?

以下代码使用附加uuid-oss模块中功能

  • 锁定表以防止SHARE模式中的并发更改(仍然允许并发读取)。尝试写入表将等待并最终失败。见下文。

  • 快速填充整个表,同时动态填充新列-可能需要同时对行进行排序。
    如果要对行进行重新排序,请确保设置得work_mem尽可能高(仅针对您的会话,而不是全局)。

  • 然后将约束,外键,索引,触发器等添加到新表中。更新表的大部分时,从头开始创建索引比迭代添加行快得多。

  • 当新表准备就绪时,删除旧表并重命名新表以使其成为嵌入式替换。只有最后一步才能在其余事务中获得旧表的排他锁-现在应该很短。
    它还要求您根据表类型(视图,在签名中使用表类型的函数,...)删除任何对象,然后再重新创建它们。

  • 一次完成所有操作,以避免出现不完整的状态。

BEGIN;
LOCK TABLE tbl IN SHARE MODE;

SET LOCAL work_mem = '???? MB';  -- just for this transaction

CREATE TABLE tbl_new AS 
SELECT uuid_generate_v1() AS tbl_uuid, <list of all columns in order>
FROM   tbl
ORDER  BY ??;  -- optionally order rows favorably while being at it.

ALTER TABLE tbl_new
   ALTER COLUMN tbl_uuid SET NOT NULL
 , ALTER COLUMN tbl_uuid SET DEFAULT uuid_generate_v1()
 , ADD CONSTRAINT tbl_uuid_uni UNIQUE(tbl_uuid);

-- more constraints, indices, triggers?

DROP TABLE tbl;
ALTER TABLE tbl_new RENAME tbl;

-- recreate views etc. if any
COMMIT;

这应该是最快的。任何其他就地更新方法都必须以更昂贵的方式重写整个表。如果磁盘上没有足够的可用空间,或者负担不起锁定整个表或为并发写入尝试生成错误,则只有采用这种方法。

并发写入会怎样?

其他交易(在其他会话中)试图INSERT/ UPDATE/ DELETE在后您的交易采取了相同的表SHARE锁,将等到锁被释放或者超时踢,以先到者为准。它们将以任何一种方式失败,因为它们要写入的表已从它们下面删除。

新表具有新表OID,但是并发事务已将表名解析为上一个表的OID 。最终释放锁定后,他们会尝试在写入表之前先锁定表,然后发现表已消失。Postgres将回答:

ERROR: could not open relation with OID 123456

123456旧表的OID 在哪里。您需要捕获该异常,然后在您的应用程序代码中重试查询以避免该异常。

如果您无法承受这种情况,则必须保留原始表。

保留现有表格的两种选择

  1. 在添加NOT NULL约束之前,进行适当的更新(可能一次在小段上运行更新)。添加具有NULL值且无NOT NULL约束的新列很便宜。
    从Postgres 9.2开始,您还可以使用以下方法创建CHECK约束NOT VALID

    该约束将仍然针对随后的插入或更新实施

    这样,您可以在多个单独的事务中更新行peuàpeu-。这样可以避免将行锁保持太长时间,并且还可以重用死行。(如果之间没有足够的时间让自动真空启动,则必须手动运行。)最后,添加约束并删除约束:VACUUMNOT NULLNOT VALID CHECK

    ALTER TABLE tbl ADD CONSTRAINT tbl_no_null CHECK (tbl_uuid IS NOT NULL) NOT VALID;
    
    -- update rows in multiple batches in separate transactions
    -- possibly run VACUUM between transactions
    
    ALTER TABLE tbl ALTER COLUMN tbl_uuid SET NOT NULL;
    ALTER TABLE tbl ALTER DROP CONSTRAINT tbl_no_null;

    相关答案NOT VALID详细讨论:

  2. 临时表TRUNCATE原始表)中准备新状态,并从temp表中重新填充。所有在一个事务准备新表之前,您仍然需要SHARE锁定 以防止丢失并发写入。

    这些相关答案的详细信息如下:


很棒的答案!正是我要找的信息。两个问题1.您是否想知道一种简单的方法来测试类似动作将花费多长时间?2.如果要花费5分钟,那么在那5分钟内尝试更新该表中某行的操作会如何?
Collin Peters

@CollinPeters:1.大部分时间都花在了复制大表上–并可能重新创建索引和约束(取决于情况)。删除和重命名很便宜。要进行测试,您可以运行准备好的SQL脚本,但LOCK不包括和DROP。我只能说出疯狂和无用的猜测。至于2.,请考虑我的答案的附录。
Erwin Brandstetter

@ErwinBrandstetter继续重新创建视图,因此,如果我有十几个视图在表重命名后仍然使用旧表(oid)。有什么方法可以执行深度替换,而不是重新运行整个视图刷新/创建吗?
CodeFarmer

@CodeFarmer:如果仅重命名表,则视图将继续使用重命名的表。要使视图改用表,您需要根据新表重新创建视图。(还允许删除旧表。)没有(实用的)解决方法。
Erwin Brandstetter

14

我没有“最佳”答案,但我有“最差”答案,它可能使您以合理的速度完成工作。

我的表有2毫米的行,并且当我尝试添加默认为第一列的辅助时间戳列时,更新性能不佳。

ALTER TABLE mytable ADD new_timestamp TIMESTAMP ;
UPDATE mytable SET new_timestamp = old_timestamp ;
ALTER TABLE mytable ALTER new_timestamp SET NOT NULL ;

挂了40分钟后,我进行了小批量尝试,以了解大概需要多长时间-预测大约是8个小时。

可接受的答案肯定会更好-但此表在我的数据库中使用率很高。FKEY上有几十张桌子;我想避免在这么多表上切换FOREIGN KEYS。然后有意见。

稍微搜索一下文档,案例研究和StackOverflow,我得到了“ A-Ha!”。时刻。消耗不是核心UPDATE,而是所有INDEX操作。我的表上有12个索引-一些用于唯一约束,一些用于加快查询计划程序的速度,还有一些用于全文本搜索。

UPDATED的每一行不仅在DELETE / INSERT上工作,而且在改变每个索引和检查约束上也有开销。

我的解决方案是删除所有索引和约束,更新表,然后再添加所有索引/约束。

花费大约3分钟编写一个执行以下操作的SQL事务:

  • 开始;
  • 删除索引/内容
  • 更新表
  • 重新添加索引/约束
  • 承诺;

该脚本运行了7分钟。

公认的答案肯定是更好,更合适的……并且实际上消除了停机时间。但就我而言,要使用该解决方案将花费更多的“开发人员”工作,并且有30分钟的预定停机时间可以实现。我们的解决方案在10天内解决了该问题。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.