J'ai l'UPSERT suivant dans PostgreSQL 9.5:
INSERT INTO chats ("user", "contact", "name")
VALUES ($1, $2, $3),
($2, $1, NULL)
ON CONFLICT("user", "contact") DO NOTHING
RETURNING id;
S'il n'y a pas de conflit, il renvoie quelque chose comme ceci:
----------
| id |
----------
1 | 50 |
----------
2 | 51 |
----------
Mais s'il y a des conflits, il ne renvoie aucune ligne:
----------
| id |
----------
Je veux retourner les nouvelles id
colonnes s'il n'y a pas de conflits ou renvoyer les id
colonnes existantes des colonnes en conflit.
Cela peut-il être fait? Si oui, comment?
ON CONFLICT UPDATE
pour qu'il y ait un changement dans la ligne. PuisRETURNING
le capturera.Réponses:
J'ai eu exactement le même problème, et je l'ai résolu en utilisant «faire une mise à jour» au lieu de «ne rien faire», même si je n'avais rien à mettre à jour. Dans votre cas, ce serait quelque chose comme ceci:
Cette requête renverra toutes les lignes, qu'elles viennent d'être insérées ou qu'elles aient existé auparavant.
la source
DO NOTHING
aspect de la question d'origine - pour moi, elle semble mettre à jour le champ de non-conflit (ici, "nom") pour toutes les lignes.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, unVACUUM
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: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
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 à unINSERT
), 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: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 automatiqueROLLBACK
), 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
), vousINSERT
détecterez un conflit (l'UNIQUE
index / la contrainte est absolue) etDO 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 .) LeSELECT
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 .
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: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 laVALUES
clause d'unINSERT
: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: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é parusr
.la source
ON CONFLICT SELECT...
où une chose cependant :)Upsert, étant une extension de la
INSERT
requête peut être défini avec deux comportements différents en cas de conflit de contraintes:DO NOTHING
ouDO UPDATE
.Notez également que cela
RETURNING
ne renvoie rien, car aucun n-uplet n'a été inséré . Désormais avecDO UPDATE
, il est possible d'effectuer des opérations sur le tuple avec lequel il y a un conflit. Notez tout d'abord qu'il est important de définir une contrainte qui sera utilisée pour définir qu'il y a un conflit.la source
Pour les insertions d'un seul élément, j'utiliserais probablement une fusion lors du retour de l'identifiant:
la source
Le but principal de l'utilisation
ON CONFLICT DO NOTHING
est d'éviter de lancer une erreur, mais cela ne provoquera aucun retour de ligne. Nous avons donc besoin d'un autreSELECT
pour obtenir l'identifiant existant.Dans ce SQL, s'il échoue sur les conflits, il ne retournera rien, alors le second
SELECT
obtiendra la ligne existante; s'il s'insère avec succès, il y aura deux mêmes enregistrements, nous devonsUNION
alors fusionner le résultat.la source
J'ai modifié la réponse étonnante d'Erwin Brandstetter, qui n'incrémentera pas la séquence et ne verrouillera aucune ligne en écriture. Je suis relativement nouveau dans PostgreSQL, alors n'hésitez pas à me le faire savoir si vous voyez des inconvénients à cette méthode:
Cela suppose que la table
chats
a une contrainte unique sur les colonnes(usr, contact)
.Mise à jour: ajout des révisions suggérées de spatar (ci-dessous). Merci!
la source
CASE WHEN r.id IS NULL THEN FALSE ELSE TRUE END AS row_exists
écrire simplementr.id IS NOT NULL as row_exists
. Au lieu d'WHERE row_exists=FALSE
écrire simplementWHERE NOT row_exists
.