Col*_*ers 45 postgresql storage ddl
我们在 Postgres 中有一个 2.2 GB 的表,其中有 7,801,611 行。我们正在向它添加一个 uuid/guid 列,我想知道填充该列的最佳方法是什么(因为我们想NOT NULL
向它添加约束)。
如果我正确理解 Postgres,更新在技术上是删除和插入,所以这基本上是重建整个 2.2 gb 表。我们还有一个奴隶在运行,所以我们不希望它落后。
有没有比编写一个随着时间慢慢填充它的脚本更好的方法?
Erw*_*ter 58
这在很大程度上取决于您的设置和要求的详细信息。
请注意,从 Postgres 11 开始,仅添加具有volatileDEFAULT
的列仍会触发 table rewrite。不幸的是,这是你的情况。
如果你有足够的可用空间在磁盘上-至少110%pg_size_pretty((pg_total_relation_size(tbl))
-和可以承受的股份锁定一段时间和一个排他锁了很短的时间内,然后创建一个新表,包括uuid
使用的列CREATE TABLE AS
。为什么?
下面的代码使用了附加uuid-oss
模块中的一个函数。
锁定表以防止SHARE
模式中的并发更改(仍然允许并发读取)。尝试写入表将等待并最终失败。见下文。
在动态填充新列的同时复制整个表 - 可能在处理时对行进行有利的排序。
如果您要重新排序行,请确保设置work_mem
足够高以在 RAM 中进行排序或尽可能高(仅针对您的会话,而不是全局)。
然后向新表添加约束、外键、索引、触发器等。在更新表的大部分内容时,从头开始创建索引比迭代添加行要快得多。手册中的相关建议。
当新表准备好后,删除旧表并重命名新表以使其成为替代品。只有这最后一步为事务的其余部分获取对旧表的排他锁 - 现在应该很短。
它还要求您根据表类型(视图、在签名中使用表类型的函数等)删除任何对象,然后重新创建它们。
在一笔交易中完成所有操作以避免不完整的状态。
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;
Run Code Online (Sandbox Code Playgroud)
这应该是最快的。任何其他就地更新方法也必须重写整个表,只是以一种更昂贵的方式。只有在磁盘上没有足够的可用空间或无法锁定整个表或为并发写入尝试产生错误时,您才会走这条路。
其他交易(在其他会话中)试图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-。这避免了保持行锁太长时间,并且还允许重用死行。(如果两者之间没有足够的时间让 autovacuum 启动,则必须手动运行。)最后,添加约束并删除约束: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;
Run Code Online (Sandbox Code Playgroud)
相关答案NOT VALID
更详细地讨论:
在临时表中准备新状态,TRUNCATE
原始状态并从临时表重新填充。所有在一个事务。在准备新表之前,您仍然需要SHARE
锁定 以防止丢失并发写入。
这些有关 SO 的相关答案中的详细信息:
小智 18
我没有“最好”的答案,但我有一个“最不坏”的答案,可以让您以相当快的速度完成工作。
我的表有 2MM 行,当我尝试添加默认为第一个的辅助时间戳列时,更新性能正在下降。
ALTER TABLE mytable ADD new_timestamp TIMESTAMP ;
UPDATE mytable SET new_timestamp = old_timestamp ;
ALTER TABLE mytable ALTER new_timestamp SET NOT NULL ;
Run Code Online (Sandbox Code Playgroud)
在它挂了 40 分钟后,我尝试了一小批,以了解这可能需要多长时间 - 预测约为 8 小时。
接受的答案肯定更好——但这张表在我的数据库中被大量使用。有几十个表被 FKEY 放在上面;我想避免在这么多表上切换 FOREIGN KEYS。然后有意见。
搜索了一些文档、案例研究和 StackOverflow,我得到了“A-Ha!” 片刻。消耗不是在核心 UPDATE 上,而是在所有 INDEX 操作上。我的表有 12 个索引——一些用于唯一约束,一些用于加速查询规划器,一些用于全文搜索。
UPDATED 的每一行不仅仅是处理 DELETE/INSERT,还有改变每个索引和检查约束的开销。
我的解决方案是删除每个索引和约束,更新表,然后重新添加所有索引/约束。
编写一个 SQL 事务大约需要 3 分钟,执行以下操作:
脚本运行了 7 分钟。
公认的答案肯定更好、更合适……并且几乎消除了停机的需要。但在我的情况下,使用该解决方案需要更多的“开发人员”工作,我们有 30 分钟的计划停机时间窗口可以完成。我们的解决方案在 10 分钟内解决了这个问题。