我们在Postgres中有一个2.2 GB的表,其中有7,801,611行。我们正在向其中添加一个uuid / guid列,我想知道填充该列的最佳方法是什么(因为我们想向其添加NOT NULL
约束)。
如果我正确理解Postgres,从技术上讲,更新就是删除和插入,因此这基本上是在重建整个2.2 GB表。另外,我们有一个正在运行的奴隶,所以我们不想让它落后。
有什么方法比编写随时间推移缓慢填充脚本的方法更好?
我们在Postgres中有一个2.2 GB的表,其中有7,801,611行。我们正在向其中添加一个uuid / guid列,我想知道填充该列的最佳方法是什么(因为我们想向其添加NOT NULL
约束)。
如果我正确理解Postgres,从技术上讲,更新就是删除和插入,因此这基本上是在重建整个2.2 GB表。另外,我们有一个正在运行的奴隶,所以我们不想让它落后。
有什么方法比编写随时间推移缓慢填充脚本的方法更好?
Answers:
这在很大程度上取决于您的要求的细节。
如果你有足够的可用空间(至少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 在哪里。您需要捕获该异常,然后在您的应用程序代码中重试查询以避免该异常。
如果您无法承受这种情况,则必须保留原始表。
在添加NOT NULL
约束之前,进行适当的更新(可能一次在小段上运行更新)。添加具有NULL值且无NOT NULL
约束的新列很便宜。
从Postgres 9.2开始,您还可以使用以下方法创建CHECK
约束NOT VALID
:
该约束将仍然针对随后的插入或更新实施
这样,您可以在多个单独的事务中更新行peuàpeu-。这样可以避免将行锁保持太长时间,并且还可以重用死行。(如果之间没有足够的时间让自动真空启动,则必须手动运行。)最后,添加约束并删除约束:VACUUM
NOT NULL
NOT 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
详细讨论:
在临时表(TRUNCATE
原始表)中准备新状态,并从temp表中重新填充。所有在一个事务。在准备新表之前,您仍然需要SHARE
锁定 ,以防止丢失并发写入。
这些相关答案的详细信息如下:
LOCK
不包括和DROP
。我只能说出疯狂和无用的猜测。至于2.,请考虑我的答案的附录。
我没有“最佳”答案,但我有“最差”答案,它可能使您以合理的速度完成工作。
我的表有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天内解决了该问题。
ALTER TABLE .. ADD COLUMN ...
或者该部分也要回答?