web-dev-qa-db-ja.com

INSERT ... ON CONFLICTからのRETURNINGに除外された行を含める方法

私はこのテーブルを持っています(Djangoが生成):

CREATE TABLE feeds_person (
  id serial PRIMARY KEY,
  created timestamp with time zone NOT NULL,
  modified timestamp with time zone NOT NULL,
  name character varying(4000) NOT NULL,
  url character varying(1000) NOT NULL,
  email character varying(254) NOT NULL,
  CONSTRAINT feeds_person_name_ad8c7469_uniq UNIQUE (name, url, email)
);

ON CONFLICT句とINSERTを使用して大量のデータを一括挿入しようとしています。

しわは、行が既に存在するかどうかに関係なく、行のallに対してidを取り戻す必要があることです。

他の場合では、私は次のようなことをします:

INSERT INTO feeds_person (created, modified, name, url, email)
VALUES blah blah blah
ON CONFLICT (name, url, email) DO UPDATE SET url = feeds_person.url
RETURNING id

UPDATEを実行すると、ステートメントはその行のidを返します。ただし、このテーブルでは機能しません。私はthink複数の一意のフィールドを一緒に持っているので機能しませんが、他の例ではこの方法を使用しましたが、一意のフィールドが1つしかありませんでした。

Djangoのカーソルを介してSQLを実行しようとすると、このエラーが発生します。

Django.db.utils.ProgrammingError: ON CONFLICT DO UPDATE command cannot affect row a second time
HINT:  Ensure that no rows proposed for insertion within the same command have duplicate constrained values.

このテーブルで一括挿入を行い、挿入されたIDと既存のIDを取得するにはどうすればよいですか?

14
Dustin Wyatt

あなたが得るエラー:

ON CONFLICT DO UPDATEコマンドは行に2回影響を与えることはできません

単一のコマンドで同じ行を複数回アップサートしようとしていることを示します。つまり、VALUESリストの_(name, url, email)_に重複があります。複製を折りたたみ(オプションの場合)、機能するはずです。ただし、各デュープセットから選択する行を決定する必要があります。

_INSERT INTO feeds_person (created, modified, name, url, email)
SELECT DISTINCT ON (name, url, email) *
FROM  (
   VALUES
   ('blah', 'blah', 'blah', 'blah', 'blah')
   -- ... more
   ) v(created, modified, name, url, email)  -- match column list
ON     CONFLICT (name, url, email) DO UPDATE
SET    url = feeds_person.url
RETURNING id;
_

ここでは、独立したVALUES式を使用しているため、デフォルト以外の型の明示的な型キャストを追加する必要があります。お気に入り:

_VALUES
    (timestamptz '2016-03-12 02:47:56+01'
   , timestamptz '2016-03-12 02:47:56+01'
   , 'n3', 'u3', 'e3')
   ...
_

timestamptz列には明示的な型キャストが必要ですが、文字列型はデフォルトのtextで動作できます。 (まだvarchar(n)にすぐにキャストできます。)

デュープの各セットから選択する行を決定する方法があります。

あなたが正しい、(現在)RETURNING句。 Postgres Wiki を引用します:

RETURNINGUPDATEからの「_EXCLUDED.*_」エイリアスを表示しないことに注意してください(一般的な「_TARGET.*_」エイリアスのみが表示されます)。そうすることは、単純で一般的なケース [30] のメリットをほとんどまたはまったくもたらさずに、迷惑なあいまいさを生み出すと考えられています。将来のある時点で、RETURNING- projectedタプルが挿入および更新されたかどうかを公開する方法を追求する可能性がありますが、これはおそらく機能の最初のコミット済みイテレーションにする必要はありません [31]

ただし、更新されるはずのない行は更新しないでください。空の更新は通常の更新とほぼ同じくらい高価であり、意図しない副作用が生じる可能性があります。 UPSERTを厳密に必要とするわけではありません。ケースは「SELECTまたはINSERT」のように見えます。関連:

行のセットを挿入する1つのよりクリーンな方法は、データ変更CTEを使用することです。

_WITH val AS (
   SELECT DISTINCT ON (name, url, email) *
   FROM  (
      VALUES 
      (timestamptz '2016-1-1 0:0+1', timestamptz '2016-1-1 0:0+1', 'n', 'u', 'e')
    , ('2016-03-12 02:47:56+01', '2016-03-12 02:47:56+01', 'n1', 'u3', 'e3')
      -- more (type cast only needed in 1st row)
      ) v(created, modified, name, url, email)
   )
, ins AS (
   INSERT INTO feeds_person (created, modified, name, url, email)
   SELECT created, modified, name, url, email FROM val
   ON     CONFLICT (name, url, email) DO NOTHING
   RETURNING id, name, url, email
   )
SELECT 'inserted' AS how, id FROM ins  -- inserted
UNION  ALL
SELECT 'selected' AS how, f.id         -- not inserted
FROM   val v
JOIN   feeds_person f USING (name, url, email);
_

追加された複雑さは、INSERTがルールでSELECTが例外である大きなテーブルに支払うべきです。

元々、結果の重複を防ぐために、最後のSELECTに_NOT EXISTS_述語を追加していました。しかし、それは冗長でした。 単一のクエリのすべてのCTEは、テーブルの同じスナップショットを参照します。ON CONFLICT (name, url, email) DO NOTHINGは、同じ列の_INNER JOIN_の後に返されるセットと相互に排他的です。

残念ながら、これは競合状態の小さなウィンドウも開きます。もし...

  • 並行トランザクションが競合する行を挿入する
  • まだコミットしていません
  • しかし最終的にはコミットする

...一部の行が失われる可能性があります。

同じトランザクション内で、_INSERT .. ON CONFLICT DO NOTHING_に続けて、すべての行に対して個別のSELECTクエリを実行すると、これを克服できます。同時トランザクションがテーブルへの書き込みをコミットできる場合、競合状態の別の小さなウィンドウを順に開きますINSERTSELECTの間(デフォルトでは _READ COMMITTED_分離レベル )。 _REPEATABLE READ_トランザクション分離 (またはより厳密)で回避できます。または、テーブル全体に対する(おそらく高価であるか、または許容できない)書き込みロックを使用します。あなたはあなたが必要とするあらゆる振る舞いを得ることができますが、支払う代償があるかもしれません。

関連:

27