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


54

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ますか?または、2つの手順で行う必要があります。最初fooに必要な型を検索し、次に新しい行を挿入しbarますか?

以下は、私が望んでいたことを示す擬似コードの例です。

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

回答:


67

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

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 ;

数回読んでみましたが、あなたが提供した2番目の解決策を理解しました。私はそれが好きです。これを使用して、システムが最初に起動したときに、いくつかの既知の値でデータベースをブートストラップします。
ステファン

37

プレーン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の CTE と同じです。共通テーブル式は追加機能を提供し、大きなクエリで読みやすくなりますが、最適化の障壁にもなります。したがって、上記のいずれも必要ない場合、通常、サブクエリは少し高速です。

  • id列名は広く普及しているアンチパターンであるため。説明的なものである必要がfoo_idありbar_idます。多数のテーブルを結合すると、すべての名前が複数の列になりidます...

  • プレーンtextまたはのvarchar代わりに検討してくださいvarchar(n)。長さ制限を本当に課す必要がある場合は、CHECK制約を追加します。

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

欠落しているFK行を同時に挿入する

存在しないエントリをfooオンザフライで1 つの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つの新しいダミー行に注意してください。どちらも紫色foo、まだ存在しません。最初のステートメントでの必要性を示す2行。DISTINCTINSERT

段階的な説明

  1. 最初のCTE selは、入力データの複数の行を提供します。サブクエリvalとのVALUES表現は、ソースとして、テーブルまたはサブクエリに置き換えることができます。すぐLEFT JOINに既存の行fooに追加します。他のすべての行はこのようになります。foo_idtypefoo_id IS NULL

  2. 2番目のCTEは、別個の新しいタイプ()をにins挿入し、新しく生成された-を結合して、行を挿入するために結合します。foo_id IS NULLfoofoo_idtype

  3. 最後のアウターINSERTは、すべての行にfoo.idを挿入できるようになりました。既存のタイプ、またはステップ2で挿入されたタイプです。

厳密に言えば、両方の挿入は「並行して」行われますが、これは単一のステートメントなので、デフォルトのFOREIGN KEY制約は文句を言いません。デフォルトでは、ステートメントの最後に参照整合性が適用されます。

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

これらのクエリを複数同時に実行すると、小さな競合状態が発生します。関連する質問についてこちらこちらこちらご覧ください。実際には、重い同時ロードが発生した場合にのみ発生します。別の回答で宣伝されているようなキャッシュソリューションと比較すると、チャンスは非常に小さいです。

繰り返し使用するための機能

繰り返し使用する場合は、レコードの配列をパラメーターとして受け取りunnest(param)VALUES式の代わりに使用する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
   );

同時トランザクションを伴う環境向けの高速で堅実な。

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

  • ... apply SELECTまたはINSERTon footypeFKテーブルに存在しないが、まだ挿入されているもの。ほとんどのタイプが既に存在すると仮定します。競合状態を完全に確認して除外するには、必要な既存の行をロックします(同時トランザクションが干渉しないようにするため)。それがあなたの場合にはあまりにも妄想的である場合、あなたは置き換えることができます:

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

      ON     CONFLICT(type) DO NOTHING
  • ...適用INSERTまたはUPDATE(true "UPSERT")on bardescription既に存在する場合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個のパラメーターに注意してください!比較:

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

関連する:


あなたのINSERT missing FK rows at the same time例では、これをトランザクションに入れることで、SQL Serverの競合状態のリスクを減らすことができますか?
element11

1
@ element11:答えはPostgresに対するものですが、単一の SQLコマンドについて話しているため、いずれにしても単一のトランザクションです。より大きなトランザクション内で実行すると、起こりうる競合状態の時間枠が長くなるだけです。SQL Serverのとおり:データ変更のCTEは、(のみすべてのサポートされていませんSELECT内部WITH節)。出典:MSドキュメント。
アーウィンブランドステッター16

1
INSERT ... RETURNING \gsetinでこれを行うこともできpsqlます。その場合、戻り値をpsqlとして使用します:'variables'が、これは単一行の挿入に対してのみ機能します。
クレイグリンガー

@ErwinBrandstetterこれは素晴らしいですが、私はそれをすべて理解するのにSQLにはあまりにも新しいので、「同時に欠落しているFK行を挿入する」にいくつかのコメントを追加できますか?また、SQLFiddleの作業例に感謝します!
glallen

@glallen:段階的な説明を追加しました。また、関連する回答へのリンクや、詳細な説明が記載されたマニュアルもあります。クエリが何をするのかを理解する必要があります。そうしないと頭がおかしくなります。
アーウィンブランドステッター

4

調べる。基本的に、barに挿入するにはfoo idが必要です。

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

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

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

例:

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

明らかに、何かをアカウントにリンクする場合-技術的には、まずIDを取得する必要があります-しかし、識別子とコードの両方がそこにいったん変更されると、メモリkanのポジティブキャッシュにより、ほとんどのルックアップがデータベースにヒットしなくなります。


10
エラーが発生しやすいキャッシュを回避して、RDBMSに単一のSQLステートメントでルックアップを実行させることができることを知っていますか?
アーウィンブランドステッター

変化しない要素を検索することはエラーを起こしにくいことを知っていますか?また、通常、RDBMSはスケーラブルではなく、ライセンスコストのためにゲームで最も高価な要素です。可能な限り多くの負荷をかけることは、まったく悪いことではありません。また、そもそもサポートしているORMは多くありません。
トムトム

14
変化しない要素?最も高価な要素?ライセンス費用(PostgreSQLの場合)?正気を定義するORM いいえ、私はそのすべてを知りませんでした。
アーウィンブランドステッター
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.