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


67

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

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

UPDATE orders SET status = null;

为了避免转移到主题之外的讨论,我们假定当前3500万列的所有status值都设置为相同(非null)值,从而使索引无用。

该语句的问题在于生效需要很长时间(仅是由于锁定),并且所有更改的行都被锁定,直到完成整个更新为止。此更新可能需要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中完成我想要的工作?


我不是PostgreSQL的人,但是您是否尝试过在status列上设置索引?
Kirtan

在这种情况下,这并没有太大帮助,因为绝大多数时间都花在了维护事务完整性上。我的例子可能有点误导;相反,想象我只想这样做:UPDATE命令SET status = null; 我上面说的所有内容仍然适用(但此处的索引显然无济于事)

实际上,我只是更新了问题以反映这一点。

尽管所有更新的行均被锁定,但FWIW仍应能够在运行时“选择”它们。postgresql.org/docs/7.2/static/locking-tables.html
rogerdpack

Answers:


44

列/行

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

任何UPDATEPostgreSQL的MVCC模型写入新版本的整行。如果并发事务更改同一行的任何列,则会出现耗时的并发问题。手册中的详细信息。知道并发事务不会触及同一可以避免一些可能的复杂性,但不会避免其他复杂性。

指数

为了避免转移到主题之外的讨论,我们假定当前3500万列的所有status值都设置为相同(非null)值,从而使索引无用。

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

性能

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

UPDATE orders SET status = null;

我了解您的目标是寻求更通用的解决方案(请参见下文)。但是要解决实际提出的问题:无论表大小如何,都可以在几毫秒内解决:

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

手册(最多Postgres 10):

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

手册(自Postgres 11起):

当添加了列ADD COLUMNDEFAULT 指定了非易失性时,将在声明时评估默认值,并将结果存储在表的元数据中。该值将用于所有现有行的列。如果未DEFAULT指定,则使用NULL。无论哪种情况都不需要重写表。

添加具有volatile的列DEFAULT或更改现有列的类型将需要重写整个表及其索引。[...]

和:

DROP COLUMN窗体不会物理删除该列,而只是使其对SQL操作不可见。表中随后的插入和更新操作将为该列存储一个空值。因此,删除列很快,但是不会立即减小表的磁盘大小,因为删除的列所占用的空间不会被回收。随着现有行的更新,空间将随着时间的推移而回收。

确保没有依赖于该列的对象(外键约束,索引,视图等)。您将需要删除/重新创建那些。除非这样,pg_attribute否则系统目录表上的微小操作即可完成工作。在表上需要排他锁,这可能会导致大量并发负载。(就像Buurman在其评论中强调的那样。)除非如此,该操作只需几毫秒。

如果您要保留默认的列,则将其添加回单独的命令中。在同一命令中执行此操作会立即将其应用于所有行。看到:

要实际应用默认值,请考虑分批执行:

一般解决方案

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

这样就可以运行一个函数,以较小的部分更新一个大表,并且每个部分都单独提交。避免增加大量行的事务开销,更重要的是,在每个部分之后释放锁。这允许并发操作继续进行而没有太多延迟,并且使死锁的可能性降低。

如果您没有并发访问权限,那么这几乎没有用-除非要避免ROLLBACK出现异常。还要考虑SAVEPOINT这种情况。

免责声明

首先,许多小额交易实际上更昂贵。这只对大桌子有意义。最佳位置取决于许多因素。

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

分步说明

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

使用dblink设置连接在很大程度上取决于数据库集群的设置和适当的安全策略。这可能很棘手。相关的稍后答案,以及更多如何与dblink连接的信息

按照此处的指示创建FOREIGN SERVERUSER 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注入:

避免空的更新:


1
请注意,根据答案中链接的文档(postgresql.org/docs/current/interactive/…),大多数ALTER TABLE操作(包括ADD COLUMN)都会在表上排它锁。这意味着操作本身可能很快,但是如果有足够多的其他线程在表(部分)上保持锁,则它可能花费很长时间等待排他锁,从而阻塞了进程中的其他(“较新”)访问操作。这意味着,尽管此操作很快,但仍可能会长时间挂起您的应用程序。
布曼

4

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

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

然后,您将状态为NULL的设置操作立即生效:

truncate order_status;

3

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

因此,如果确实需要5个小时,则可能是由于其他原因(例如,您确实有并发写入,与您声称没有并发写入相反)。


1
对于上述情况,我上面引用的时间(5小时,35分钟,〜3分钟)是准确的。我没有说数据库中没有其他写操作。只是我知道在进行更新时没有人会向该写入数据(系统完全没有使用此列,尽管行是读/写的)。换句话说,我不在乎这项工作是大笔交易还是小笔交易。我关心的是速度。我可以使用上述方法来提高速度,但是它们很麻烦。

