【问题标题】:How to Update a single column in postgres in a batch for 55 Million records如何为 5500 万条记录批量更新 postgres 中的单个列
【发布时间】:2021-04-14 12:30:52
【问题描述】:

我想更新一列 postgres 表。记录大约有 5500 万条,因此我们需要分批更新 10000 条记录。 注意:我们要更新所有行。但我们不想锁定我们的表。

我正在尝试以下查询 -

Update account set name = Some name where id between 1 and 10000

我们如何为每 10000 条记录更新创建一个循环?

我们将不胜感激任何建议和帮助。

PostgreSQL 10.5

【问题讨论】:

    标签: sql postgresql plpgsql


    【解决方案1】:

    与其一次提交所有更改(或其他答案中建议的 5500 万次),我宁愿尝试小批量拆分更新行,例如10k 条记录,如您所建议的。在 PL/pgSQL 中,可以使用关键字BY 以给定的步骤迭代集合。因此,您可以像这样在anonymous code block 中进行批量更新:

    PostgreSQL 11+

    DO $$ 
    DECLARE 
      page int := 10000;
      min_id bigint; max_id bigint;
    BEGIN
      SELECT max(id),min(id) INTO max_id,min_id FROM account;
      FOR j IN min_id..max_id BY page LOOP 
        UPDATE account SET name = 'your magic goes here'
        WHERE id >= j AND id < j+page;
        COMMIT;            
      END LOOP;
    END; $$;
    
    • 您可能需要调整WHERE 子句以避免不必要的重叠。

    测试

    具有 1051 行且具有顺序 ID 的数据样本:

    CREATE TABLE account (id int, name text);
    INSERT INTO account VALUES(generate_series(0,1050),'untouched record..');
    

    正在执行匿名代码块...

    DO $$ 
    DECLARE 
      page int := 100;
      min_id bigint; max_id bigint;
    BEGIN
      SELECT max(id),min(id) INTO max_id,min_id FROM account;
      FOR j IN min_id..max_id BY page LOOP 
        UPDATE account SET name = now() ||' -> UPDATED ' || j  || ' to ' || j+page
        WHERE id >= j AND id < j+page;
        RAISE INFO 'committing data from % to % at %', j,j+page,now();
        COMMIT;            
      END LOOP;
    END; $$;
        
    INFO:  committing data from 0 to 100 at 2021-04-14 17:35:42.059025+02
    INFO:  committing data from 100 to 200 at 2021-04-14 17:35:42.070274+02
    INFO:  committing data from 200 to 300 at 2021-04-14 17:35:42.07806+02
    INFO:  committing data from 300 to 400 at 2021-04-14 17:35:42.087201+02
    INFO:  committing data from 400 to 500 at 2021-04-14 17:35:42.096548+02
    INFO:  committing data from 500 to 600 at 2021-04-14 17:35:42.105876+02
    INFO:  committing data from 600 to 700 at 2021-04-14 17:35:42.114514+02
    INFO:  committing data from 700 to 800 at 2021-04-14 17:35:42.121946+02
    INFO:  committing data from 800 to 900 at 2021-04-14 17:35:42.12897+02
    INFO:  committing data from 900 to 1000 at 2021-04-14 17:35:42.134388+02
    INFO:  committing data from 1000 to 1100 at 2021-04-14 17:35:42.13951+02
    

    .. 你可以批量更新你的行。为了证明我的观点,以下查询按更新时间对记录进行分组:

    SELECT DISTINCT ON (name) name, count(id)
    FROM account 
    GROUP BY name ORDER BY name;
    
                             name                         | count 
    ------------------------------------------------------+-------
     2021-04-14 17:35:42.059025+02 -> UPDATED 0 to 100    |   100
     2021-04-14 17:35:42.070274+02 -> UPDATED 100 to 200  |   100
     2021-04-14 17:35:42.07806+02 -> UPDATED 200 to 300   |   100
     2021-04-14 17:35:42.087201+02 -> UPDATED 300 to 400  |   100
     2021-04-14 17:35:42.096548+02 -> UPDATED 400 to 500  |   100
     2021-04-14 17:35:42.105876+02 -> UPDATED 500 to 600  |   100
     2021-04-14 17:35:42.114514+02 -> UPDATED 600 to 700  |   100
     2021-04-14 17:35:42.121946+02 -> UPDATED 700 to 800  |   100
     2021-04-14 17:35:42.12897+02 -> UPDATED 800 to 900   |   100
     2021-04-14 17:35:42.134388+02 -> UPDATED 900 to 1000 |   100
     2021-04-14 17:35:42.13951+02 -> UPDATED 1000 to 1100 |    51
    

    演示:db&lt;&gt;fiddle

    【讨论】:

    • 我正在运行您的查询,出现错误。错误:无法在 pl/pgsql 中开始/结束事务上下文:pl/pgsql 函数 inline_code_block 第 9 行在 SQL 语句 SQL 状态:0A000
    • @codegeek 这很奇怪。您使用的是哪个 postgres 版本?您是否可能在同一事务中将代码块与其他查询一起运行?那会引发异常!请参阅我的 dbfiddle,我将 do 块与其他语句分开。它必须工作:-D
    • 好的,谢谢。我看到了,但我的版本是 10.5 在小提琴上它适用于高于 10 的版本。我检查了它适用于 11、12 和 13。任何低版本的解决方案。 :)
    • @codegeek 太不幸了。在像 10 这样的旧版本中,它不起作用。您可以尝试捕获异常,但这是一个丑陋的解决方法。对于此类问题,请始终提及您使用的是哪个版本。如果您使用的是 linux,则可以创建一个 bash 脚本并轻松重现此循环。除此之外,我没有看到任何适合您的优雅解决方案。
    • 是的,主键。不知道是什么问题,我现在不打扰,因为我已经完成了更新。您的查询非常有用。再次感谢!
    【解决方案2】:

    您可以使用procedure(从版本 11 开始提供)并一一进行,如下所示:

    CREATE or replace PROCEDURE do_update()
    LANGUAGE plpgsql
    AS $$
    BEGIN
        FOR i IN 1..55000000 -- 55 million, or whatever number you need
        LOOP 
    
            Update account set name = Some name where id = i;
            COMMIT;
            
            RAISE INFO 'id: %', i;
        END LOOP;
    END;
    $$;
    
    CALL do_update();
    

    【讨论】:

      【解决方案3】:

      设置测试环境:

      DROP TABLE IF EXISTS account;
      CREATE TABLE account(id integer, name text);
      
      INSERT INTO account
      VALUES (1, 'jonas'),(10002, 'petras');
      

      更新脚本:

      DO $$
      DECLARE
        _id integer;
        _min_id integer;
        _max_id integer; 
        _batch_size integer = 10000;
      BEGIN
        SELECT 
          MIN(id),
          MAX(id)
        INTO
          _min_id,
          _max_id
        FROM
          account;
      
        _id := _min_id;
      
        LOOP
          UPDATE account SET
            name = 'Some name' 
          WHERE id >=_id 
            AND id < _id + _batch_size;
      
          COMMIT;
      
          _id := _id + _batch_size;
          IF _id > _max_id THEN
            EXIT;
          END IF;
        END LOOP;
      END;
      $$;
      

      【讨论】:

        【解决方案4】:

        但我们不想锁定我们的表。

        在许多情况下都有意义但您没有透露您的实际设置。你甚至需要一把锁吗?是否有并发写入活动?如果没有,是否有足够的存储空间来写入该表的另一个副本?然后最好在后台构建一个新的原始更新表,然后切换并删除旧表。见:

        假设表的并发写入活动。而且您不想长时间阻止太多。而且您想重用死元组来防止表膨胀和索引膨胀。所以批量更新是有道理的。您必须在批次之间使用COMMIT(和VACUUM),以便可以重复使用死元组占用的空间。并将写入分散到整个表中,以允许连续事务在同一块中产生和消耗死元组。

        Postgres 11 或更新版本中的DO 语句中的过程或匿名代码块中允许使用事务控制语句(如COMMIT)。其他人则使用它来回答提供的解决方案。

        autovacuum 应该以激进的设置运行,以便及时释放死元组以供重用。或者在某些时间间隔手动运行VACUUM - 但(当前)根本不能在事务上下文中运行(仅作为单个命令),因此在 PL/pgSQL 循环中是不可能的。

        Postgres 10 或更早版本

        目前还不允许在代码块中进行事务控制。不过,我们可以使用dblink 模拟自主事务。见:

        可能看起来像:

        DO
        $do$
        DECLARE
           _cur  int := 0;  -- just start with 0 unless min is far off
           _step int := 10000;  -- batch size
           _max  CONSTANT int := (SELECT max(id) FROM account);  -- max id
           _val  CONSTANT text := 'SOME name';
        BEGIN
           -- as superuser, or you must also provide the password for the current role;
           PERFORM dblink_connect('dbname=' || current_database());  -- current db
        
           LOOP
              RAISE NOTICE '%', _cur;
              PERFORM dblink_exec(  -- committed implicitly!
                 $$
                 UPDATE account
                 SET    name = 'SOME name'
                 WHERE  id BETWEEN _cur AND _cur + _step   -- gaps don't matter unless huge
                 AND    name IS DISTINCT FROM 'SOME name'  -- avoid empty updates
                 $$);
        
              
              _cur := _cur + _step;
              EXIT WHEN _cur > _max;          -- stop when done
           END LOOP;
        
           PERFORM dblink_disconnect();
        END
        $do$;
        

        我还添加了另一个谓词:

             AND    name IS DISTINCT FROM 'SOME name'  -- avoid empty updates
        

        跳过已具有新名称的行的空更新成本。只有当这种情况发生时才有用。见:

        您可能希望进一步拆分它,并在其间运行VACUUM。您可能希望使用除id(未聚集的列)之外的其他列进行选择,以便在整个表格中获得良好的分布。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2017-12-01
          • 2012-05-30
          • 2018-05-28
          相关资源
          最近更新 更多