web-dev-qa-db-ja.com

PostgreSQLで大規模な非ブロッキング更新を行うにはどうすればよいですか?

PostgreSQLのテーブルで大規模な更新を行いたいのですが、操作中にトランザクションの整合性を維持する必要はありません。更新。これらのタイプの操作を高速化する簡単な方法psqlコンソール内があるかどうか知りたいです。

たとえば、3500万行の「orders」というテーブルがあり、これを実行したいとします。

UPDATE orders SET status = null;

オフトピックの議論に流されることを避けるために、3500万列のステータスのすべての値が現在同じ(null以外の)値に設定されているため、インデックスが役に立たないと仮定しましょう。

このステートメントの問題は、(ロックのみのため)有効になるまでに非常に長い時間がかかり、更新全体が完了するまで、変更されたすべての行がロックされることです。この更新には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分ではなく、数分で完了する場合があります。

それは、私が本当に求めていることです。このような大規模な1回限りの更新を行いたいときは、毎回操作を分割するような異常なスクリプトを書きたくありません。完全にSQL内で必要なことを達成する方法はありますか?

61
S D

列行

...変更中の列は更新中に読み書きされないことがわかっているため、操作全体でトランザクションの整合性を維持する必要はありません。

PostgreSQLのMVCCモデルUPDATEは、行全体の新しいバージョンを書き込みます。同時トランザクションが同じ行のany列を変更すると、時間がかかる同時実行の問題が発生します。 マニュアルの詳細 同じが同時トランザクションに触れないことは、someの可能性のある複雑化を回避しますが、他の複雑化は回避しません。

索引

オフトピックの議論に流されることを避けるために、3500万列のステータスのすべての値が現在同じ(null以外の)値に設定されているため、インデックスが役に立たないと仮定しましょう。

テーブル全体(またはその主要部分)Postgresインデックスを使用しないを更新するとき。すべてまたはほとんどの行を読み取る必要がある場合、順次スキャンは高速です。それどころか:インデックスのメンテナンスは、UPDATEの追加コストを意味します。

性能

たとえば、3500万行の「orders」というテーブルがあり、これを実行したいとします。

UPDATE orders SET status = null;

より一般的な解決策を目指していることを理解しています(以下を参照)。しかし、実際の質問に対処するには:これはミリ秒単位で処理できます。テーブルサイズ:

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

ドキュメントごと:

ADD COLUMNで列が追加されると、テーブル内のすべての既存の行は列のデフォルト値(NULL句が指定されていない場合はDEFAULT)で初期化されます。 DEFAULT句がない場合、これは単なるメタデータの変更です...

そして:

DROP COLUMNフォームは、列を物理的に削除するのではなく、単にSQL操作から見えないようにします。テーブル内の後続の挿入および更新操作では、列のNULL値が格納されます。したがって、列の削除は迅速ですが、削除された列が占有していたスペースは再利用されないため、テーブルのディスク上のサイズはすぐには縮小されません。既存の行が更新されると、時間の経過とともにスペースが回収されます。 (これらのステートメントは、システムOID列をドロップする場合には適用されません。これは、即時の書き換えで行われます。)

列に依存するオブジェクト(外部キー制約、インデックス、ビューなど)がないことを確認してください。それらをドロップ/再作成する必要があります。それを除けば、システムカタログ表に対する小さな操作pg_attributeが仕事をします。テーブルにexclusive lockが必要です。これは、同時ロードが重い場合に問題になる可能性があります。数ミリ秒しかかからないので、それでも大丈夫です。

維持したい列のデフォルトがある場合は、別のコマンドでを追加して戻します。同じコマンドで実行すると、すべての行にすぐに適用され、効果が無効になります。その後、 batches の既存の列を更新できます。ドキュメントのリンクをたどり、マニュアルのを読んでください。

一般的な解決策

dblink は別の回答で言及されています。暗黙的な個別の接続で「リモート」Postgresデータベースにアクセスできます。 「リモート」データベースは現在のものである可能性があり、それによって"自律トランザクション"を実現します。「リモート」データベースに関数が書き込む内容はコミットされ、ロールバックできません。

これにより、小さなパーツの大きなテーブルを更新する単一の関数を実行でき、各パーツは個別にコミットされます。非常に多数の行のトランザクションオーバーヘッドの蓄積を回避し、さらに重要なことに、各部分の後にロックを解放します。これにより、同時操作が大幅に遅れることなく進行し、デッドロックが発生する可能性が低くなります。

同時アクセスがない場合、これはほとんど役に立ちません-例外の後のROLLBACKを避けることを除いて。その場合は SAVEPOINT も検討してください。

免責事項