1
目前尚不清楚是长时间运行是由于锁定,还是由于吸尘。尝试在更新之前获取表锁,从而锁定任何其他类型的操作。然后,您应该能够完成此更新而不会受到任何干扰。
Martin v。Löwis09年

3
如果我锁定所有其他类型的操作,则系统可能会停滞直到完成。我发布的两种将时间减少到35min / 3min的解决方案并不妨碍系统正常运行。我正在寻找的是一种无需每次执行这样的更新都需要编写脚本的方法(每次我要执行其中一个更新将节省5分钟)。

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;

可能是最好的解决方案,如果(但仅当)表的其他列在执行此操作时不会被修改。
fzzfzzfzz

3

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

也许有些行已经有 statusNULL?

如果是这样,则:

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

至于分区更改-在纯sql中是不可能的。所有更新都在单个事务中。

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

通常只需添加适当的即可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

感谢您的答复,但这与我在问题中的#3解决方案基本相同;基本上,这就是我已经做的。但是,编写这样的脚本需要5分钟,而我正在尝试找出一种方法,可以在psql中完成该任务,因此可以在20秒或更短的时间内完成(并消除潜在的错别字/错误)。这就是我要问的问题。

我想我已经回答了-无法在SQL中完成(除非使用dblink之类的技巧)。另一方面-我写了我在大约30秒内显示的那条线,所以看起来时间不多:)绝对比假设的5分钟脚本编写更接近您的20秒目标。

3
谢谢,但是我说“ SQL”时弄错了。实际上,我在问如何在PostgreSQL的psql控制台中使用任何可能的技巧,包括plgpsql,来做到这一点。如上所述编写脚本正是我现在正在做的。这需要30秒钟以上的时间,因为每次执行这些更新之一时,您都必须编写一个自定义的小型脚本,并且必须进行查询以查明您有多少行,并且必须确保没有行。我想做的事情是这样的:#select nonblocking_query('update order set status = null'); 这就是我要完成的工作。

这是我已经2次回答的问题:这是不可能的,除非您将使用dblink,但这比您不喜欢的单行代码还要复杂。

2

我绝不是DBA,但您经常需要更新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);

您可能要选择行数,但是要注意,准确的行数会花费很多时间。PostgreSQL Wiki上有一篇有关计数缓慢以及如何避免计数的文章。

另外,“ RAISE NOTICE”部分就在那里,用于跟踪脚本的执行距离。如果您不在监视通知,或者不在乎,最好将其省略。


这将无济于事,因为函数调用将在单个事务中进行-因此,锁定问题仍然存在。

嗯,我没有考虑–仍然,我认为这将比UPDATE命令SET status = null;快,因为那将意味着要进行全表扫描。
mikl,2009年

1
我了解对使用索引运行速度更快的查询的兴趣,但这并不是我真正关心的问题,因为在某些情况下,该列的每个值都相同,从而使索引无用。我真的很担心一次查询(5小时)和将其拆分(3分钟)之间的时间差异,并希望在psql中这样做而不必每次都编写脚本。我确实了解索引,以及如何通过使用索引来节省更多时间。

哦,回答您问题的第一部分:确实很少需要更新3500万行。这主要是为了清理;例如,我们可能决定:“为什么order_status ='a'对于订单表意味着'已接受'而对于发货表意味着'已废止'?我们应该使它们保持一致!” 因此,我们需要更新代码并对数据库进行大量更新以消除不一致之处。当然,这是一个抽象,因为我们实际上根本没有“订单”。


2

您确定这是因为锁定?我不这么认为,还有许多其他可能的原因。要找出答案,您始终可以尝试仅进行锁定。试试这个:BEGIN; SELECT NOW(); SELECT * FROM order FOR UPDATE; SELECT NOW(); 回滚;

要了解实际情况,您应该先运行EXPLAIN(EXPLAIN UPDATE订单SET状态...)和/或EXPLAIN ANALYZE。也许您会发现您没有足够的内存来有效地执行UPDATE。如果是这样,请将work_mem设置为'xxxMB';可能是一个简单的解决方案。

另外,在PostgreSQL日志尾部查看是否出现一些与性能相关的问题。


1

一些未提及的选项:

使用新的表格技巧。在这种情况下,您可能要做的是编写一些触发器来处理它,以便将对原始表的更改也传播到您的表副本中,诸如此类……(percona是执行此操作的示例触发方式)。另一个选择可能是“创建新列,然后用它替换旧列”的技巧,以避免锁定(不清楚是否有助于提高速度)。

可能会计算最大ID,然后生成“所需的所有查询”,并将其作为单个查询传递给它,就像这样update X set Y = NULL where ID < 10000 and ID >= 0; update X set Y = NULL where ID < 20000 and ID > 10000; ...,尽管您确实需要额外的逻辑来做,但它可能并没有做太多的锁定,仍然是全部SQL。 :(


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.