web-dev-qa-db-ja.com

外部キーを含む行を挿入するにはどうすればよいですか?

PostgreSQL v9.1を使用します。次の表があります。

CREATE TABLE foo
(
    id BIGSERIAL     NOT NULL UNIQUE PRIMARY KEY,
    type VARCHAR(60) NOT NULL UNIQUE
);

CREATE TABLE bar
(
    id BIGSERIAL NOT NULL UNIQUE PRIMARY KEY,
    description VARCHAR(40) NOT NULL UNIQUE,
    foo_id BIGINT NOT NULL REFERENCES foo ON DELETE RESTRICT
);

最初のテーブルfooに次のように入力するとします。

INSERT INTO foo (type) VALUES
    ( 'red' ),
    ( 'green' ),
    ( 'blue' );

barテーブルを参照して簡単にfooに行を挿入する方法はありますか?または、最初に必要なfooタイプを検索し、次にbarに新しい行を挿入するという2つの手順で実行する必要がありますか?

これが私が望んでいたことを示す疑似コードの例です:

INSERT INTO bar (description, foo_id) VALUES
    ( 'testing',     SELECT id from foo WHERE type='blue' ),
    ( 'another row', SELECT id from foo WHERE type='red'  );
61
Stéphane

あなたの構文はほぼ良好です、サブクエリの周りにいくつかの括弧が必要であり、それは動作します:

INSERT INTO bar (description, foo_id) VALUES
    ( 'testing',     (SELECT id from foo WHERE type='blue') ),
    ( 'another row', (SELECT id from foo WHERE type='red' ) );

SQL-Fiddle でテスト

別の方法として、挿入する値が多い場合は構文を短くします。

WITH ins (description, type) AS
( VALUES
    ( 'more testing',   'blue') ,
    ( 'yet another row', 'green' )
)  
INSERT INTO bar
   (description, foo_id) 
SELECT 
    ins.description, foo.id
FROM 
  foo JOIN ins
    ON ins.type = foo.type ;
74
ypercubeᵀᴹ

プレーンINSERT

_INSERT INTO bar (description, foo_id)
SELECT val.description, f.id
FROM  (
   VALUES
      (text 'testing', text 'blue')  -- explicit type declaration; see below
    , ('another row', 'red' )
    , ('new row1'   , 'purple')      -- purple does not exist in foo, yet
    , ('new row2'   , 'purple')
   ) val (description, type)
LEFT   JOIN foo f USING (type);
_
  • _LEFT [OUTER] JOIN_の代わりに_[INNER] JOIN_を使用すると、valの行は削除されません一致はfooにあります。代わりに、_NULLが_foo_id_に入力されます。

  • サブクエリのVALUES式は @ ypercube's CTEと同じです。 共通テーブル式 は追加の機能を提供し、大きなクエリで読みやすくなりますが、最適化の障壁としても機能します。したがって、上記のいずれも必要としない場合、サブクエリは通常少し高速です。

  • id as column name is a広く普及しているアンチパターン。 _foo_id_および_bar_id_、または説明的なものにする必要があります。一連のテーブルを結合すると、すべてidという名前の複数の列ができます...

  • varchar(n)ではなく、単純なtextまたはvarcharを検討してください。長さ制限を課す必要がある場合は、CHECK制約を追加します。

  • 明示的な型キャストを追加する必要がある場合があります。 VALUES式は直接テーブルにアタッチされないため(_INSERT ... VALUES ..._のように)、型を導出できず、明示的な型宣言なしにデフォルトのデータ型が使用されますが、すべての場合で機能するとは限りません。最初の行でそれを行うだけで十分です。残りは整列します。

不足しているFK行を同時に挿入する

存在しないエントリをその場でfooに作成する場合は、単一のSQLステートメントで、CTEが役立ちます。

_WITH sel AS (
   SELECT val.description, val.type, f.id AS foo_id
   FROM  (
      VALUES
         (text 'testing', text 'blue')
       , ('another row', 'red'   )
       , ('new row1'   , 'purple')
       , ('new row2'   , 'purple')
      ) val (description, type)
   LEFT   JOIN foo f USING (type)
   )
, ins AS ( 
   INSERT INTO foo (type)
   SELECT DISTINCT type FROM sel WHERE foo_id IS NULL
   RETURNING id AS foo_id, type
   )
INSERT INTO bar (description, foo_id)
SELECT sel.description, COALESCE(sel.foo_id, ins.foo_id)
FROM   sel
LEFT   JOIN ins USING (type);
_

挿入する2つの新しいダミー行に注意してください。どちらもpurpleですが、fooにはまだ存在しません。 最初のDISTINCTステートメントでのINSERTの必要性を示す2つの行。

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

  1. 最初のCTE selは、入力データの複数の行を提供します。 val式を持つサブクエリVALUESは、ソースとしてテーブルまたはサブクエリに置き換えることができます。すぐに_LEFT JOIN_をfooに追加して、既存のtype行の_foo_id_を追加します。他のすべての行は、この方法で_foo_id IS NULL_を取得します。

  2. 2番目のCTE insは、distinct新しいタイプ(_foo_id IS NULL_)をfooに挿入し、新しく生成された_foo_id_を返します-typeと一緒に結合して行を挿入します。

  3. 最後の外側のINSERTは、すべての行にfoo.idを挿入できるようになりました。既存の型か、手順2で挿入された型のどちらかです。

