NULL değerlerle PostgreSQL UPSERT sorunu


13

Postgres 9.5'teki yeni UPSERT özelliğini kullanma konusunda sorun yaşıyorum

Başka bir tablodan veri toplamak için kullanılan bir tablo var. Kompozit anahtar, 10 tanesi boş olabilen 20 sütundan oluşur. Aşağıda, özellikle NULL değerleri ile yaşadığım sorunun daha küçük bir sürümünü oluşturduk.

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)
);

Bu sorguyu çalıştırmak gerektiği gibi çalışır (İlk ekleme, ardından sonraki ekleme sayısı sayımı artırır):

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';

Ancak bu sorguyu çalıştırırsam, ilk satırın sayısını artırmak yerine her seferinde 1 satır eklenir:

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;

Benim sorunum bu. Ben sadece sayım değerini artırmak ve null değerleri ile birden çok özdeş satır oluşturmak gerekir.

Kısmi benzersiz bir dizin eklemeye çalışılıyor:

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

Ancak, aynı sonuçları verir, birden çok boş satır eklenir veya eklemeye çalışırken bu hata iletisi:

HATA: ON CONFLICT spesifikasyonuyla eşleşen benzersiz veya hariç tutma kısıtlaması yok

Zaten kısmi dizine ekstra ayrıntılar eklemeye çalıştım WHERE test_field is not null OR identifier is not null. Ancak, eklerken kısıtlama hata mesajı alıyorum.

Yanıtlar:


15

ON CONFLICT DO UPDATEDavranışı netleştirin

Buradaki kılavuzu düşünün :

Ekleme için önerilen her bir satır için, ekleme işlemi devam eder veya belirtilen bir hakem kısıtlaması veya dizini conflict_targetihlal edilirse alternatif conflict_actionalınır.

Cesur vurgu benim. Dolayısıyla WHERE, UPDATE(the conflict_action) maddesindeki benzersiz dizine dahil edilen sütunlar için tahminleri tekrarlamanız gerekmez :

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'

Eşsiz ihlal, ek WHEREmaddenizin gereğinden fazla zorlayacağını zaten tespit ediyor .

Kısmi dizini açıklığa kavuştur

Kendinizden bahsettiğiniz gibi WHEREgerçek bir kısmi dizin yapmak için bir cümle ekleyin (ancak ters mantıkla):

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

To kullanmak için Upsert bu kısmi endeksi bir eşleştirme ihtiyaç @ypercube gösterir gibi :conflict_target

ON CONFLICT (name, status) WHERE test_field IS NULL

Şimdi yukarıdaki kısmi endeks çıkarılmaktadır. Ancak , el kitabında da belirtildiği gibi :

[...] kısmi olmayan benzersiz bir indeks (yüklemi olmayan benzersiz bir indeks), ON CONFLICTdiğer tüm kriterleri karşılayan böyle bir indeks mevcut olduğunda çıkartılacaktır (ve böylece tarafından kullanılacaktır ).

Ek (veya yalnızca) bir dizininiz varsa, yalnızca (name, status)(ayrıca) kullanılır. Üzerinde bir dizin (name, status, test_field)açıkça olurdu değil çıkarılabilir. Bu sorununuzu açıklamaz, ancak test sırasında karışıklığa neden olmuş olabilir.

Çözüm

AIUI, yukarıdakilerin hiçbiri sorununuzu henüz çözmedi . Kısmi endeks ile yalnızca eşleşen NULL değerleri olan özel durumlar yakalanır. Eşleşen başka benzersiz dizinler / kısıtlamalarınız yoksa diğer yinelenen satırlar eklenir veya varsa bir istisna oluşturur. Sanırım istediğin bu değil. Sen yaz:

Kompozit anahtar, 10'u boş olabilen 20 sütundan oluşur.

Tam olarak neyi yineleniyorsunuz? Postgres (SQL standardına göre) iki NULL değerinin eşit olduğunu düşünmez. Kullanım kılavuzu:

Genel olarak, tabloda, kısıtlamaya dahil edilen tüm sütunların değerlerinin eşit olduğu birden fazla satır varsa benzersiz bir kısıtlama ihlal edilir. Ancak bu karşılaştırmada iki boş değer asla eşit kabul edilmez. Bu, benzersiz bir kısıtlamanın varlığında bile, kısıtlanmış sütunlardan en az birinde boş değer içeren yinelenen satırların saklanabileceği anlamına gelir. Bu davranış, SQL standardına uygundur, ancak diğer SQL veritabanlarının bu kurala uymayabileceğini duyduk. Bu nedenle taşınabilir olması amaçlanan uygulamalar geliştirirken dikkatli olun.

