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

Posted

技术标签:

【中文标题】如何在 PostgreSQL 中进行大型非阻塞更新?【英文标题】:How do I do large non-blocking updates in PostgreSQL? 【发布时间】:2010-11-09 22:35:21 【问题描述】:

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

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

UPDATE orders SET status = null;

为了避免被转移到离题的讨论上,我们假设 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 中完成我想要的?

【问题讨论】:

我不是 PostgreSQL 人,但你试过在状态列上设置索引吗? 在这种情况下没有多大帮助,因为绝大多数时间都花在了维护事务完整性上。我的例子可能有点误导;相反,想象一下我只想这样做: UPDATE orders SET status = null;我上面所说的一切仍然适用(但这里的索引显然无济于事) 其实我只是更新了问题来反映这一点。 虽然所有更新的行都被锁定了,你仍然应该能够在它运行时“选择”它们,FWIW。 postgresql.org/docs/7.2/static/locking-tables.html 【参考方案1】:

列/行

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

PostgreSQL's MVCC model 中的任何 UPDATE 都会写入 整行 的新版本。如果并发事务更改同一行的任何列,则会出现耗时的并发问题。 Details in the manual. 知道并发事务不会触及相同的可以避免一些可能的复杂情况,但不能避免其他情况。

索引

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

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

性能

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

UPDATE orders SET status = null;

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

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

The manual (up to Postgres 10):

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

The manual (since Postgres 11):

当使用ADD COLUMN 和非易失性DEFAULT 添加列时 指定时,在语句时评估默认值 并将结果存储在表的元数据中。将使用该值 对于所有现有行的列。如果没有指定DEFAULT, 使用 NULL。在这两种情况下都不需要重写表。

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

还有:

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

确保您没有依赖于列的对象(外键约束、索引、视图...)。您需要删除/重新创建这些。除此之外,系统目录表pg_attribute 上的微小操作就可以完成这项工作。需要在表上使用独占锁,这可能是并发负载过重的问题。 (就像 Buurman 在他的 comment 中强调的那样。)除此之外,操作只需几毫秒。

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

Add new column without table lock?

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

Does PostgreSQL optimize adding columns with non-NULL DEFAULTs?

一般解决方案

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

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

如果您没有并发访问权限,这几乎没有用 - 除了在异常之后避免ROLLBACK。这种情况也可以考虑SAVEPOINT

免责声明

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

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

分步说明

需要先安装附加模块dblink:

How to use (install) dblink in PostgreSQL?

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

Persistent inserts in a UDF even if the function aborts

按照那里的说明创建一个 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 注入:

Table name as a PostgreSQL function parameter

避免空更新:

How do I (or can I) SELECT DISTINCT on multiple columns?

【讨论】:

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

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

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

【讨论】:

我上面引用的时间(5 小时,35 分钟,~3 分钟)对于我上面描述的场景是准确的。我没有说数据库中没有发生其他写入;只是我知道在我进行更新时没有人会写入 column (系统根本没有使用此列,但行是读/写的) .换句话说,我不在乎这项工作是在一个巨大的交易中处理还是在较小的部分中处理;我关心的是速度。而且我可以使用上面的方法来提高速度,但是它们很麻烦。 目前尚不清楚长时间运行是由于锁定还是真空吸尘造成的。尝试在更新之前获取表锁,锁定任何其他类型的操作。那么您应该能够在不受任何干扰的情况下完成此更新。 如果我锁定所有其他类型的操作,那么系统可能会在完成之前停止。而我发布的将时间减少到 35 分钟/3 分钟的两个解决方案不会阻止系统正常运行。我正在寻找一种方法,无需每次我想进行这样的更新时都编写脚本(每次我想进行其中一个更新时,这可以节省我 5 分钟)。【参考方案3】:

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

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

那么你设置status=NULL的操作就立竿见影了:

truncate order_status;

【讨论】:

【参考方案4】:

我会使用 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】:

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

也许有些行已经有status NULL?

