La réponse actuellement acceptée semble correcte pour une seule cible de conflit, peu de conflits, de petits tuples et aucun déclencheur. Il évite le problème de concurrence 1 (voir ci-dessous) avec la force brute. La solution simple a son attrait, les effets secondaires peuvent être moins importants.
Dans tous les autres cas, cependant, ne mettez pas à jour des lignes identiques sans nécessité. Même si vous ne voyez aucune différence en surface, il existe divers effets secondaires :
Il pourrait déclencher des déclencheurs qui ne devraient pas être déclenchés.
Il verrouille en écriture les lignes «innocentes», ce qui peut entraîner des coûts pour les transactions simultanées.
Cela peut donner l'impression que la ligne est nouvelle, bien qu'elle soit ancienne (horodatage de la transaction).
Plus important encore , avec le modèle MVCC de PostgreSQL, une nouvelle version de ligne est écrite pour chaque UPDATE
, peu importe si les données de ligne ont changé. Cela entraîne une pénalité de performance pour l'UPSERT lui-même, un gonflement de table, un gonflement d'index, une pénalité de performance pour les opérations ultérieures sur la table, un VACUUM
coût. Un effet mineur pour quelques doublons, mais massif pour la plupart des dupes.
De plus , parfois, il n'est pas pratique ou même possible à utiliser ON CONFLICT DO UPDATE
. Le manuel:
Pour ON CONFLICT DO UPDATE
, un conflict_target
doit être fourni.
Une seule "cible de conflit" n'est pas possible si plusieurs index / contraintes sont impliqués.
Vous pouvez obtenir (presque) la même chose sans mises à jour vides et effets secondaires. Certaines des solutions suivantes fonctionnent également avec ON CONFLICT DO NOTHING
(pas de «cible de conflit»), pour capturer tous les conflits possibles qui pourraient survenir - ce qui peut être souhaitable ou non.
Sans charge d'écriture simultanée
WITH input_rows(usr, contact, name) AS (
VALUES
(text 'foo1', text 'bar1', text 'bob1') -- type casts in first row
, ('foo2', 'bar2', 'bob2')
-- more?
)
, ins AS (
INSERT INTO chats (usr, contact, name)
SELECT * FROM input_rows
ON CONFLICT (usr, contact) DO NOTHING
RETURNING id --, usr, contact -- return more columns?
)
SELECT 'i' AS source -- 'i' for 'inserted'
, id --, usr, contact -- return more columns?
FROM ins
UNION ALL
SELECT 's' AS source -- 's' for 'selected'
, c.id --, usr, contact -- return more columns?
FROM input_rows
JOIN chats c USING (usr, contact); -- columns of unique index
La source
colonne est un ajout facultatif pour montrer comment cela fonctionne. Vous en aurez peut-être besoin pour faire la différence entre les deux cas (un autre avantage par rapport aux écritures vides).
La dernière JOIN chats
fonctionne car les lignes nouvellement insérées à partir d'un CTE de modification de données attaché ne sont pas encore visibles dans la table sous-jacente. (Toutes les parties de la même instruction SQL voient les mêmes instantanés des tables sous-jacentes.)
Puisque l' VALUES
expression est autonome (pas directement attachée à un INSERT
), Postgres ne peut pas dériver de types de données à partir des colonnes cibles et vous devrez peut-être ajouter des transtypages de types explicites. Le manuel:
Lorsque VALUES
est utilisé dans INSERT
, les valeurs sont toutes automatiquement forcées au type de données de la colonne de destination correspondante. Lorsqu'il est utilisé dans d'autres contextes, il peut être nécessaire de spécifier le type de données correct. Si les entrées sont toutes des constantes littérales entre guillemets, il suffit de forcer la première pour déterminer le type supposé pour tous.
La requête elle-même (sans compter les effets secondaires) peut être un peu plus chère pour quelques dupes, en raison de la surcharge du CTE et du supplément SELECT
(qui devrait être bon marché puisque l'index parfait est là par définition - une contrainte unique est implémentée avec Un index).
Peut être (beaucoup) plus rapide pour de nombreux doublons. Le coût effectif des écritures supplémentaires dépend de nombreux facteurs.
Mais il y a de toute façon moins d'effets secondaires et de coûts cachés . C'est probablement moins cher dans l'ensemble.
Les séquences attachées sont encore avancées, car les valeurs par défaut sont renseignées avant de tester les conflits.
À propos des CTE:
Avec charge d'écriture simultanée
En supposant l' READ COMMITTED
isolation de transaction par défaut . En relation:
La meilleure stratégie pour se défendre contre les conditions de course dépend des exigences exactes, du nombre et de la taille des lignes du tableau et des UPSERT, du nombre de transactions simultanées, de la probabilité de conflits, des ressources disponibles et d'autres facteurs ...
Problème de concurrence 1
Si une transaction simultanée a été écrite sur une ligne que votre transaction essaie maintenant de UPSERT, votre transaction doit attendre que l'autre se termine.
Si l'autre transaction se termine par ROLLBACK
(ou toute erreur, c'est-à-dire automatique ROLLBACK
), votre transaction peut se dérouler normalement. Effet secondaire possible mineur: lacunes dans les nombres séquentiels. Mais pas de lignes manquantes.
Si l'autre transaction se termine normalement (implicite ou explicite COMMIT
), vous INSERT
détecterez un conflit (l' UNIQUE
index / la contrainte est absolue) et DO NOTHING
, par conséquent, vous ne retournerez pas non plus la ligne. (Il ne peut pas non plus verrouiller la ligne, comme illustré dans le problème de concurrence 2 ci-dessous, car il n'est pas visible .) Le SELECT
voit le même instantané depuis le début de la requête et ne peut pas non plus retourner la ligne encore invisible.
De telles lignes sont absentes du jeu de résultats (même si elles existent dans la table sous-jacente)!
Cela peut être correct tel quel . Surtout si vous ne renvoyez pas de lignes comme dans l'exemple et que vous êtes satisfait de savoir que la ligne est là. Si cela ne suffit pas, il existe différentes façons de contourner le problème.
Vous pouvez vérifier le nombre de lignes de la sortie et répéter l'instruction si elle ne correspond pas au nombre de lignes de l'entrée. Peut être assez bon pour le cas rare. Le but est de démarrer une nouvelle requête (peut être dans la même transaction), qui verra alors les lignes nouvellement validées.
Ou vérifiez les lignes de résultats manquantes dans la même requête et écrasez celles avec l'astuce de force brute démontrée dans la réponse d'Alextoni .
WITH input_rows(usr, contact, name) AS ( ... ) -- see above
, ins AS (
INSERT INTO chats AS c (usr, contact, name)
SELECT * FROM input_rows
ON CONFLICT (usr, contact) DO NOTHING
RETURNING id, usr, contact -- we need unique columns for later join
)
, sel AS (
SELECT 'i'::"char" AS source -- 'i' for 'inserted'
, id, usr, contact
FROM ins
UNION ALL
SELECT 's'::"char" AS source -- 's' for 'selected'
, c.id, usr, contact
FROM input_rows
JOIN chats c USING (usr, contact)
)
, ups AS ( -- RARE corner case
INSERT INTO chats AS c (usr, contact, name) -- another UPSERT, not just UPDATE
SELECT i.*
FROM input_rows i
LEFT JOIN sel s USING (usr, contact) -- columns of unique index
WHERE s.usr IS NULL -- missing!
ON CONFLICT (usr, contact) DO UPDATE -- we've asked nicely the 1st time ...
SET name = c.name -- ... this time we overwrite with old value
-- SET name = EXCLUDED.name -- alternatively overwrite with *new* value
RETURNING 'u'::"char" AS source -- 'u' for updated
, id --, usr, contact -- return more columns?
)
SELECT source, id FROM sel
UNION ALL
TABLE ups;
C'est comme la requête ci-dessus, mais nous ajoutons une étape de plus avec le CTE ups
, avant de renvoyer l' ensemble de résultats complet . Ce dernier CTE ne fera rien la plupart du temps. Ce n'est que si des lignes manquent dans le résultat renvoyé, nous utilisons la force brute.
Encore plus de frais généraux. Plus il y a de conflits avec des lignes préexistantes, plus il est probable que cela surclassera l'approche simple.
Un effet secondaire: le 2ème UPSERT écrit les lignes dans le désordre, donc il réintroduit la possibilité de blocages (voir ci-dessous) si trois transactions ou plus écrivant sur les mêmes lignes se chevauchent. Si c'est un problème, vous avez besoin d'une solution différente - comme répéter l'ensemble de la déclaration comme mentionné ci-dessus.
Problème de concurrence 2
Si des transactions simultanées peuvent écrire dans les colonnes impliquées des lignes affectées et que vous devez vous assurer que les lignes que vous avez trouvées sont toujours là à un stade ultérieur de la même transaction, vous pouvez verrouiller les lignes existantes à moindre coût dans le CTE ins
(qui autrement seraient déverrouillées) avec:
...
ON CONFLICT (usr, contact) DO UPDATE
SET name = name WHERE FALSE -- never executed, but still locks the row
...
Et ajouter une clause de verrouillage à la SELECT
ainsi, commeFOR UPDATE
.
Cela oblige les opérations d'écriture concurrentes à attendre la fin de la transaction, lorsque tous les verrous sont libérés. Alors soyez bref.
Plus de détails et d'explications:
Des blocages?
Protégez-vous contre les blocages en insérant des lignes dans un ordre cohérent . Voir:
Types de données et casts
Tableau existant comme modèle pour les types de données ...
Des casts de type explicite pour la première ligne de données dans l' VALUES
expression autonome peuvent être peu pratiques. Il existe des moyens de contourner cela. Vous pouvez utiliser n'importe quelle relation existante (table, vue, ...) comme modèle de ligne. La table cible est le choix évident pour le cas d'utilisation. Les données d'entrée sont automatiquement forcées aux types appropriés, comme dans la VALUES
clause d'un INSERT
:
WITH input_rows AS (
(SELECT usr, contact, name FROM chats LIMIT 0) -- only copies column names and types
UNION ALL
VALUES
('foo1', 'bar1', 'bob1') -- no type casts here
, ('foo2', 'bar2', 'bob2')
)
...
Cela ne fonctionne pas pour certains types de données. Voir:
... et noms
Cela fonctionne également pour tous les types de données.
Lors de l'insertion dans toutes les colonnes (de début) du tableau, vous pouvez omettre les noms de colonne. En supposant que le tableau chats
de l'exemple ne comprend que les 3 colonnes utilisées dans UPSERT:
WITH input_rows AS (
SELECT * FROM (
VALUES
((NULL::chats).*) -- copies whole row definition
('foo1', 'bar1', 'bob1') -- no type casts needed
, ('foo2', 'bar2', 'bob2')
) sub
OFFSET 1
)
...
A part: n'utilisez pas de mots réservés comme "user"
identifiant. C'est une arme à pied chargée. Utilisez des identifiants légaux, en minuscules et sans guillemets. Je l'ai remplacé par usr
.
ON CONFLICT UPDATE
pour qu'il y ait un changement dans la ligne. PuisRETURNING
le capturera.