İlişkili:

NULLHer 10 null sütunundaki değerlerin eşit kabul edilmesiniistediğinizi varsayalım . Burada gösterildiği gibi ek kısmi bir dizine sahip tek bir boş sütunun kapsanması zarif ve pratiktir:

Ancak, daha boş sütunlar için bu hızla elden çıkar. Sıfırlanabilir sütunların her farklı birleşimi için kısmi bir dizine ihtiyacınız vardır. Sadece 2 için 3 kısmi endeksler var olanların için (a), (b)ve (a,b). Sayı katlanarak artıyor 2^n - 1. 10 boş değerli sütununuz için, olası tüm NULL değer kombinasyonlarını kapsamak için zaten 1023 kısmi dizine ihtiyacınız vardır. Gitme.

Basit çözüm: NULL değerleri değiştirin ve ilgili sütunları tanımlayın NOT NULL, her şey basit bir UNIQUEkısıtlamayla sorunsuz çalışır .

Bu bir seçenek değilse COALESCE, dizinde NULL yerine bir ifade dizini öneririz :

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

Boş dize ( '') karakteri türleri için bariz bir aday olduğunu, ancak kullanabileceğiniz herhangi görünür ya göre NULL ile katlanabilir ya hiç yasal değeri sizin "benzersiz" tanımına.

Sonra şu ifadeyi kullanın:

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 gibi count, mevcut sayıma eklemek istediğinizi varsayalım . Sütun NULL olabileceğinden, NULL eklenmesi NULL sütununu ayarlar. Tanımlarsanız count NOT NULL, basitleştirebilirsiniz.


Başka bir fikir sadece düşmesi olacaktır conflict_target kapsayacak şekilde ifadeden tüm benzersiz ihlalleri . Ardından, "benzersiz" olması gerekenlerin daha karmaşık bir tanımı için çeşitli benzersiz dizinler tanımlayabilirsiniz. Ama bu uçmayacak ON CONFLICT DO UPDATE. Kılavuz bir kez daha:

Için ON CONFLICT DO NOTHING, bir anlaşmazlık_hedef belirtmek isteğe bağlıdır; atlandığında, tüm kullanılabilir kısıtlamalarla (ve benzersiz dizinlerle) çakışmalar ele alınır. İçin ON CONFLICT DO UPDATE, bir conflict_target gerekir sağlanacaktır.


1
Güzel. Soruyu ilk kez okuduğumda 20-10 sütun bölümünü atladım ve daha sonra tamamlayacak zamanım yoktu. count = CASE WHEN EXCLUDED.count IS NULL THEN tu.count ELSE COALESCE(tu.count, 0) + COALESCE(EXCLUDED.count, 0) ENDBasitleştiirlebilircount = COALESCE(tu.count+EXCLUDED.count, EXCLUDED.count, tu.count)
ypercubeᵀᴹ

Tekrar baktığımda, "basitleştirilmiş" versiyonum o kadar kendi kendini belgelemiyor.
ypercubeᵀᴹ

@ ypercubeᵀᴹ: Önerilen güncellemenizi uyguladım. Daha basit, teşekkürler.
Erwin Brandstetter

Sen iyi @ErwinBrandstetter
Seamus Abshere

7

Sorun, kısmi bir dizin yok ve ON CONFLICTsözdizimi test_upsert_upsert_id_idxdizin ama diğer benzersiz kısıtlama ile eşleşmiyor olduğunu düşünüyorum .

Dizini kısmi (ile WHERE test_field IS NULL) olarak tanımlarsanız :

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 ;

ve bu satırlar zaten tabloda:

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

sorgu başarılı olur:

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

aşağıdaki sonuçlarla:

('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

Bu, kısmi bir dizinin nasıl kullanılacağını açıklar. Ama (bence) henüz sorunu çözmüyor.
Erwin Brandstetter

güncelleme yapılmadığından 'maria' için sayı 1'de kalmamalı mı?
mpprdev

@mpprdev evet, haklısın.
ypercubeᵀᴹ
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.