Clarifier le ON CONFLICT DO UPDATE
comportement
Considérez le manuel ici :
Pour chaque ligne individuelle proposée pour l'insertion, l'insertion se poursuit ou, si une contrainte d'arbitre ou un index spécifié par
conflict_target
est violé, l'alternative conflict_action
est prise.
Accentuation sur moi. Vous n'avez donc pas à répéter les prédicats pour les colonnes incluses dans l'index unique dans la WHERE
clause à la UPDATE
(la 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 violation unique établit déjà ce que votre WHERE
clause ajoutée appliquerait de manière redondante.
Clarifier l'index partiel
Ajoutez une WHERE
clause pour en faire un index partiel réel comme vous l'avez mentionné vous-même (mais avec une logique inversée):
CREATE UNIQUE INDEX test_upsert_partial_idx
ON public.test_upsert (name, status)
WHERE test_field IS NULL; -- not: "is not null"
Pour utiliser cet index partiel dans votre UPSERT, vous avez besoin d'une correspondance comme @ypercube le démontre :conflict_target
ON CONFLICT (name, status) WHERE test_field IS NULL
Maintenant, l'indice partiel ci-dessus est déduit. Cependant , comme le note également le manuel :
[...] un indice unique non partiel (un index unique sans prédicat) sera déduit (et donc utilisé par ON CONFLICT
) si un tel indice satisfaisant à tous les autres critères est disponible.
Si vous avez un index supplémentaire (ou seulement) juste, (name, status)
il sera (également) utilisé. Un indice sur (name, status, test_field)
ne serait explicitement pas déduit. Cela n'explique pas votre problème, mais peut avoir ajouté à la confusion lors du test.
Solution
AIUI, rien de ce qui précède ne résout encore votre problème . Avec l'index partiel, seuls les cas spéciaux avec des valeurs NULL correspondantes seraient interceptés. Et d'autres lignes en double seraient insérées si vous n'avez pas d'autres index / contraintes uniques correspondants, ou déclencheraient une exception si vous en avez. Je suppose que ce n'est pas ce que tu veux. Vous écrivez:
La clé composite est composée de 20 colonnes, dont 10 peuvent être annulées.
Que considérez-vous exactement comme un doublon? Postgres (selon la norme SQL) ne considère pas deux valeurs NULL égales. Le manuel:
En général, une contrainte unique est violée s'il existe plusieurs lignes dans le tableau où les valeurs de toutes les colonnes incluses dans la contrainte sont égales. Cependant, deux valeurs nulles ne sont jamais considérées comme égales dans cette comparaison. Cela signifie que même en présence d'une contrainte unique, il est possible de stocker des lignes en double qui contiennent une valeur nulle dans au moins une des colonnes contraintes. Ce comportement est conforme à la norme SQL, mais nous avons entendu que d'autres bases de données SQL pourraient ne pas suivre cette règle. Soyez donc prudent lorsque vous développez des applications destinées à être portables.
En relation:
Je suppose que vous voulez que lesNULL
valeurs des 10 colonnes nullables soient considérées comme égales. Il est élégant et pratique de couvrir une seule colonne nullable avec un index partiel supplémentaire comme illustré ici:
Mais cela devient rapidement incontrôlable pour les colonnes plus nullables. Vous auriez besoin d'un index partiel pour chaque combinaison distincte de colonnes nullables. Pour seulement 2 de ces 3 index partiels pour (a)
, (b)
et (a,b)
. Le nombre augmente de façon exponentielle avec 2^n - 1
. Pour vos 10 colonnes nullables, pour couvrir toutes les combinaisons possibles de valeurs NULL, vous auriez déjà besoin de 1023 index partiels. Ne pas aller.
La solution simple: remplacer les valeurs NULL et définir les colonnes impliquées NOT NULL
, et tout fonctionnerait très bien avec une simple UNIQUE
contrainte.
Si ce n'est pas une option, je suggère un index d'expression avec COALESCE
pour remplacer NULL dans l'index:
CREATE UNIQUE INDEX test_upsert_solution_idx
ON test_upsert (name, status, COALESCE(test_field, ''));
La chaîne vide ( ''
) est un candidat évident pour les types de caractères, mais vous pouvez utiliser n'importe quelle valeur légale qui n'apparaît jamais ou peut être pliée avec NULL selon votre définition de "unique".
Utilisez ensuite cette instruction:
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);
Comme @ypercube, je suppose que vous voulez réellement ajouter count
au décompte existant. Étant donné que la colonne peut être NULL, l'ajout de NULL définirait la colonne NULL. Si vous définissez count NOT NULL
, vous pouvez simplifier.
Une autre idée serait de simplement supprimer le conflict_target de l'instruction pour couvrir toutes les violations uniques . Ensuite, vous pouvez définir divers index uniques pour une définition plus sophistiquée de ce qui est censé être "unique". Mais cela ne volera pas avec ON CONFLICT DO UPDATE
. Le manuel une fois de plus:
Pour ON CONFLICT DO NOTHING
, il est facultatif de spécifier un conflict_target; lorsqu'il est omis, les conflits avec toutes les contraintes utilisables (et les index uniques) sont traités. Pour ON CONFLICT DO UPDATE
, un conflict_target doit être fourni.
count = CASE WHEN EXCLUDED.count IS NULL THEN tu.count ELSE COALESCE(tu.count, 0) + COALESCE(EXCLUDED.count, 0) END
peut être simplifié pourcount = COALESCE(tu.count+EXCLUDED.count, EXCLUDED.count, tu.count)