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

时间:2009-07-11 08:46:42

标签: postgresql transactions sql-update plpgsql dblink

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

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

UPDATE orders SET status = null;

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

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

UPDATE orders SET status = null WHERE (order_id > 0 and order_id < 1000000);

可能需要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) " +  ")");
}

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

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

9 个答案:

答案 0 :(得分:36)

列/行

  

...我不需要保持交易完整性   整个操作,因为我知道我改变的列是   在更新期间不会被写入或读取。

PostgreSQL's MVCC model中的任何UPDATE都会写入新版 整行 。如果并发事务更改同一行的任何列,则会出现耗时的并发问题。 Details in the manual.了解相同的并不会被并发事务所触及,这可以避免某些可能出现的并发症,而不是其他的。

索引

  

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

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

性能

  

例如,假设我有一个名为&#34; order&#34;有3500万   行,我想这样做:

UPDATE orders SET status = null;

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

ALTER TABLE orders DROP column status
                 , ADD  column status text;

Per documentation:

  

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

  

DROP COLUMN表单不会物理删除列,只是简单地删除   使它对SQL操作不可见。随后插入和更新   表中的操作将为列存储空值。从而,   删除列很快但不会立即减少   表的磁盘大小,作为丢弃的空间占用的空间   列未回收。随着时间的推移,这个空间将被回收   现有行已更新。 (这些陈述不适用于   丢弃系统列;这是通过立即重写完成的。)

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

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

一般解决方案

另一个答案中提到了{p> dblink。它允许访问&#34; remote&#34; Postgres数据库隐式单独连接。 &#34;遥控器&#34;数据库可以是当前数据库,从而实现&#34;自治事务&#34; :函数在&#34; remote&#34;中写入的内容。 db已提交且无法回滚。

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

如果您没有并发访问权限,这几乎没有用 - 除了在异常后避免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;

呼叫:

SELECT f_update_in_steps();

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

关于避免空UPDATE:

答案 1 :(得分:4)

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

create table order_status (
  order_id int not null references orders(order_id) primary key,
  status int not null
);

然后你设置status = NULL的操作将是即时的:

truncate order_status;

答案 2 :(得分:3)

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

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

答案 3 :(得分:3)

首先 - 您确定需要更新所有行吗?

也许有些行已经有status NULL?

如果是,那么:

UPDATE orders SET status = null WHERE status is not null;

至于对更改进行分区 - 这在纯sql中是不可能的。所有更新都在单笔交易中。

在“纯sql”中执行此操作的一种可能方法是安装dblink,使用dblink连接到同一个数据库,然后通过dblink发出大量更新,但对于这样一个简单的任务来说似乎有点过分了。 / p>

通常只需添加适当的where即可解决问题。如果没有 - 只需手动分区。编写脚本太多了 - 你通常可以用简单的单行编写:

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
    }
'

为了便于阅读,我在这里包裹了一行,通常它只是一行。上述命令的输出可以直接输入psql:

perl -e '...' | psql -U ... -d ...

或者首先发送文件然后发送到psql(如果以后需要该文件):

perl -e '...' > updates.partitioned.sql
psql -U ... -d ... -f updates.partitioned.sql

答案 4 :(得分:3)

我会使用CTAS:

begin;
create table T as select col1, col2, ..., <new value>, colN from orders;
drop table orders;
alter table T rename to orders;
commit;

答案 5 :(得分:2)

我绝不是DBA,但是经常需要更新3500万行的数据库设计可能会有......问题。

一个简单的WHERE status IS NOT NULL可能会加速一些事情(如果你有一个状态索引) - 不知道实际的用例,我假设这是经常运行的,很大一部分35百万行可能已经处于空状态。

但是,您可以通过LOOP statement在查询中创建循环。我只是做一个小例子:

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);

您可能希望选择行计数,但请注意确切的行计数可能需要很长时间。 PostgreSQL wiki有一篇关于slow counting and how to avoid it的文章。

另外,RAISE NOTICE部分就是跟踪脚本的距离。如果您没有监控通知,或者不在乎,最好将其删除。

答案 6 :(得分:2)

你确定这是因为锁定吗?我不这么认为,还有很多其他可能的原因。要找出答案,你总是可以尝试做锁定。试试这个: 开始; SELECT NOW(); SELECT * FROM order FOR UPDATE; SELECT NOW(); ROLLBACK;

要了解实际发生的情况,首先应运行EXPLAIN(EXPLAIN UPDATE命令SET状态...)和/或EXPLAIN ANALYZE。也许你会发现你没有足够的内存来有效地进行UPDATE。如果是这样,SET work_mem TO'xxxMB';可能是一个简单的解决方案。

另外,请关闭PostgreSQL日志,看看是否出现了一些与性能相关的问题。

答案 7 :(得分:1)

尚未提及的一些选项:

使用new table技巧。在你的情况下你可能需要做的就是编写一些触发器来处理它,以便原始表的更改也会传播到你的表副本,就像这样......(percona就是一个例子做触发方式的东西)。另一种选择可能是&#34;创建一个新列然后用它替换旧列&#34; trick,以避免锁定(不清楚是否有助于提高速度)。

可能会计算最大ID,然后生成&#34;您需要的所有查询&#34;并将它们作为单个查询传递给# home_controller.rb class HomeController < ApplicationController def index; end end 然后它可能没有那么多锁定,仍然是所有SQL,尽管你事先有额外的逻辑来执行它:(

答案 8 :(得分:0)

PostgreSQL版本11通过Fast ALTER TABLE ADD COLUMN with a non-NULL default功能自动为您处理。请尽可能升级到版本11。

blog post中提供了解释。