単一の主キーを持つテーブルがあります。挿入を行おうとすると、既存のキーを使用して行を挿入しようとすると競合が発生する可能性があります。挿入ですべての列を更新できるようにしたいですか?これには簡単な構文はありますか?私はそれがすべてのカラムを「upsert」させようとしています。
PostgreSQL 9.5.5を使用しています。
UPDATE
syntaxrequiresターゲット列に明示的に名前を付ける。それを回避する考えられる理由:
"All columns"
は、一致する順序と一致するデータ型で"ターゲットテーブルのすべての列"(または少なくとも"テーブルの先頭列")を意味する必要があります。そうでない場合は、とにかくターゲット列名のリストを提供する必要があります。
テストテーブル:
CREATE TABLE tbl (
id int PRIMARY KEY
, text text
, extra text
);
INSERT INTO tbl AS t
VALUES (1, 'foo')
, (2, 'bar');
DELETE
&INSERT
id
。以外の列名を知ることなく
"ターゲットテーブルのすべての列"に対してのみ機能します。構文は先頭のサブセットでも機能しますが、ターゲットテーブルの余分な列は、DELETE
およびINSERT
を使用してNULLにリセットされます。
UPSERT(INSERT ... ON CONFLICT ...
)は、同時書き込み負荷での同時性/ロックの問題を回避するために必要です。これは、Postgresにまだ存在しない行をロックする一般的な方法がないためです( 値のロック =)。
特別な要件はUPDATE
部分にのみ影響します。 existing行が影響を受ける場合、起こり得る複雑化は適用されません。それらは適切にロックされています。さらに単純化すると、ケースをDELETE
とINSERT
に減らすことができます。
WITH data(id) AS ( -- Only 1st column gets explicit name!
VALUES
(1, 'foo_upd', 'a') -- changed
, (2, 'bar', 'b') -- unchanged
, (3, 'baz', 'c') -- new
)
, del AS (
DELETE FROM tbl AS t
USING data d
WHERE t.id = d.id
-- AND t <> d -- optional, to avoid empty updates
) -- only works for complete rows
INSERT INTO tbl AS t
TABLE data -- short for: SELECT * FROM data
ON CONFLICT (id) DO NOTHING
RETURNING t.id;
Postgres MVCCモデルでは、UPDATE
はDELETE
およびINSERT
とほとんど同じです(ただし、同時実行性、HOT更新、および格納されている大きな列の値を伴ういくつかの例外的なケースを除く)行の)。とにかくすべての行を置き換えたいので、INSERT
の前に競合する行を削除します。削除された行は、トランザクションがコミットされるまでロックされたままになります。 INSERT
は、同時トランザクションがそれらを同時に挿入した場合(DELETE
の後、INSERT
の前)に、以前は存在しなかったキー値の競合する行のみを検出する可能性があります。
この特殊なケースでは、影響を受ける行の追加の列値が失われます。例外は発生しません。ただし、競合するクエリの優先度が同じ場合、それはほとんど問題になりません。他のクエリはsome行に対して勝ちます。また、他のクエリが同様のUPSERTである場合、その代替策は、このトランザクションがコミットするのを待って、すぐに更新することです。 「勝利」はピューリクスの勝利かもしれない。
「空の更新」について:
OK、あなたはそれを求めました:
WITH data(id) AS ( -- Only 1st column gets explicit name!
VALUES -- rest gets default names "column2", etc.
(1, 'foo_upd', NULL) -- changed
, (2, 'bar', NULL) -- unchanged
, (3, 'baz', NULL) -- new
, (4, 'baz', NULL) -- new
)
, ups AS (
INSERT INTO tbl AS t
TABLE data -- short for: SELECT * FROM data
ON CONFLICT (id) DO UPDATE
SET id = t.id
WHERE false -- never executed, but locks the row!
RETURNING t.id
)
, del AS (
DELETE FROM tbl AS t
USING data d
LEFT JOIN ups u USING (id)
WHERE u.id IS NULL -- not inserted !
AND t.id = d.id
-- AND t <> d -- avoid empty updates - only for full rows
RETURNING t.id
)
, ins AS (
INSERT INTO tbl AS t
SELECT *
FROM data
JOIN del USING (id) -- conflict impossible!
RETURNING id
)
SELECT ARRAY(TABLE ups) AS inserted -- with UPSERT
, ARRAY(TABLE ins) AS updated -- with DELETE & INSERT;
どうやって?
data
はデータを提供するだけです。代わりにテーブルである可能性があります。ups
:UPSERT。 id
が競合する行は変更されませんが、lockedも変更されません。del
は、競合する行を削除します。彼らはロックされたままです。ins
は行全体を挿入します。同じトランザクションでのみ許可次のコマンドを使用して、空の更新テスト(前後)をチェックするには
SELECT ctid, * FROM tbl; -- did the ctid change?
これは、既存の値を保持しながら、主要な列のサブセットでも機能します。
トリックは、Postgresにシステムカタログの列名を含むクエリ文字列を動的に作成させ、それを実行させることです。
コードの関連回答を参照してください:
コメントする評判が足りないので、id列が最初の列ではない場合、Erwin Brandstetterの回答は失敗するようです。
以下は、彼の他の answers からのスニペットを使用して、私の場合の「リターン/アップ」機能を再現します。
DO
$do$
BEGIN
EXECUTE (
SELECT
'DROP TABLE IF EXISTS res_tbl; CREATE TABLE res_tbl AS
WITH
ins AS (
INSERT INTO dest
TABLE src -- short for: SELECT * FROM data
ON CONFLICT (id) DO UPDATE
SET id = dest.id
WHERE false -- never executed, but locks the row!
RETURNING id
),
repl AS (
UPDATE dest
SET (' || string_agg(quote_ident(column_name), ',') || ')
= (' || string_agg('src.' || quote_ident(column_name), ',') || ')
FROM src
WHERE src.id = dest.id
AND src <> dest
-- ^ avoids empty updates - only for full-row updates where all columns are comparable (e.g. jsonb not json)
RETURNING dest.id
)
SELECT ARRAY(TABLE ins) AS inserted -- with UPSERT
, ARRAY(TABLE repl) AS updated -- with DYNAMIC UPDATE
;'
FROM information_schema.columns
WHERE table_name = 'src' -- table name, case sensitive
AND table_schema = 'public' -- schema name, case sensitive
AND column_name <> 'id' -- all columns except id)
);
END
$do$;