我如何在PostgreSQL中进行大量的无阻塞更新?
我想在PostgreSQL的表上做一个大的更新,但是我不需要在整个操作中维护事务完整性,因为我知道我正在更改的列不会被写入或读取更新。 我想知道在psql控制台中是否有一种简单的方法来使这些types的操作更快。
例如,假设我有一个包含3500万行的“orders”表,我想这样做:
UPDATE orders SET status = null;
为了避免被转移到偏离的讨论,让我们假设3500万列的所有状态值目前设置为相同(非空值),从而使索引无用。
这个声明的问题是生效需要很长时间(仅仅是因为locking),所有更改的行都被locking,直到整个更新完成。 此更新可能需要5个小时,而类似的
UPDATE orders SET status = null WHERE (order_id > 0 and order_id < 1000000);
可能需要1分钟。 超过三千五百万行,做上面的,并把它分成35块只需要35分钟,为我节省4小时25分钟。
我可以用脚本(在这里使用伪代码)进一步分解:
for (i = 0 to 3500) { db_operation ("UPDATE orders SET status = null WHERE (order_id >" + (i*1000)" + " AND order_id <" + ((i+1)*1000) " + ")"); }
这个操作可能在几分钟内完成,而不是35分钟。
所以这归结于我真正要求的。 我不想写一个吓人的脚本来打破每一次我想做一个大的一次性更新这样的操作。 有没有办法完成我想完全在SQL内?
列/行
…我不需要在整个操作中维护事务完整性,因为我知道我正在更改的列不会在更新过程中写入或读取。
在PostgreSQL的MVCC模型中的任何UPDATE
写入整个行的新版本。 如果并发事务更改同一行的任何列,则会产生耗时的并发问题。 手册中的细节。 了解同一列不会被并发事务触及,避免了一些可能的复杂情况,而不是其他问题。
指数
为了避免被转移到偏离的讨论,让我们假设3500万列的所有状态值目前设置为相同(非空值),从而使索引无用。
在更新整个表格 (或其主要部分)时,Postgres 从不使用索引 。 当所有或大部分行都必须被读取时,顺序扫描速度更快。 相反:索引维护意味着UPDATE
额外成本。
性能
例如,假设我有一个包含3500万行的“orders”表,我想这样做:
UPDATE orders SET status = null;
我知道你正在寻求一个更一般的解决scheme(见下文)。 但要解决实际问题 :无论表大小如何,都可以在几毫秒内处理:
ALTER TABLE orders DROP column status , ADD column status text;
每个文档:
当使用
ADD COLUMN
,表中的所有现有行都将使用该列的默认值进行初始化(如果未指定DEFAULT
子句,则为NULL
)。
和:
DROP COLUMN
表单不会在物理上删除该列,而只是使其对SQL操作不可见。 表中的后续插入和更新操作将为该列存储空值。 因此,删除列很快,但不会立即减less您的表的磁盘大小,因为被删除的列占用的空间不会被回收。 随着现有行的更新,空间将随时间被回收。 (这些语句在删除系统oid列时不适用;这是通过立即重写完成的。)
确保你没有对象取决于列(外键约束,索引,视图,…)。 你需要删除/重新创build这些。 除此之外,系统目录表pg_attribute
上的微小操作可以完成这项工作。 需要桌面上的独占locking ,这可能是一个严重的并发负载问题。 由于它只需要几毫秒,你应该还可以。
如果您想要保留一个列的默认值,请将其添加回单独的命令中 。 在同一个命令中执行它将立即将其应用于所有行,从而排除效果。
按照链接阅读手册中的注释 。
一般解决scheme
在另一个答案中已经提到了dblink
。 它允许以隐式的单独连接访问“远程”Postgres数据库。 “远程”数据库可以是当前的数据库,从而实现“自治事务” :在“远程”数据库中写入的function被提交并且不能被回滚。
这允许运行一个单独的函数来更新较小部分的大表,并且每个部分都是单独提交的。 避免为很大数量的行build立事务开销,更重要的是,在每个部分之后释放锁。 这允许并发操作不会很多延迟,并且使得死锁的可能性较小。
如果你没有并发访问,这是没有用的 – 除了在例外之后避免ROLLBACK
。 对于这种情况也要考虑SAVEPOINT
。
放弃
首先,很多小额交易实际上是比较昂贵的。 这只对大桌子才有意义 。 最佳点取决于许多因素。
如果你不确定你在做什么: 单一交易是安全的方法 。 为了能够正常工作,桌面上的并发操作必须同时进行。 例如:并发写入可以将一行移动到被认为已经被处理的分区。 或者并发读取可以看到不一致的中间状态。 你被警告了。
分步说明
需要先安装附加模块dblink:
- 如何在PostgreSQL中使用(安装)dblink?
build立与dblink的连接非常依赖于数据库集群和安全策略的设置。 这可能会很棘手。 相关的后续答案更多如何与dblink连接 :
- 即使函数中止,也可以持久插入到UDF中
根据指示创build一个FOREIGN SERVER
和一个USER MAPPING
,以简化和简化连接(除非已经有了)。
假设有或没有一些空白的serial PRIMARY KEY
。
CREATE OR REPLACE FUNCTION f_update_in_steps() RETURNS void AS $func$ DECLARE _step int; -- size of step _cur int; -- current ID (starting with minimum) _max int; -- maximum ID BEGIN SELECT INTO _cur, _max min(order_id), max(order_id) FROM orders; -- 100 slices (steps) hard coded _step := ((_max - _cur) / 100) + 1; -- rounded, possibly a bit too small -- +1 to avoid endless loop for 0 PERFORM dblink_connect('myserver'); -- your foreign server as instructed above FOR i IN 0..200 LOOP -- 200 >> 100 to make sure we exceed _max PERFORM dblink_exec( $$UPDATE public.orders SET status = 'foo' WHERE order_id >= $$ || _cur || $$ AND order_id < $$ || _cur + _step || $$ AND status IS DISTINCT FROM 'foo'$$); -- avoid empty update _cur := _cur + _step; EXIT WHEN _cur > _max; -- stop when done (never loop till 200) END LOOP; PERFORM dblink_disconnect(); END $func$ LANGUAGE plpgsql;
呼叫:
SELECT f_update_in_steps();
您可以根据需要参数化任何部分:表名,列名,值,…只要确保清理标识符以避免SQL注入:
- 表名称作为PostgreSQL函数参数
关于避免空UPDATE:
- 我如何(或可以)在多列上selectDISTINCT?
我会用CTAS:
begin; create table T as select col1, col2, ..., <new value>, colN from orders; drop table orders; alter table T rename to orders; commit;
Postgres使用MVCC(多版本并发控制),从而避免任何locking,如果你是唯一的作家; 任何数量的并发阅读器都可以在桌面上工作,并且不会有任何locking。
所以,如果真的需要5小时,它必须是由于不同的原因(例如,你有并行写入,违背你的声明,你没有)。
您应该将此列委派给另一个表,如下所示:
create table order_status ( order_id int not null references orders(order_id) primary key, status int not null );
那么你的设置status = NULL的操作将是即时的:
truncate order_status;
你确定这是因为locking? 我不这么认为,还有其他很多可能的原因。 为了找出你总是可以尝试只是locking。 试试这个:BEGIN; SELECT NOW(); SELECT * FROM order FOR UPDATE; SELECT NOW(); ROLLBACK;
要了解真正发生了什么,您应该首先运行EXPLAIN(EXPLAIN UPDATE命令SET状态…)和/或EXPLAIN ANALYZE。 也许你会发现你没有足够的内存来有效地执行UPDATE。 如果是这样,SET work_mem TO'xxxMB'; 可能是个简单的解决scheme。
此外,尾巴的PostgreSQL日志,看看是否有一些性能相关的问题发生。
首先 – 你确定你需要更新所有行吗?
也许一些行已经有了status
NULL?
如果是,那么:
UPDATE orders SET status = null WHERE status is not null;
至于分区的变化 – 这是纯粹的SQL不可能的。 所有更新都在单个事务中。
一个可能的方式来做“纯sql”将是安装dblink,使用dblink连接到相同的数据库,然后发出大量的dblink更新,但它似乎是这样一个简单的任务矫枉过正。
通常只需join适当的地方解决问题。 如果没有 – 只是手动分区。 写一个脚本太多了 – 通常你可以用简单的一行来完成:
perl -e ' for (my $i = 0; $i <= 3500000; $i += 1000) { printf "UPDATE orders SET status = null WHERE status is not null and order_id between %u and %u;\n", $i, $i+999 } '
为了可读性,我在这里包装线,通常是一行。 以上命令的输出可以直接input到psql中:
perl -e '...' | psql -U ... -d ...
或者先到文件然后到psql(如果你稍后需要这个文件的话):
perl -e '...' > updates.partitioned.sql psql -U ... -d ... -f updates.partitioned.sql
我不是一个DBA,但是一个数据库devise,你经常需要更新3500万行,可能会有…问题。
一个简单的WHERE status IS NOT NULL
可能会加速一些事情(假设你有一个状态索引) – 不知道实际的用例,假设这是经常运行的,这3500万行中的很大一部分可能已经有一个空状态。
但是,您可以通过LOOP语句在查询中进行循环 。 我只是做一个小例子:
CREATE OR REPLACE FUNCTION nullstatus(count INTEGER) RETURNS integer AS $$ DECLARE i INTEGER := 0; BEGIN FOR i IN 0..(count/1000 + 1) LOOP UPDATE orders SET status = null WHERE (order_id > (i*1000) and order_id <((i+1)*1000)); RAISE NOTICE 'Count: % and i: %', count,i; END LOOP; RETURN 1; END; $$ LANGUAGE plpgsql;
然后可以通过类似于以下操作来运行:
SELECT nullstatus(35000000);
您可能需要select行数,但要注意确切的行数可能需要很长时间。 PostgreSQL wiki有一篇关于慢计数和如何避免它的文章 。
此外,RAISE NOTICE部分只是在那里跟踪脚本的距离。 如果您没有监控通知,或者不在意,最好不要这样做。