Chiarire ON CONFLICT DO UPDATE
comportamento
Considera il manuale qui :
Per ogni singola riga proposta per l'inserimento, procede l'inserimento oppure, se conflict_target
viene violato un vincolo arbitrale o un indice specificato da
, conflict_action
viene presa l'alternativa .
Enorme enfasi sulla mia. Quindi non è necessario ripetere i predicati per le colonne incluse nell'indice univoco nella WHERE
clausola al UPDATE
(il 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'
La violazione unica stabilisce già ciò che hai aggiunto WHERE
clausola imporrebbe in modo ridondante.
Chiarire l'indice parziale
Aggiungi una WHERE
clausola per renderla un indice parziale effettivo come hai menzionato (ma con logica invertita):
CREATE UNIQUE INDEX test_upsert_partial_idx
ON public.test_upsert (name, status)
WHERE test_field IS NULL; -- not: "is not null"
Per utilizzare questo indice parziale nel tuo UPSERT hai bisogno di una corrispondenza come dimostra @ypercube :conflict_target
ON CONFLICT (name, status) WHERE test_field IS NULL
Ora viene dedotto l'indice parziale sopra. Tuttavia , come osserva anche il manuale :
[...] un indice univoco non parziale (un indice univoco senza predicato) verrà dedotto (e quindi utilizzato da ON CONFLICT
) se tale indice soddisfa tutti gli altri criteri.
Se hai solo un indice aggiuntivo (o solo) (name, status)
, verrà (anche) usato. Un indice (name, status, test_field)
attivo non lo farebbe esplicitamente essere dedotto. Questo non spiega il tuo problema, ma potrebbe aver aggiunto confusione durante il test.
Soluzione
AIUI, nessuno dei precedenti risolve il tuo problema . Con l'indice parziale, verrebbero rilevati solo casi speciali con valori NULL corrispondenti. E altre righe duplicate verrebbero inserite se non si hanno altri indici / vincoli univoci corrispondenti, o se si crea un'eccezione. Suppongo che non sia quello che vuoi. Scrivi:
La chiave composita è composta da 20 colonne, 10 delle quali possono essere nullable.
Cosa consideri esattamente un duplicato? Postgres (secondo lo standard SQL) non considera due valori NULL uguali. Il manuale:
In generale, un vincolo univoco viene violato se nella tabella è presente più di una riga in cui i valori di tutte le colonne incluse nel vincolo sono uguali. Tuttavia, due valori nulli non sono mai considerati uguali in questo confronto. Ciò significa che anche in presenza di un vincolo univoco è possibile memorizzare righe duplicate che contengono un valore null in almeno una delle colonne vincolate. Questo comportamento è conforme allo standard SQL, ma abbiamo sentito che altri database SQL potrebbero non seguire questa regola. Quindi fai attenzione quando sviluppi applicazioni che devono essere portatili.
Relazionato:
Suppongo che desideri che iNULL
valori in tutte e 10 le colonne annullabili siano considerati uguali. È elegante e pratico coprire una singola colonna nullable con un indice parziale aggiuntivo come mostrato qui:
Ma questo sfugge di mano rapidamente per colonne più annullabili. Avresti bisogno di un indice parziale per ogni combinazione distinta di colonne nullable. Solo per 2 di questi sono 3 indici parziali per (a)
, (b)
e (a,b)
. Il numero sta crescendo esponenzialmente con 2^n - 1
. Per le tue 10 colonne annullabili, per coprire tutte le possibili combinazioni di valori NULL, avresti già bisogno di 1023 indici parziali. Non andare.
La soluzione semplice: sostituire i valori NULL e definire le colonne coinvolte NOT NULL
, e tutto funzionerebbe perfettamente con un semplice UNIQUE
vincolo.
Se questa non è un'opzione, suggerisco un indice di espressione con COALESCE
per sostituire NULL nell'indice:
CREATE UNIQUE INDEX test_upsert_solution_idx
ON test_upsert (name, status, COALESCE(test_field, ''));
La stringa vuota ( ''
) è un candidato naturale per i tipi di carattere, ma è possibile utilizzare qualsiasi valore legale che o non appare mai o può essere piegato con NULL in base alla tua definizione di "unico".
Quindi utilizzare questa affermazione:
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);
Come @ypercube suppongo che tu voglia davvero aggiungere count
al conteggio esistente. Poiché la colonna può essere NULL, l'aggiunta di NULL imposterà la colonna NULL. Se lo definisci count NOT NULL
, puoi semplificare.
Un'altra idea sarebbe quella di eliminare il target_target dalla dichiarazione per coprire tutte le violazioni uniche . Quindi potresti definire vari indici unici per una definizione più sofisticata di ciò che dovrebbe essere "unico". Ma questo non volerà con ON CONFLICT DO UPDATE
. Il manuale ancora una volta:
Per ON CONFLICT DO NOTHING
, è facoltativo specificare un conflitto_target; se omesso, vengono gestiti i conflitti con tutti i vincoli utilizzabili (e gli indici univoci). Per ON CONFLICT DO UPDATE
, un conflitto_target deve essere fornito.
count = CASE WHEN EXCLUDED.count IS NULL THEN tu.count ELSE COALESCE(tu.count, 0) + COALESCE(EXCLUDED.count, 0) END
può essere semplificato incount = COALESCE(tu.count+EXCLUDED.count, EXCLUDED.count, tu.count)