web-dev-qa-db-ja.com

NULL値に関するPostgreSQL UPSERTの問題

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
Shaun McCready

_ON CONFLICT DO UPDATE_の動作を明確にする

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

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

大胆な強調鉱山。したがって、WHERE句の一意のインデックスに含まれる列の述語をUPDATE(_conflict_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のuseこの部分インデックスに一致する_conflict_target_@ypercubeのデモのように

_ON CONFLICT (name, status) WHERE test_field IS NULL
_

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

[...]部分的でない一意のインデックス(述語のない一意のインデックス)は、他のすべての基準を満たすインデックスが利用可能な場合に推論されます(したがって、_ON CONFLICT_によって使用されます)。

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

解決

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

複合キーは20列で構成され、そのうち10列はnullにすることができます。

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

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

関連:

I仮定 10個のnull許容列すべてのNULL値が等しいと見なされるようにします。次の例のように、単一のnull許容列を追加の部分インデックスでカバーするのはエレガントで実用的です。

しかし、これはnull可能な列の場合、すぐに手に負えなくなります。 null許容列のすべての異なる組み合わせに対して部分的なインデックスが必要になります。 _(a)_、_(b)_、_(a,b)_の3つの部分インデックスである2つだけについて。この数は_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, ''));_

空の文字列(_''_)は文字タイプの明らかな候補ですが、any表示されないか、またはNULLで折りたたむことができる正当な値を使用できますyour「ユニーク」の定義。

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

_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 mustを指定する必要があります。

15

問題は、部分的なインデックスがなく、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
7
ypercubeᵀᴹ