大きなテーブルに新しい列を追加する最良の方法は?


33

Postgresには7,801,611行の2.2 GBのテーブルがあります。uuid / guid列を追加していますが、その列にデータを入力する最良の方法は何ですか(NOT NULL制約を追加したいので)。

Postgresを正しく理解している場合、更新は技術的には削除と挿入であるため、これは基本的に2.2 gbテーブル全体を再構築しています。また、スレーブが実行されているため、遅れることを望みません。

時間をかけてゆっくりと入力するスクリプトを書くよりも良い方法はありますか?


2
すでに実行したALTER TABLE .. ADD COLUMN ...ことがありますか、それとも回答する必要がありますか?
ypercubeᵀᴹ

計画段階で、まだテーブルの変更を実行していない。これを行う前に、列を追加し、それを設定してから、制約またはインデックスを追加しました。ただし、このテーブルは非常に大きく、ロード、ロック、レプリケーションなどが心配です。
Collin Peters

回答:


45

要件の詳細に大きく依存します。

場合あなたが持っている十分な空き容量(の少なくとも110% pg_size_pretty((pg_total_relation_size(tbl))、ディスク上の)と余裕があるいくつかの時間のための共有ロック非常に短い時間のために排他ロックをし、作成新しいテーブルを含むuuid使用して列をCREATE TABLE AS。どうして?

以下のコードは、追加uuid-ossモジュールの関数を使用しています

  • SHAREモードの同時変更に対してテーブルをロックします(同時読み取りを許可します)。テーブルへの書き込み試行は待機し、最終的に失敗します。下記参照。

  • テーブル全体をコピーし、その場で新しい列にデータを追加します-行を並べ替える可能性があります。
    場合はリオーダー行しようとしている、設定してくださいwork_memあなたが(ない世界的に、ちょうどあなたのセッションのために)余裕ができるように高いとして。

  • 次に、制約、外部キー、インデックス、トリガーなどを新しいテーブルに追加します。テーブルの大部分を更新する場合、行を繰り返し追加するよりもゼロからインデックスを作成する方がはるかに高速です。

  • 新しいテーブルの準備ができたら、古いテーブルを削除し、新しいテーブルの名前を変更して、ドロップインの代替品にします。この最後のステップのみが、残りのトランザクションのために古いテーブルの排他ロックを取得します-これは非常に短いはずです。
    また、テーブルの種類(ビュー、署名でテーブルの種類を使用する関数など)に応じてオブジェクトを削除し、後でそれらを再作成する必要があります。

  • 不完全な状態を避けるために、すべてを1つのトランザクションで行います。

BEGIN;
LOCK TABLE tbl IN SHARE MODE;

SET LOCAL work_mem = '???? MB';  -- just for this transaction

CREATE TABLE tbl_new AS 
SELECT uuid_generate_v1() AS tbl_uuid, <list of all columns in order>
FROM   tbl
ORDER  BY ??;  -- optionally order rows favorably while being at it.

ALTER TABLE tbl_new
   ALTER COLUMN tbl_uuid SET NOT NULL
 , ALTER COLUMN tbl_uuid SET DEFAULT uuid_generate_v1()
 , ADD CONSTRAINT tbl_uuid_uni UNIQUE(tbl_uuid);

-- more constraints, indices, triggers?

DROP TABLE tbl;
ALTER TABLE tbl_new RENAME tbl;

-- recreate views etc. if any
COMMIT;

これは最速でなければなりません。他の方法でインプレース更新する場合は、テーブル全体を同様に書き換える必要がありますが、より高価な方法です。ディスク上に十分な空き領域がない場合、またはテーブル全体をロックしたり、同時書き込みの試行に対してエラーを生成する余裕がない場合にのみ、このルートを使用します。

同時書き込みはどうなりますか?

トランザクションがロックを取得した後、同じテーブルでINSERT/ UPDATE/ DELETEにしようとする他のトランザクション(他のセッション)SHAREは、ロックが解除されるか、タイムアウトが発生するまでのいずれか早い方を待ちます。彼らはなり失敗し、彼らはそれらの下から削除されたために書き込みしようとしていたテーブルから、いずれかの方法を。

新しいテーブルには新しいテーブルOIDがありますが、同時トランザクションはすでにテーブル名を前のテーブルの OIDに解決しています。ロックが最終的に解除されると、テーブルに書き込む前にテーブルをロックしようとして、テーブルがなくなったことを確認します。Postgresは答えます:

ERROR: could not open relation with OID 123456

123456古いテーブルのOIDはどこにありますか。それを回避するには、その例外をキャッチし、アプリコードでクエリを再試行する必要があります。

それを実現する余裕がない場合は、元のテーブルを保持する必要があります。

既存のテーブルを保持する2つの選択肢

  1. NOT NULL制約を追加する前に、インプレースで更新します(小さなセグメントで一度に更新を実行することもあります)。NULL値を使用してNOT NULL制約なしで新しい列を追加するのは安価です。
    Postgres 9.2以降では、次を使用してCHECK制約をNOT VALID作成することもできます。

    後続の挿入または更新に対して制約は引き続き適用されます

    これは、更新行にあなたを可能にPEUàPEUで- 複数の別々のトランザクション。これにより、行ロックが長く維持されるのを防ぎ、デッド行を再利用できます。(VACUUMautovacuumを開始するのに十分な時間がない場合は、手動で実行する必要があります。)最後に、NOT NULL制約を追加して、制約を削除しNOT VALID CHECKます。

    ALTER TABLE tbl ADD CONSTRAINT tbl_no_null CHECK (tbl_uuid IS NOT NULL) NOT VALID;
    
    -- update rows in multiple batches in separate transactions
    -- possibly run VACUUM between transactions
    
    ALTER TABLE tbl ALTER COLUMN tbl_uuid SET NOT NULL;
    ALTER TABLE tbl ALTER DROP CONSTRAINT tbl_no_null;

    NOT VALIDより詳細に議論する関連回答:

  2. 一時テーブルに新しい状態を準備しTRUNCATE、元のテーブル一時テーブルから補充します。すべて1つのトランザクションで。同時書き込みが失われないように、新しいテーブルを準備する前にSHAREロック を取得する必要があります。

    SOに関するこれらの関連する回答の詳細:


素晴らしい答え!まさに私が探していた情報。2つの質問1.このようなアクションにかかる時間をテストする簡単な方法についてのアイデアはありますか?2.たとえば5分かかる場合、その5分間にそのテーブルの行を更新しようとするアクションはどうなりますか?
コリンピーターズ

@CollinPeters:1.大部分の時間は、大きなテーブルをコピーすることに費やされます-場合によっては、インデックスと制約を再作成します。ドロップと名前の変更は安価です。テストするにはせずに準備されたSQLスクリプトを実行することができますLOCKまでとは除きますDROP。私はワイルドで無駄な推測しか口にすることができませんでした。2.については、私の回答の補遺を考慮してください。
アーウィンブランドステッター

@ErwinBrandstetterビューの再作成を続けます。したがって、テーブルの名前を変更した後でも古いテーブル(oid)を使用するビューが多数ある場合は。ビュー全体の更新/作成を再実行するのではなく、ディープリプレースを実行する方法はありますか?
CodeFarmer

@CodeFarmer:テーブルの名前を変更しただけの場合、ビューは名前を変更したテーブルで動作し続けます。ビューで代わりに新しいテーブルを使用するには、新しいテーブルに基づいてビューを再作成する必要があります。(古いテーブルを削除することもできます。)(実用的な)方法はありません。
アーウィンブランドステッター

14

「ベスト」の答えはありませんが、合理的に高速に処理できる「最低の悪い」答えがあります。

私のテーブルには2MM行があり、デフォルトで最初に設定されたセカンダリタイムスタンプ列を追加しようとすると、更新パフォーマンスが一気に低下しました。

ALTER TABLE mytable ADD new_timestamp TIMESTAMP ;
UPDATE mytable SET new_timestamp = old_timestamp ;
ALTER TABLE mytable ALTER new_timestamp SET NOT NULL ;

それが40分間ハングした後、これをどれくらいの時間がかかるかを知るために小さなバッチでこれを試しました-予測は約8時間でした。

受け入れられた答えは間違いなく優れていますが、このテーブルは私のデータベースで頻繁に使用されています。FKEYするテーブルは数十個あります。非常に多くのテーブルで外部キーを切り替えることを避けたかった。そして、ビューがあります。

ドキュメント、ケーススタディ、StackOverflowを少し検索すると、「A-Ha!」が見つかりました。瞬間。ドレインはコアのUPDATEではなく、すべてのINDEX操作にありました。私のテーブルには12個のインデックスがありました-ユニーク制約のためのいくつか、クエリプランナの高速化のためのいくつか、全文検索のためのいくつか。

更新されたすべての行は、DELETE / INSERTだけでなく、各インデックスを変更して制約をチェックするオーバーヘッドも処理していました。

私の解決策は、すべてのインデックスと制約を削除し、テーブルを更新してから、すべてのインデックス/制約を再び追加することでした。

次のことを行うSQLトランザクションを記述するのに約3分かかりました。

  • ベギン;
  • ドロップされたインデックス/コンテナ
  • テーブルを更新する
  • インデックス/制約の再追加
  • コミット;

スクリプトの実行には7分かかりました。

受け入れられた答えは間違いなくより適切で適切です...そして、ダウンタイムの必要性を事実上排除します。しかし、私の場合、そのソリューションを使用するには「開発者」の作業が大幅に必要であり、30分間のスケジュールされたダウンタイムを達成できました。ソリューションは10で対処しました。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.