如何在PostgreSQL中进行大型非阻塞更新?

61 postgresql dblink transactions plpgsql sql-update

我想在PostgreSQL中对表进行大量更新,但我不需要在整个操作中维护事务完整性,因为我知道我正在更改的列不会被写入或读取更新.我想知道psql控制台中是否有一种简单的方法可以更快地完成这些类型的操作.

例如,假设我有一个名为"orders"的表,有3500万行,我想这样做:

UPDATE orders SET status = null;
Run Code Online (Sandbox Code Playgroud)

为避免被转移到offtopic讨论,让我们假设3500万列的所有状态值当前都设置为相同(非空)值,从而使索引无用.

此语句的问题是需要很长时间才能生效(仅因为锁定),并且所有更改的行都将被锁定,直到整个更新完成.此更新可能需要5个小时,而类似

UPDATE orders SET status = null WHERE (order_id > 0 and order_id < 1000000);
Run Code Online (Sandbox Code Playgroud)

可能需要1分钟.超过3500万行,执行上述操作并将其分成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) " +  ")");
}
Run Code Online (Sandbox Code Playgroud)

此操作可能仅在几分钟内完成,而不是35分钟.

所以这归结为我真正的要求.我不想写一个怪异的脚本来分解操作,每次我想做这样一个大的一次性更新.有没有办法在SQL中完成我想要的东西?

Erw*_*ter 38

列/行

...我不需要在整个操作中维护事务完整性,因为我知道我正在更改的列不会在更新期间写入或读取.

任何UPDATEPostgreSQL的MVCC模型写入新版本的整行.如果并发事务更改同一行的任何列,则会出现耗时的并发问题.手册中的详细信息.并发事务不会触及相同的,避免了一些可能的并发症,而不是其他的.

指数

为避免被转移到offtopic讨论,让我们假设3500万列的所有状态值当前都设置为相同(非空)值,从而使索引无用.

更新整个表(或其主要部分)时,Postgres 从不使用索引.当必须读取所有或大多数行时,顺序扫描更快.相反:指数维护意味着额外的成本UPDATE.

性能

例如,假设我有一个名为"orders"的表,有3500万行,我想这样做:

UPDATE orders SET status = null;
Run Code Online (Sandbox Code Playgroud)

我知道你的目标是更通用的解决方案(见下文).但要解决实际问题:无论表大小如何,都可以在几毫秒内处理:

ALTER TABLE orders DROP column status
                 , ADD  column status text;
Run Code Online (Sandbox Code Playgroud)

每个文件:

添加ADD COLUMN列时,表中的所有现有行都使用列的默认值进行初始化(NULL如果未DEFAULT 指定子句).如果没有DEFAULT子句,这只是一个元数据更改......

和:

DROP COLUMN形式不物理删除列,但只是让无形的SQL操作.表中的后续插入和更新操作将为列存储空值.因此,删除列很快,但不会立即减少表的磁盘大小,因为已删除列所占用的空间不会被回收.随着现有行的更新,该空间将随着时间的推移而被回收.(这些语句在删除系统oid列时不适用;这是通过立即重写完成的.)

确保根据列不具有对象(外键约束,索引,视图......).您需要删除/重新创建它们.除此之外,系统目录表上的微小操作可以pg_attribute完成这项工作.需要对表进行独占锁定,这可能是重度并发负载的问题.因为它只需要几毫秒,你应该仍然没问题.

如果您希望保留列默认值,请将其添加回单独的命令中.在同一个命令中执行此操作会立即将其应用于所有行,从而消除效果.然后,您可以批量更新现有列.请按照文档链接阅读手册中的注释.

一般解决方案

dblink已经在另一个答案中提到了.它允许在隐式单独连接中访问"远程"Postgres数据库."远程"数据库可以是当前数据库,从而实现"自治事务":函数在"远程"数据库中写入的内容已提交且无法回滚.

这允许运行单个函数,以更小的部分更新大表,并且每个部分都单独提交.避免为非常大的行构建事务开销,更重要的是,在每个部分之后释放锁.这允许并发操作在没有太多延迟的情况下继续进行并且使死锁的可能性降低.

如果您没有并发访问权限,这几乎没有用 - 除非ROLLBACK在异常后避免.同样考虑SAVEPOINT这种情况.

放弃

首先,许多小交易实际上更昂贵.这对大表来说才有意义.甜蜜点取决于许多因素.

如果您不确定自己在做什么:单个交易是安全的方法.为了使其正常工作,桌面上的并发操作必须发挥作用.例如:并发写入可以将行移动到应该已经处理的分区.或并发读取可以看到不一致的中间状态.你被警告了.

分步说明

需要首先安装附加模块dblink:

与dblink建立连接非常依赖于数据库集群的设置和安全策略.这可能很棘手.相关的后续回答以及如何与dblink连接:

创建一个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;
Run Code Online (Sandbox Code Playgroud)

呼叫:

SELECT f_update_in_steps();
Run Code Online (Sandbox Code Playgroud)

您可以根据需要参数化任何部分:表名,列名,值,...只需确保清理标识符以避免SQL注入:

关于避免空UPDATE:

  • 请注意,大多数 ALTER TABLE 操作(包括 ADD COLUMN)都会根据答案中链接的文档在表上放置排它锁(http://www.postgresql.org/docs/current/interactive/sql-altertable.html#AEN67134 )。这意味着,操作本身可能非常快,但如果有足够多的其他线程在表(部分)上持有锁,则它可能会花费很长时间等待排它锁,从而阻塞进程中的其他(“较新”)访问操作。这意味着此操作尽管速度很快,但仍有可能长时间挂起您的应用程序。 (2认同)

Tom*_*zky 5

您应该将此列委托给另一个表,如下所示:

create table order_status (
  order_id int not null references orders(order_id) primary key,
  status int not null
);
Run Code Online (Sandbox Code Playgroud)

那么你设置 status=NULL 的操作将是即时的:

truncate order_status;
Run Code Online (Sandbox Code Playgroud)


Mar*_*wis 3

Postgres 使用 MVCC(多版本并发控制),因此如果您是唯一的写入者,则可以避免任何锁定;任意数量的并发读取器都可以在该表上工作,并且不会有任何锁定。

因此,如果确实需要 5 小时,则一定是出于不同的原因(例如,您确实有并发写入,与您声称没有并发写入相反)。

  • 如果我锁定所有其他类型的操作,那么系统可能会面临停滞直至完成的风险。而我发布的将时间减少到 35 分钟/3 分钟的两个解决方案并不会阻止系统正常运行。我正在寻找一种方法来做到这一点,而不必每次我想做这样的更新时都编写脚本(这样每次我想做其中一个更新时都会节省 5 分钟)。 (4认同)
  • 我上面引用的时间(5 小时、35 分钟、~3 分钟)对于我上面描述的场景来说是准确的。我没有声明数据库中没有发生其他写入操作;只是我知道在我进行更新时没有人会写入*列*(系统根本不使用该列,但行已被读取/写入)。换句话说,我不在乎这项工作是在一笔巨大的交易中还是在较小的交易中处理;我关心的是速度。我可以使用上面的方法来提高速度,但它们很麻烦。 (2认同)