簡體   English   中英

如何為 5500 萬條記錄批量更新 postgres 中的單個列

[英]How to Update a single column in postgres in a batch for 55 Million records

我想更新一列 postgres 表。 記錄大約有 5500 萬條,因此我們需要分批更新 10000 條記錄。 注意:我們要更新所有行。 但是我們不想鎖定我們的表。

我正在嘗試以下查詢-

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

我們如何為每 10000 條記錄更新創建一個循環?

任何建議和幫助將不勝感激。

PostgreSQL 10.5

而不是一次提交所有更改(或其他答案中建議的 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<>fiddle

您可以使用一個過程(從版本 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();

設置測試環境:

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;
$$;

但是我們不想鎖定我們的表。

在許多情況下都有意義但您沒有透露您的實際設置。 你甚至需要一把鎖嗎? 是否有並發寫入活動? 如果沒有,是否有足夠的存儲空間來寫入該表的另一個副本? 然后最好在后台構建一個新的原始更新表,然后切換並刪除舊表。 看:

假設表的並發寫入活動。 而且您不想長時間阻止太多。 並且您希望重用死元組以防止表膨脹和索引膨脹。 所以批量更新是有道理的。 您必須在批次之間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以外的其他列進行選擇(未聚集的列),以便在整個表中獲得良好的分布。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM