NULL値に関するPostgreSQL UPSERTの問題


13

Postgres 9.5の新しいUPSERT機能の使用に問題があります

別のテーブルからデータを集計するために使用されるテーブルがあります。複合キーは20列で構成され、そのうち10列はNULL可能です。以下に、特にNULL値を使用して、私が抱えている問題のより小さなバージョンを作成しました。

CREATE TABLE public.test_upsert (
upsert_id serial,
name character varying(32) NOT NULL,
status integer NOT NULL,
test_field text,
identifier character varying(255),
count integer,
CONSTRAINT upsert_id_pkey PRIMARY KEY (upsert_id),
CONSTRAINT test_upsert_name_status_test_field_key UNIQUE (name, status, test_field)
);

このクエリの実行は、必要に応じて機能します(最初の挿入、その後の挿入は単純にカウントを増やします)。

INSERT INTO test_upsert as tu(name,status,test_field,identifier, count) 
VALUES ('shaun',1,'test value','ident', 1)
ON CONFLICT (name,status,test_field) DO UPDATE set count = tu.count + 1 
where tu.name = 'shaun' AND tu.status = 1 AND tu.test_field = 'test value';

ただし、このクエリを実行すると、最初の行のカウントを増やすのではなく、毎回1行が挿入されます。

INSERT INTO test_upsert as tu(name,status,test_field,identifier, count) 
VALUES ('shaun',1,null,'ident', 1)
ON CONFLICT (name,status,test_field) DO UPDATE set count = tu.count + 1  
where tu.name = 'shaun' AND tu.status = 1 AND tu.test_field = null;

これが私の問題です。カウント値を単純にインクリメントする必要があり、NULL値を持つ複数の同一行を作成する必要はありません。

部分的な一意のインデックスを追加しようとしています:

CREATE UNIQUE INDEX test_upsert_upsert_id_idx
ON public.test_upsert
USING btree
(name COLLATE pg_catalog."default", status, test_field, identifier);

ただし、これにより同じ結果が得られます。複数のnull行が挿入されるか、挿入しようとしたときにこのエラーメッセージが表示されます。

エラー:ON CONFLICT仕様に一致する一意性制約または除外制約はありません

私はすでにのような部分インデックスに追加の詳細を追加しようとしましたWHERE test_field is not null OR identifier is not null。ただし、挿入すると制約エラーメッセージが表示されます。

回答:


14

ON CONFLICT DO UPDATE動作を明確にする

ここでマニュアルを検討してください

挿入が提案された個々の行ごとに、挿入が続行されるか、またはで指定されたアービター制約またはインデックスに conflict_target違反した場合、代替conflict_actionが採用されます。

大胆な強調鉱山。そのWHEREため、UPDATEconflict_action)に対する句の一意のインデックスに含まれる列に対して述語を繰り返す必要はありません。

INSERT INTO test_upsert AS tu
       (name   , status, test_field  , identifier, count) 
VALUES ('shaun', 1     , 'test value', 'ident'   , 1)
ON CONFLICT (name, status, test_field) DO UPDATE
SET count = tu.count + 1;
WHERE tu.name = 'shaun' AND tu.status = 1 AND tu.test_field = 'test value'

固有の違反により、追加した内容が既に確立されています WHERE条項が冗長に実施する。

部分インデックスの明確化

WHERE句を追加して、自分で言ったように実際の部分インデックスにします(ただし、逆論理を使用します)。

CREATE UNIQUE INDEX test_upsert_partial_idx
ON public.test_upsert (name, status)
WHERE test_field IS NULL;  -- not: "is not null"

UPSERTでこの部分インデックスを使用するには、@ ypercubeが示すようなマッチングが必要です。conflict_target

ON CONFLICT (name, status) WHERE test_field IS NULL

これで、上記の部分インデックスが推測されます。ただしマニュアルにも次のように記載さています

[...] ON CONFLICT他のすべての基準を満たすインデックスが利用可能な場合、非部分的なユニークインデックス(述語のないユニークインデックス)が推測されます(したがって、によって使用されます)。

追加の(または唯一の)インデックスがあれば、(name, status)それも(また)使用されます。上のインデックス(name, status, test_field)を明示的だろうではありませんに推測されん。これはあなたの問題を説明するものではありませんが、テスト中に混乱を招く可能性があります。

解決

AIUI、上記のどれもあなたの問題をまだ解決しません。部分インデックスを使用すると、一致するNULL値を持つ特殊なケースのみがキャッチされます。一致する一意のインデックス/制約が他にない場合は、他の重複行が挿入されるか、存在する場合は例外が発生します。私はそれがあなたが望むものではないと思います。あなたが書く:

複合キーは20列で構成され、そのうち10列はNULL可能です。

重複とは正確に何を考えますか?(SQL標準による)Postgresは、2つのNULL値が等しいとは見なしません。マニュアル:

一般に、制約に含まれるすべての列の値が等しいテーブルに複数の行がある場合、一意制約に違反します。ただし、この比較では、2つのNULL値が等しいと見なされることはありません。つまり、一意の制約がある場合でも、制約された列の少なくとも1つにnull値を含む重複行を格納することができます。この動作はSQL標準に準拠していますが、他のSQLデータベースはこの規則に従わない可能性があると聞いています。そのため、移植性のあるアプリケーションを開発する場合は注意してください。

