74

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

たとえば、3,500 万行の "orders" というテーブルがあり、次のようにしたいとします。

UPDATE orders SET status = null;

話題から外れた議論に転用されるのを避けるために、3500 万の列の status のすべての値が現在同じ (null 以外の) 値に設定されているため、インデックスが役に立たないと仮定します。

このステートメントの問題は、有効になるまでに非常に長い時間がかかり (単にロックが原因で)、変更されたすべての行が更新全体が完了するまでロックされることです。この更新には 5 時間かかる場合がありますが、

UPDATE orders SET status = null WHERE (order_id > 0 and order_id < 1000000);

1 分かかる場合があります。3,500 万行を超える場合、上記を実行して 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 内で目的を達成する方法はありますか?

4

9 に答える 9

45

列行

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

PostgreSQL の MVCC モデルのAnyUPDATE、行全体の新しいバージョンを書き込みます。同時トランザクションが同じ行のいずれかの列を変更すると、時間のかかる同時実行の問題が発生します。詳細はマニュアルにて。同じが同時トランザクションによって処理されないことがわかっていると、いくつかの複雑な問題を回避できますが、他の問題は回避できません。

索引

話題から外れた議論に転用されるのを避けるために、3500 万の列の status のすべての値が現在同じ (null 以外の) 値に設定されているため、インデックスが役に立たないと仮定します。

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

パフォーマンス

たとえば、3,500 万行の "orders" というテーブルがあり、次のようにしたいとします。

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 が指定されている場合、デフォルトはステートメントの時点で評価され、結果はテーブルのメタデータに格納されます。その値は、既存のすべての行の列に使用されます。noDEFAULTを指定すると、NULL が使用されます。どちらの場合も、テーブルの書き換えは必要ありません。

volatile を持つ列を追加しDEFAULTたり、既存の列の型を変更したりするには、テーブル全体とそのインデックスを書き換える必要があります。[...]

と:

フォームは列を物理的に削除するのDROP COLUMNではなく、単に SQL 操作から見えなくするだけです。テーブルでの後続の挿入操作と更新操作では、列に null 値が格納されます。したがって、列を削除するのは簡単ですが、削除された列が占有していた領域が再利用されないため、テーブルのディスク上のサイズがすぐに減少するわけではありません。既存の行が更新されると、時間の経過とともにスペースが再利用されます。

列に依存するオブジェクト (外部キー制約、インデックス、ビューなど) がないことを確認してください。それらを削除/再作成する必要があります。それを除けば、システム カタログ テーブルに対する小さな操作が機能しますpg_attributeテーブルに排他ロックが必要です。これは、同時負荷が大きい場合に問題になる可能性があります。(Buurman が彼のコメントで強調しているように。) それを除いて、操作は数ミリ秒の問題です。

保持したい列のデフォルトがある場合は、別のコマンドで追加し直します。同じコマンドで実行すると、すぐにすべての行に適用されます。見る:

デフォルトを実際に適用するには、バッチで行うことを検討してください。

一般的な解決策

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

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

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

免責事項

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

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

ステップバイステップの説明

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

dblink を使用した接続の設定は、DB クラスターの設定と適用されているセキュリティ ポリシーに大きく依存します。それは難しいかもしれません。dblink に接続する方法の詳細については、後で関連する回答を参照してください。

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

空の UPDATE を避ける:

于 2014-03-04T05:27:53.527 に答える
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 に答える
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 に答える
2

これはロックが原因だと確信していますか?私はそうは思いませんし、他にも多くの理由が考えられます。調べるために、いつでもロックだけを試すことができます。これを試してください: BEGIN; 今すぐ選択(); SELECT * FROM order FOR UPDATE; 今すぐ選択(); ロールバック;

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

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

于 2009-07-14T21:07:54.997 に答える