74

我想对 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 中完成我想要的?

4

9 回答 9

45

列/行

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

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

指数

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

在更新整个表(或它的主要部分)时,Postgres从不使用索引。当必须读取所有或大多数行时,顺序扫描会更快。相反:索引维护意味着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 COLUMN并指定了非易失性列DEFAULT 时,将在语句执行时评估默认值,并将结果存储在表的元数据中。该值将用于所有现有行的列。如果DEFAULT指定 no,则使用 NULL。在这两种情况下都不需要重写表。

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

和:

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

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

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

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

一般解决方案

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

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

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

免责声明

首先,很多小额交易实际上更贵。这仅对大桌子有意义。甜蜜点取决于许多因素。

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

分步说明

附加模块 dblink 需要先安装:

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

按照那里的说明创建 aFOREIGN SERVER和 aUSER 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 注入:

避免空更新:

于 2014-03-04T05:27:53.527 回答
3

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

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

于 2009-07-11T09:17:21.207 回答
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;
于 2009-07-14T11:50:43.293 回答
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
于 2009-07-11T10:24:58.073 回答
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;
于 2011-08-18T11:12:44.773 回答
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 部分只是用来跟踪脚本的距离。如果您不监视通知或不在乎,最好将其排除在外。

于 2009-07-11T09:25:44.030 回答
2

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

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

此外,跟踪 PostgreSQL 日志以查看是否发生了一些与性能相关的问题。

于 2009-07-14T21:07:54.997 回答
1

PostgreSQL 版本 11使用具有非 NULL 默认功能的快速 ALTER TABLE ADD COLUMN 自动为您处理此问题。如果可能,请升级到版本 11。

此博客文章中提供了解释。

于 2019-05-28T23:29:00.180 回答
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,尽管您确实有额外的逻辑来做它:(

于 2017-11-23T20:07:12.713 回答