厳密に言えば、両方の挿入は「並行して」行われますが、これはsingleステートメントであるため、デフォルトの_FOREIGN KEY_制約は問題になりません。参照整合性は、デフォルトでステートメントの最後に適用されます。

SQL Fiddlefor Postgres 9.3。 (9.1でも同じように機能します。)

これらのクエリを同時に複数実行すると、小さな競合状態が発生します。関連する質問 ここ および ここ および ここ で詳細をお読みください。実際には、同時負荷が重い場合にのみ発生します。別の回答で宣伝されているようなキャッシングソリューションと比較すると、チャンスはsuper-tinyです。

繰り返し使用できる機能

繰り返し使用する場合は、パラメーターとしてレコードの配列を取り、VALUES式の代わりにunnest(param)を使用するSQL関数を作成します。

または、レコードの配列の構文が複雑すぎる場合は、パラメーター__param_としてコンマ区切りの文字列を使用します。たとえば、フォームの例:

_'description1,type1;description2,type2;description3,type3'
_

次に、これを使用して上記のステートメントのVALUES式を置き換えます。

_SELECT split_part(x, ',', 1) AS description
       split_part(x, ',', 2) AS type
FROM unnest(string_to_array(_param, ';')) x;
_


Postgres 9.5のUPSERTで機能する

パラメータを渡すためのカスタム行タイプを作成します。それなしでも可能ですが、もっと簡単です:

_CREATE TYPE foobar AS (description text, type text);
_

関数:

_CREATE OR REPLACE FUNCTION f_insert_foobar(VARIADIC _val foobar[])
  RETURNS void AS
$func$
   WITH val AS (SELECT * FROM unnest(_val))    -- well-known row type
   ,    ins AS ( 
      INSERT INTO foo AS f (type)
      SELECT DISTINCT v.type                   -- DISTINCT!
      FROM   val v
      ON     CONFLICT(type) DO UPDATE          -- type already exists
      SET    type = excluded.type WHERE FALSE  -- never executed, but lock rows
      RETURNING f.type, f.id
      )
   INSERT INTO bar AS b (description, foo_id)
   SELECT v.description, COALESCE(f.id, i.id)  -- assuming most types pre-exist
   FROM        val v
   LEFT   JOIN foo f USING (type)              -- already existed
   LEFT   JOIN ins i USING (type)              -- newly inserted
   ON     CONFLICT (description) DO UPDATE     -- description already exists
   SET    foo_id = excluded.foo_id             -- real UPSERT this time
   WHERE  b.foo_id IS DISTINCT FROM excluded.foo_id  -- only if actually changed
$func$  LANGUAGE sql;
_

コール:

_SELECT f_insert_foobar(
     '(testing,blue)'
   , '(another row,red)'
   , '(new row1,purple)'
   , '(new row2,purple)'
   , '("with,comma",green)'  -- added to demonstrate row syntax
   );
_

同時トランザクションが存在する環境では高速かつ強固です。

上記のクエリに加えて、これは...

  • ... SELECTまたはINSERTfooに適用:FKテーブルにまだ存在しないtypeが挿入されます。ほとんどのタイプが事前に存在すると仮定します。確実に競合状態を排除するために、必要な既存の行はロックされます(同時トランザクションが干渉できないようにするため)。それがあなたのケースにとってあまりにも偏執的であるなら、あなたは置き換えることができます:

    _  ON     CONFLICT(type) DO UPDATE          -- type already exists
      SET    type = excluded.type WHERE FALSE  -- never executed, but lock rows
    _

    _  ON     CONFLICT(type) DO NOTHING
    _
  • ... INSERTUPDATEまたはbar(true "UPSERT")を適用します:descriptionがすでに存在する場合、typeが更新されます:

    _  ON     CONFLICT (description) DO UPDATE     -- description already exists
      SET    foo_id = excluded.foo_id             -- real UPSERT this time
      WHERE  b.foo_id IS DISTINCT FROM excluded.foo_id  -- only if actually changed
    _

    ただし、typeが実際に変更された場合のみ:

  • ... VARIADICパラメータを使用して、既知の行タイプである値を渡します。デフォルトの最大100パラメータに注意してください!比較:

    複数の行を渡す方法は他にもたくさんあります...

関連:

45

調べる。それらをbarに挿入するには、基本的にfoo IDが必要です。

ちなみに、postgres固有ではありません。 (そしてあなたはそのようにタグ付けしていませんでした)-これは一般的にSQLが機能する方法です。ここにショートカットはありません。

ただし、アプリケーションについては、メモリ内にfooアイテムのキャッシュがある場合があります。多くの場合、テーブルには最大3つの一意のフィールドがあります。

  • テーブルレベルの主キーであるID(整数または何か)。
  • GUIDである識別子)。アプリケーションレベルで安定したIDとして使用されます(URLなどで顧客に公開される場合があります)。
  • コード-存在する可能性があり、存在する場合は一意である必要がある文字列(SQLサーバー:null以外のフィルター処理された一意のインデックス)。これは、顧客が設定した識別子です。

例:

  • アカウント(取引アプリケーション内)-> Idは、外部キーに使用されるintです。 ->識別子はGuidであり、Webポータルなどで使用されます-常に受け入れられます。 ->コードは手動で設定されます。ルール:一度設定すると、変更されません。

明らかに、何かをアカウントにリンクしたい場合-最初に、技術的にはIDを取得する必要があります-が、IDとコードの両方が存在すると変更されないため、メモリカンのポジティブキャッシュは、ほとんどのルックアップがデータベースにアクセスするのを防ぎます。

5
TomTom