関連:

NULL10個すべてのNULL入力可能列の値が等しいと見なされると仮定します。以下に示すように、追加の部分インデックスを使用して単一のNULL可能列をカバーすることはエレガントで実用的です。

しかし、これは、nullを許可する列が増えるとすぐに手に負えなくなります。NULL許容列の個別の組み合わせごとに部分インデックスが必要になります。ちょうど2のための3つの部分インデックスだもののために(a)(b)(a,b)。数は指数関数的に増加しています2^n - 1ます。10個のNULL入力可能列の場合、NULL値のすべての可能な組み合わせをカバーするには、1023個の部分インデックスが既に必要です。立ち入り禁止。

簡単な解決策:NULL値を置き換えてNOT NULL、関連する列を定義すると、すべてがシンプルで問題なく動作しますUNIQUE制約でます。

それがオプションではない場合COALESCE、インデックスのNULLを置き換える式インデックスをお勧めします。

CREATE UNIQUE INDEX test_upsert_solution_idx
    ON test_upsert (name, status, COALESCE(test_field, ''));

空の文字列は('')文字の種類の明白な候補であるが、あなたは使用することができます任意の表示されないかに応じてNULLに折り畳むことができるいずれか決して正当な値あなたの「ユニーク」の定義を。

次に、次のステートメントを使用します。

INSERT INTO test_upsert as tu(name,status,test_field,identifier, count) 
VALUES ('shaun', 1, null        , 'ident', 11)  -- works with
     , ('bob'  , 2, 'test value', 'ident', 22)  -- and without NULL
ON     CONFLICT (name, status, COALESCE(test_field, '')) DO UPDATE  -- match expr. index
SET    count = COALESCE(tu.count + EXCLUDED.count, EXCLUDED.count, tu.count);

@ypercubeのように私はあなたが実際に追加したいと仮定しcount、既存のカウントに。列はNULLになる可能性があるため、NULLを追加すると列がNULLに設定されます。を定義するとcount NOT NULL、単純化できます。


別のアイデアは、ステートメントからconflict_targetを削除して、すべての一意の違反をカバーすることです。次に、「一意」であると想定されるもののより洗練された定義のために、さまざまな一意のインデックスを定義できます。しかし、それはと一緒に飛びませんON CONFLICT DO UPDATE。もう一度マニュアル:

の場合ON CONFLICT DO NOTHING、conflict_targetを指定することはオプションです。省略すると、使用可能なすべての制約(および一意のインデックス)との競合が処理されます。の場合ON CONFLICT DO UPDATE、conflict_targetを指定する必要あります。


1
いいね 質問を初めて読んだときに20〜10列をスキップし、後で完了する時間はありませんでした。count = CASE WHEN EXCLUDED.count IS NULL THEN tu.count ELSE COALESCE(tu.count, 0) + COALESCE(EXCLUDED.count, 0) END単純化することができるcount = COALESCE(tu.count+EXCLUDED.count, EXCLUDED.count, tu.count)
ypercubeᵀᴹ

もう一度見てみると、私の「単純化された」バージョンはそれほど自己文書化されていません。
ypercubeᵀᴹ

@ypercubeᵀᴹ:提案された更新を適用しました。簡単です、ありがとう。
アーウィンブランドステッター16年

@ErwinBrandstetterあなたは最高です
シーマスアブシー

7

問題は、部分的なインデックスON CONFLICTがなく、構文がインデックスではなくtest_upsert_upsert_id_idx他の一意の制約と一致しないことだと思います。

インデックスを部分(としてWHERE test_field IS NULL)として定義する場合:

CREATE UNIQUE INDEX test_upsert_upsert_id_idx
ON public.test_upsert
USING btree
(name COLLATE pg_catalog."default", status)
WHERE test_field IS NULL ;

そして、これらの行はすでにテーブルにあります:

INSERT INTO test_upsert as tu
    (name, status, test_field, identifier, count) 
VALUES 
    ('shaun', 1, null, 'ident', 1),
    ('maria', 1, null, 'ident', 1) ;

クエリは成功します:

INSERT INTO test_upsert as tu
    (name, status, test_field, identifier, count) 
VALUES 
    ('peter', 1,   17, 'ident', 1),
    ('shaun', 1, null, 'ident', 3),
    ('maria', 1, null, 'ident', 7)
ON CONFLICT 
    (name, status) WHERE test_field IS NULL   -- the conflicting condition
DO UPDATE SET
    count = tu.count + EXCLUDED.count 
WHERE                                         -- when to update
    tu.name = 'shaun' AND tu.status = 1 ;     -- if you don't want all of the
                                              -- updates to happen

次の結果があります。

('peter', 1,   17, 'ident', 1)  -- no conflict: row inserted

('shaun', 1, null, 'ident', 3)  -- conflict: no insert
                           -- matches where: row updated with count = 1+3 = 4

('maria', 1, null, 'ident', 1)  -- conflict: no insert
                     -- doesn't match where: no update

これにより、部分インデックスの使用方法が明確になります。しかし、(私は思う)それはまだ問題を解決していません。
アーウィンブランドステッター16年

更新は行われないため、「maria」のカウントは1のままにしてはいけませんか?
mpprdev

@mpprdevはい、あなたは正しいです。
ypercubeᵀᴹ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.