web-dev-qa-db-fra.com

Comment utiliser RETURNING avec ON CONFLICT dans PostgreSQL?

J'ai le 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 conflits, cela retourne quelque chose comme ceci:

----------
    | id |
----------
  1 | 50 |
----------
  2 | 51 |
----------

Mais s'il y a des conflits, cela ne renvoie aucune ligne:

----------
    | id |
----------

Je souhaite renvoyer les nouvelles colonnes id s'il n'y a pas de conflits ou renvoyer les colonnes id existantes des colonnes en conflit.
Cela peut-il être fait? Si oui, comment?

120
zola

J'avais exactement le même problème et je l'ai résolu en utilisant "do update" au lieu de "ne rien faire", même si je n'avais rien à mettre à jour. Dans votre cas, ce serait quelque chose comme ça:

INSERT INTO chats ("user", "contact", "name") 
       VALUES ($1, $2, $3), 
              ($2, $1, NULL) 
ON CONFLICT("user", "contact") DO UPDATE SET name=EXCLUDED.name RETURNING id;

Cette requête renverra toutes les lignes, qu'elles viennent d'être insérées ou qu'elles existaient auparavant.

68
Alextoni

La réponse actuellement acceptée semble acceptable pour une cibleuniqueconflit,peuconflits, petits tuples et aucun déclencheur. Et cela évitele 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.

Pour tous les autres cas, cependant, faitesnot/ mettez à jour des lignes identiques sans nécessité. Même si vous ne voyez aucune différence en surface, il y a divers effets secondaires :

  • Il pourrait déclencher des déclencheurs qui ne devraient pas être déclenchés.

  • Il verrouille en écriture des lignes "innocentes", ce qui peut entraîner des coûts pour les transactions simultanées.

  • La ligne peut sembler nouvelle, même si elle est ancienne (horodatage de la transaction).

  • Plus important encore , avec le modèle MVCC de PostgreSQL , une nouvelle version de ligne est écrite dans un sens ou dans l'autre, que les données de la ligne soient identiques ou non. Cela entraîne une pénalité de performance pour UPSERT lui-même, un gonflement de table, un gonflement d'index, une pénalité de performance pour toutes les opérations ultérieures sur la table, VACUUM coût. Un effet mineur pour quelques doublons, maismassivepour la plupart des dupes.

Plus , Parfois, il n'est pas pratique ni même possible d'utiliser ON CONFLICT DO UPDATE. Le manuel:

Pour ON CONFLICT DO UPDATE, unconflict_targetdoit être fourni.

Vous pouvez atteindre (presque) la même chose sans mises à jour vides et effets secondaires. Et certaines des solutions suivantes fonctionnent également avec ON CONFLICT DO NOTHING (pas de "cible de conflit"), afin d’attraperallles conflits possibles pouvant survenir. (Peut ou peut ne pas être souhaitable.)

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 colonne source est un ajout facultatif pour montrer comment cela fonctionne. Vous pouvez en avoir réellement besoin pour faire la différence entre les deux cas (autre avantage par rapport aux écritures vides).

La version finale de JOIN chats fonctionne car les lignes récemment insérées à partir d'un CTE attaché modifiant les données 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.)

Comme l'expression VALUES est autonome (non directement attachée à un INSERT), Postgres ne peut pas dériver les types de données des colonnes cibles et vous devrez peut-être ajouter des transformations de types explicites. Le manuel:

Lorsque VALUES est utilisé dans INSERT, les valeurs sont automatiquement forcées dans le 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 citées, le fait de forcer la première suffit à déterminer le type supposé pour toutes.

La requête elle-même peut être un peu plus chère pourpeudupes, en raison des frais généraux du CTE et de la SELECT supplémentaire (qui devrait être bon marché, car l'indice parfait existe par définition - une contrainte unique est implémentée avec un index).

Peut être (beaucoup) plus rapide pourmanyduplicates. Le coût effectif des écritures supplémentaires dépend de nombreux facteurs.

Mais il y a moins d’effets secondaires et de coûts cachés dans tous les cas. C'est probablement moins cher dans l'ensemble.

(Les séquences attachées sont toujours avancées, car les valeurs par défaut sont renseignées dansavantpour tester les conflits.)

À propos des CTE:

Avec charge d'écriture simultanée

En supposant que l'isolation de transaction READ COMMITTED par défaut.

Réponse associée sur dba.SE avec explication détaillée:

La meilleure stratégie de défense contre les conditions de concurrence 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 transmettre à UPSERT, votre transaction doit attendre la fin de l’autre.

Si l'autre transaction se termine par ROLLBACK (ou par toute erreur, c'est-à-dire automatique ROLLBACK), votre transaction peut s'effectuer normalement. Effet secondaire mineur: lacunes dans les nombres séquentiels. Mais pas de lignes manquantes.

