web-dev-qa-db-ja.com

PostgreSQLの主キーを持つ行の重複

次のようなpeopleという名前のテーブルがあるとします。ここで、idは主キーです。

+-----------+---------+---------+
|  id       |  fname  |  lname  |
| (integer) | (text)  | (text)  |
+===========+=========+=========+
|  1        | Daniel  | Edwards |
|  2        | Fred    | Holt    |
|  3        | Henry   | Smith   |
+-----------+---------+---------+

テーブルへのスキーマの変更を考慮するのに十分堅牢な行複製クエリを記述しようとしています。テーブルに列を追加するときはいつでも、前に戻って複製クエリを変更する必要はありません。

私はこれを実行できることを知っています。これにより、レコードID 2が複製され、複製されたレコードに新しいIDが付与されます。

INSERT INTO people (fname, lname) SELECT fname, lname FROM people WHERE id = 2;

ただし、age列を追加する場合は、クエリを変更して、年齢列も考慮する必要があります。

次のことはできません。主キーも複製され、duplicate key value violates unique constraint-そして、私は彼らに同じIDを共有してほしくない:

INSERT INTO people SELECT * FROM people WHERE id = 2

それでは、この課題を解決するための合理的なアプローチは何でしょうか?私はストアドプロシージャに近づかないようにしたいと思いますが、私はそれらに対して100%ではありません。

7
Joshua Burns

hstoreでシンプル

追加モジュールhstoreがインストールされている場合( 以下のリンクの手順 )、驚くほど単純な他の列について何も知らずに個々のフィールドの値を置き換える方法:

基本的な例:_id = 2_で行を複製しますが、_2_を_3_で置き換えます。

_INSERT INTO people
SELECT (p #= hstore('id', '3')).* FROM people p WHERE id = 2;
_

詳細:

仮定(質問で定義されていないため)_people.id_はserial列にシーケンスがアタッチされている場合、シーケンスの次の値が必要になります。シーケンス名はpg_get_serial_sequence()で決定できます。詳細:

または、シーケンス名が変更されない場合は、シーケンス名をハードコーディングすることもできます。
私たちはこのクエリを持っています

_INSERT INTO people
SELECT (p #= hstore('id', nextval(pg_get_serial_sequence('people', 'id'))::text)).*
FROM people p WHERE id = 2;
_

どのでも機能しますが、Postgresクエリプランナーの弱点があります。式は行のすべての列に対して個別に評価され、シーケンス番号とパフォーマンスを無駄にします。これを回避するには、式をサブクエリに移動し、行を1回だけ分解します。

_INSERT INTO people
SELECT (p1).*
FROM  (
   SELECT p #= hstore('id', nextval(pg_get_serial_sequence('people', 'id'))::text) AS p1
   FROM   people p WHERE id = 2
   ) sub;
_

おそらく単一の(または少数の)行を同時に処理するのに最も高速です。

json/jsonb

hstoreがインストールされておらず、追加のモジュールをインストールできない場合、json_populate_record()またはjsonb_populate_record()を使用して同様のトリックを実行できますが、その機能は文書化されておらず、信頼できない場合があります。

一時的な一時テーブル

別の簡単な解決策は、次のような一時的な一時ファイルを使用することです。

_BEGIN;
CREATE TEMP TABLE people_tmp ON COMMIT DROP AS
SELECT * FROM people WHERE id = 2;
UPDATE people_tmp SET id = nextval(pg_get_serial_sequence('people', 'id'));
INSERT INTO people TABLE people_tmp;
COMMIT;
_

トランザクションの最後にテーブルを自動的に削除するために_ON COMMIT DROP_を追加しました。そのため、操作も独自のトランザクションにラップしました。どちらも厳密には必要ありません。

これにより、幅広い追加オプションが提供されます。挿入する前に行を使用して何でも実行できますが、一時テーブルの作成と削除のオーバーヘッドのため、少し遅くなります。

このソリューションは、単一の行または任意の数の行を一度に機能します。各行は、シーケンスから新しいデフォルト値を自動的に取得します。

short(SQL standard)notation _TABLE people_ を使用します。

動的SQL

多くの行の場合、動的SQLが最も高速になります。システムテーブル_pg_attribute_または情報スキーマの列を連結し、DOステートメントで動的に実行するか、繰り返し使用するための関数を記述します。

_CREATE OR REPLACE FUNCTION f_row_copy(_tbl regclass, _id int, OUT row_ct int) AS
$func$
BEGIN
   EXECUTE (
      SELECT format('INSERT INTO %1$s(%2$s) SELECT %2$s FROM %1$s WHERE id = $1',
                    _tbl, string_agg(quote_ident(attname), ', '))
      FROM   pg_attribute
      WHERE  attrelid = _tbl
      AND    NOT attisdropped  -- no dropped (dead) columns
      AND    attnum > 0        -- no system columns
      AND    attname <> 'id'   -- exclude id column
      )
   USING _id;

   GET DIAGNOSTICS row_ct = ROW_COUNT;  -- directly assign OUT parameter
END
$func$  LANGUAGE plpgsql;
_

コール:

_SELECT f_row_copy('people', 9);
_

idという名前の整数列を持つ任意のテーブルで機能します。列名も簡単に動的にすることができます...

_stay away from stored procedures_にしたかったので、おそらく最初の選択ではないかもしれませんが、それでも 「ストアドプロシージャ」ではない とにかく...

関連:

高度なソリューション

serial列は特殊なケースです。より多くまたはすべての列にそれぞれのデフォルト値を入力する場合は、より高度になります。この関連する回答を検討してください:

15

挿入時にtriggerを作成してみてください:

_CREATE TRIGGER name BEFORE INSERT
_

このトリガーでは、IDをNULLにします。トリガーが完了すると挿入が行われ、PostgresがIDを提供します。 IDをDEFAULT NEXTVAL('A_SEQUENCE'::REGCLASS)として定義したと思います。

0
Marco