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

标签 postgresql transactions sql-update plpgsql dblink

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

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

UPDATE orders SET status = null;

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

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

最佳答案

列/行

... I don't need the transactional integrity to be maintained across the entire operation, because I know that the column I'm changing is not going to be written to or read during the update.


任意 UPDATEPostgreSQL's MVCC model写了的新版本整排 .如果并发事务改变了同一行的任何列,就会出现耗时的并发问题。 Details in the manual.知道并发事务不会触及同一列可以避免一些可能的并发症,但不会避免其他并发症。
索引

To avoid being diverted to an offtopic discussion, let's assume that all the values of status for the 35 million columns are currently set to the same (non-null) value, thus rendering an index useless.


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

For example, let's say I have a table called "orders" with 35 million rows, and I want to do this:


UPDATE orders SET status = null;

我知道您的目标是更通用的解决方案(见下文)。但地址实际问题问:这个可以在处理一毫秒 , 无论表大小:
ALTER TABLE orders DROP column status
                 , ADD  column status text;
The manual (up to Postgres 10):

When a column is added with ADD COLUMN, all existing rows in the table are initialized with the column's default value (NULL if no DEFAULT clause is specified). If there is no DEFAULT clause, this is merely a metadata change [...]


The manual (since Postgres 11):

When a column is added with ADD COLUMN and a non-volatile DEFAULT is specified, the default is evaluated at the time of the statement and the result stored in the table's metadata. That value will be used for the column for all existing rows. If no DEFAULT is specified, NULL is used. In neither case is a rewrite of the table required.

Adding a column with a volatile DEFAULT or changing the type of an existing column will require the entire table and its indexes to be rewritten. [...]


和:

The DROP COLUMN form does not physically remove the column, but simply makes it invisible to SQL operations. Subsequent insert and update operations in the table will store a null value for the column. Thus, dropping a column is quick but it will not immediately reduce the on-disk size of your table, as the space occupied by the dropped column is not reclaimed. The space will be reclaimed over time as existing rows are updated.


确保您没有依赖于列的对象(外键约束、索引、 View 等)。您需要删除/重新创建那些。除此之外,对系统目录表的微小操作pg_attribute做这份工作。需要 排他锁 在表上这可能是重并发负载的问题。 (就像 Buurman 在他的 comment 中强调的那样。)除此之外,该操作只需几毫秒。
如果您要保留列默认值,请将其添加回单独的命令中。在同一命令中执行此操作会立即将其应用于所有行。看:
  • Add new column without table lock?

  • 要实际应用默认值,请考虑分批执行:
  • Does PostgreSQL optimize adding columns with non-NULL DEFAULTs?

  • 通用解决方案
    dblink 已经在另一个答案中提到了。它允许在隐式独立连接中访问“远程”Postgres 数据库。 “远程”数据库可以是当前数据库,从而实现“自治事务”:函数写入“远程”数据库的内容已提交,无法回滚。
    这允许运行单个函数,该函数以较小的部分更新大表,并且每个部分都单独提交。避免为大量行增加事务开销,更重要的是,在每个部分之后释放锁。这允许并发操作在没有太多延迟的情况下进行,并减少死锁的可能性。
    如果您没有并发访问,这几乎没有用 - 除非避免 ROLLBACK异常后。还可以考虑 SAVEPOINT 对于这种情况。
    免责声明
    首先,很多小额交易实际上更昂贵。此 只对大表有意义 .甜蜜点取决于许多因素。
    如果您不确定自己在做什么:单笔交易才是安全的方法 .为了使其正常工作,表上的并发操作必须同时进行。例如:并发写入可以将一行移动到据称已经处理的分区。或者并发读取可以看到不一致的中间状态。你被警告了。
    分步说明
    需要先安装附加模块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 注入(inject):
  • Table name as a PostgreSQL function parameter

  • 避免空更新:
  • How do I (or can I) SELECT DISTINCT on multiple columns?
  • 关于postgresql - 如何在 PostgreSQL 中进行大型非阻塞更新?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/1113277/

    相关文章:

    multithreading - 我如何在Neo4j中使用并行事务?

    mysql - 使用不同的值更新多行

    c++ - 未定义对 PQfinish 的引用,即使包含库等

    postgresql - SQL窗口函数检测列值的变化

    spring - 为什么在 @Transactional block 内没有调用 AbstractRoutingDataSource.defineCurrentLookupKey() ?

    mysql - 在 MySQL 中更新 1 列中的多行

    java - 使用 Hibernate 仅更新特定列

    sql - 在 Postgresql 中,如何按列选择前 n% 的行?

    sql - 如何在主键上创建 SQL 约束以确保它只能被引用一次?

    nhibernate - 工作单元实现