まず第一に、多くの小さなトランザクションは実際にはより高価です。この大きなテーブルでのみ意味があります。スイートスポットは多くの要因に依存します。

何をしているのかわからない場合:単一のトランザクションが安全な方法です。これが適切に機能するためには、テーブルでの同時操作が一緒に行われなければなりません。たとえば、同時書き込みは、すでに処理されていると思われるパーティションに行を移動できます。または、同時読み取りは、一貫性のない中間状態を見ることができます。 警告されました。

段階的な手順

追加のモジュールdblinkを最初にインストールする必要があります。

Dblinkとの接続のセットアップは、DBクラスターのセットアップと適切なセキュリティポリシーに大きく依存します。難しいかもしれません。dblinkで接続する方法については、後の関連する回答をご覧ください。

FOREIGN SERVERUSER 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インジェクションを避けるために、必ず識別子をサニタイズしてください:

空のUPDATEの回避について:

37

この列を次のような別のテーブルに委任する必要があります。

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
Tometzky

まず、すべての行を更新する必要がありますか?

おそらく、いくつかの行にはすでにstatus NULLがありますか?

その場合、次に:

UPDATE orders SET status = null WHERE status is not null;

変更のパーティション分割に関しては、純粋なSQLでは不可能です。すべての更新は単一のトランザクションにあります。

「純粋なsql」でそれを行う1つの方法は、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
    }
'

ここでは読みやすくするために行をラップしていますが、通常は1行です。上記のコマンドの出力は、psqlに直接送ることができます。

Perl -e '...' | psql -U ... -d ...

または、最初にファイルし、次にpsqlにします(後でファイルが必要になる場合):

Perl -e '...' > updates.partitioned.sql
psql -U ... -d ... -f updates.partitioned.sql
3
user80168

私はCTASを使用します:

begin;
create table T as select col1, col2, ..., <new value>, colN from orders;
drop table orders;
alter table T rename to orders;
commit;
3
mys

これはロックのせいですか?私はそうは思いませんし、他にも多くの理由が考えられます。調べるには、常にロックのみを試みることができます。これを試してください:BEGIN; SELECT NOW(); SELECT * FROM FOR UPDATE; SELECT NOW(); ROLLBACK;

実際に何が起こっているのかを理解するには、最初にEXPLAIN(EXPLAIN UPDATE orders SET status ...)および/またはEXPLAIN ANALYZEを実行する必要があります。たぶん、UPDATEを効率的に行うのに十分なメモリがないことがわかるでしょう。その場合、SET work_mem TO 'xxxMB';簡単な解決策かもしれません。

また、PostgreSQLログを追跡して、パフォーマンス関連の問題が発生するかどうかを確認します。

2
Martin Torhage

PostgresはMVCC(マルチバージョン同時実行制御)を使用するため、あなたが唯一のライターである場合、ロックを回避できます。任意の数の同時リーダーがテーブルで作業でき、ロックは発生しません。

したがって、実際に5時間かかる場合は、別の理由(たとえば、do同時書き込みがあり、そうではないという主張に反する)である必要があります。

2

私は決してDBAではありませんが、3500万行を頻繁に更新しなければならないデータベース設計には、問題があるかもしれません。

シンプルな WHERE status IS NOT NULLは、状況をかなり高速化する可能性があります(ステータスに関するインデックスがある場合)–実際のユースケースがわからないため、これが頻繁に実行される場合、3500万行の大部分がすでにnullステータスになっている可能性があります。

ただし、クエリ内で 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部分は、スクリプトがどれだけ遠くにあるかを追跡するためだけにあります。通知を監視していない場合、または気にしない場合は、除外する方が良いでしょう。

2
mikl

言及されていないいくつかのオプション:

new table トリックを使用します。おそらくあなたの場合にあなたがしなければならないことは、元のテーブルへの変更もテーブルコピーに伝播されるように、それを処理するためのいくつかのトリガーを書くことです...( percona はトリガー方法の例です)。別のオプションは、ロックを回避するための「新しい列を作成してから古い列をそれに置き換える」 trick かもしれません(速度に役立つかどうか不明です)。

おそらく最大IDを計算し、「必要なすべてのクエリ」を生成し、update X set Y = NULL where ID < 10000 and ID >= 0; update X set Y = NULL where ID < 20000 and ID > 10000; ...のような単一のクエリとして渡すと、ロックはそれほど行われず、すべてのSQLになりますが、追加のロジックがありますそれを行う前に:(

1
rogerdpack

PostgreSQLバージョン11は、これを NULL以外のデフォルトでのALTER TABLE ADD COLUMNの高速化 機能で自動的に処理します。可能であれば、バージョン11にアップグレードしてください。

説明はこの ブログ投稿 で提供されています。

0
axiopisty