Si l'autre transaction se termine normalement (COMMIT implicite ou explicite), votre INSERT détectera un conflit (l'indicateur/la contrainte UNIQUE est absolu) et DO NOTHING, par conséquent, ne renverra pas non plus la ligne. (Vous ne pouvez pas non plus verrouiller la ligne, comme indiqué dansnuméro d'accès simultané 2ci-dessous, car il s'agit denon visible.) La SELECT voit le même instantané de la début de la requête et ne peut pas non plus renvoyer la ligne encore invisible.

De telles lignes sont absentes du jeu de résultats (même si elles existent dans la table sous-jacente)!

Ceci peut être correct tel quel . Surtout si vous ne renvoyez pas de lignes comme dans l'exemple et êtes satisfait de savoir que la ligne est là. Si cela ne suffit pas, il existe différentes manières de le contourner.

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 point est de commencer une nouvelle requête (peut être dans la même transaction), qui verra alors les lignes nouvellement validées.

Ou recherchez les lignes de résultat manquantesdansla même requête etécrasezceux qui utilisent la ruse de la 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 supplémentaire avec le CTE ups, avant de renvoyer l'ensemble de résultatscomplet/. Ce dernier CTE ne fera rien la plupart du temps. Ce n'est que si les lignes disparaissent du résultat renvoyé que nous utilisons la force brute.

Plus de frais généraux, pour le moment. Plus il y a de conflits avec des lignes préexistantes, plus il est probable que cela surperformera l'approche simple.

Un effet secondaire: le deuxième UPSERT écrit les lignes dans le désordre, ainsi il réintroduit la possibilité de blocages (voir ci-dessous) sitrois ou plustransactions écrivant sur les mêmes lignes se chevauchent. Si cela pose un problème, vous avez besoin d'une solution différente.

Problème de simultanéité 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 présentes ultérieurement dans la même transaction, vous pouvez verrouiller les lignes à moindre coût avec:

...
ON CONFLICT (usr, contact) DO UPDATE
SET name = name WHERE FALSE  -- never executed, but still locks the row
...

Et ajoutez également une clause de verrouillage à la SELECT, telle que FOR 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 explications:

Des impasses?

Défendre contre des blocages en insérant des lignes dans ordre cohérent . Voir:

Types de données et conversions

Table existante en tant que modèle pour les types de données ...

Les conversions de type explicite pour la première ligne de données dans l'expression VALUES libre peuvent être gênantes. Il y a des façons de le contourner. Vous pouvez utiliser n'importe quelle relation existante (table, vue, ...) en tant que 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 dans les types appropriés, comme dans une clause VALUES 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 needed
    , ('foo2', 'bar2', 'bob2')
   )
   ...

Cela ne fonctionne pas pour certains types de données (explication dans la réponse liée en bas). L'astuce suivante fonctionne pour les types de donnéesall:

... et des noms

Si vous insérez des lignes entières (toutes les colonnes de la table - ou au moins un ensemble deleadcolumns), vous pouvez également omettre les noms de colonne. En supposant que la table chats dans l'exemple ne comporte que les 3 colonnes utilisées:

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

Explication détaillée et autres alternatives:


De plus: n'utilisez pas de mots réservés tels que "user" comme identifiant. C'est un footgun chargé. Utilisez des identificateurs légaux, minuscules et non cités. Je l'ai remplacé par usr.

154
Erwin Brandstetter

Upsert, étant une extension de la requête INSERT peut être défini avec deux comportements différents en cas de conflit de contraintes: DO NOTHING ou DO UPDATE.

INSERT INTO upsert_table VALUES (2, 6, 'upserted')
   ON CONFLICT DO NOTHING RETURNING *;

 id | sub_id | status
----+--------+--------
 (0 rows)

Notez également que RETURNING ne renvoie rien car aucun n-uplat n'a été inséré. Maintenant, avec DO UPDATE, il est possible d'effectuer des opérations sur le tuple en conflit. Notons d’abord qu’il est important de définir une contrainte qui servira à définir qu’il existe un conflit.

INSERT INTO upsert_table VALUES (2, 2, 'inserted')
   ON CONFLICT ON CONSTRAINT upsert_table_sub_id_key
   DO UPDATE SET status = 'upserted' RETURNING *;

 id | sub_id |  status
----+--------+----------
  2 |      2 | upserted
(1 row)
13
Jaumzera