如果是,那么:

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 秒内展示的那条线,所以它看起来不会花太多时间 :) 它肯定更接近你的 20 秒目标,而不是假设的 5 分钟脚本编写。跨度> 谢谢,但是当我说“SQL”时我说错了;事实上,我在问如何在 PostgreSQL 的 psql 控制台中使用任何可能的技巧,包括 plgpsql。像上面那样编写脚本正是我现在正在做的事情。这需要超过 30 秒,因为每次执行这些更新时都必须编写自定义迷你脚本,并且必须执行查询以找出您有多少行,并且必须确保没有错别字等。我想做的是:#select nonblocking_query('update orders set status=null');这就是我想要实现的目标。 这是我已经回答过 2 次的问题:这是不可能的,除非你会使用 dblink,但这比你不喜欢的那些单行代码还要复杂。【参考方案6】:

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

一个简单的WHERE status IS NOT NULL 可能会加快速度(前提是您有关于状态的索引)——不知道实际用例,我假设如果经常运行,3500 万行中的很大一部分可能已经有一个空状态。

但是,您可以通过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 部分只是用来跟踪脚本的进度。如果您不监视通知,或者不在乎,最好将其排除在外。

【讨论】:

这无济于事,因为函数调用将在单个事务中进行 - 因此,锁定问题仍然存在。 嗯,我没有考虑过——不过,我认为这会比 UPDATE 命令 SET status = null; 更快,因为这意味着全表扫描。 我理解对使用索引更快地运行查询的兴趣,但这并不是我真正关心的问题,因为在某些情况下,列的每个值都是相同的,从而使索引变得无用。我真的很担心将这个查询作为一个操作(5 小时)运行和将其分解为多个部分(3 分钟)之间的时间差异,并且希望在 psql 中这样做,而不必每次都编写脚本。我确实知道索引以及如何通过使用它们来节省这些操作的更多时间。 哦,回答您问题的第一部分:确实很少需要更新 3500 万行。这主要用于清理;例如,我们可能会决定,“为什么 order_status = 'a' 表示 order 表的 'accepted' 而 shipping 表的 'annuled'?我们应该使它们保持一致!”所以我们需要更新代码并对数据库进行大规模更新以清理不一致。当然这是一个抽象,因为我们实际上根本没有“订单”。 目前在 postgres 中嵌套事务的答案似乎是“使用 dblink”:\【参考方案7】:

您确定这是因为锁定吗?我不这么认为,还有很多其他可能的原因。要找出答案,您可以随时尝试只进行锁定。尝试这个: 开始; 现在选择(); SELECT * FROM 更新订单; 现在选择(); 回滚;

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

另外,跟踪 PostgreSQL 日志以查看是否出现一些与性能相关的问题。

【讨论】:

【参考方案8】:

一些没有提到的选项:

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

可能计算最大 ID,然后生成“您需要的所有查询”并将它们作为单个查询(如 update X set Y = NULL where ID &lt; 10000 and ID &gt;= 0; update X set Y = NULL where ID &lt; 20000 and ID &gt; 10000; ...)传递,然后它可能不会执行那么多锁定,并且仍然是所有 SQL,尽管您确实有额外的预先做的逻辑:(

【讨论】:

【参考方案9】:

PostgreSQL 版本 11 使用 Fast ALTER TABLE ADD COLUMN with a non-NULL default 功能自动为您处理此问题。如果可能,请升级到版本 11。

blog post 中提供了解释。

【讨论】:

以上是关于如何在 PostgreSQL 中进行大型非阻塞更新?的主要内容,如果未能解决你的问题,请参考以下文章

使用 PostgreSQL 在 SORM 中进行非敏感搜索

如何在 PostgreSQL 中进行 UPSERT(合并、插入……重复更新)?

如何对 postgresql 中具有联合的查询进行更新?

MPI 将阻塞转换为非阻塞问题

在大型数据集(~3M 条目)上使用 PostgreSQL 进行特征工程

如何使 Ignite 像使用 TcpDiscoveryMulticastIpFinder 一样使用 TcpDiscoveryVmIpFinder 进行非阻塞行为?