PostgreSQL
 sql >> Base de données >  >> RDS >> PostgreSQL

Comment inclure des lignes exclues dans RETURNING from INSERT ... ON CONFLICT

L'erreur que vous obtenez :

ON CONFLICT DO UPDATE ne peut pas affecter la ligne une seconde fois

indique que vous essayez d'upsert la même ligne plus d'une fois dans une seule commande. En d'autres termes :vous avez des doublons sur (name, url, email) dans vos VALUES liste. Pliez les doublons (si c'est une option) et cela devrait fonctionner. Mais vous devrez décider quelle ligne choisir parmi chaque ensemble de dupes.

INSERT INTO feeds_person (created, modified, name, url, email)
SELECT DISTINCT ON (name, url, email) *
FROM  (
   VALUES
   ('blah', 'blah', 'blah', 'blah', 'blah')
   -- ... more
   ) v(created, modified, name, url, email)  -- match column list
ON     CONFLICT (name, url, email) DO UPDATE
SET    url = feeds_person.url
RETURNING id;

Puisque nous utilisons un VALUES autonome expression maintenant, vous devez ajouter des conversions de type explicites pour les types autres que ceux par défaut. Comme :

VALUES
    (timestamptz '2016-03-12 02:47:56+01'
   , timestamptz '2016-03-12 02:47:56+01'
   , 'n3', 'u3', 'e3')
   ...

Votre timestamptz les colonnes ont besoin d'un cast de type explicite, tandis que les types de chaîne peuvent fonctionner avec le text par défaut . (Vous pouvez toujours caster en varchar(n) tout de suite.)

Il existe des moyens de déterminer quelle ligne sélectionner dans chaque ensemble de dupes :

  • Sélectionner la première ligne de chaque groupe GROUP BY ?

Vous avez raison, il n'y a (actuellement) aucun moyen d'être exclu lignes dans le RETURNING clause. Je cite le Wiki Postgres :

Notez que RETURNING ne rend pas visible le "EXCLUDED.* " alias de la UPDATE (juste le générique "TARGET.* " l'alias est visible ici). On pense que cela crée une ambiguïté gênante pour les cas simples et courants [30] pour peu ou pas d'avantages. À un moment donné dans le futur, nous pourrons rechercher un moyen d'exposer ifRETURNING -les tuples projetés ont été insérés et mis à jour, mais cela n'a probablement pas besoin d'en faire la première itération validée de la fonctionnalité [31].

Cependant , vous ne devez pas mettre à jour des lignes qui ne sont pas censées être mises à jour. Les mises à jour vides sont presque aussi chères que les mises à jour régulières - et peuvent avoir des effets secondaires imprévus. Vous n'avez pas strictement besoin d'UPSERT pour commencer, votre cas ressemble plus à "SELECT ou INSERT". Connexe :

  • SELECT ou INSERT est-il dans une fonction sujette à des conditions de concurrence ?

Un une façon plus propre d'insérer un ensemble de lignes serait d'utiliser des CTE modificateurs de données :

WITH val AS (
   SELECT DISTINCT ON (name, url, email) *
   FROM  (
      VALUES 
      (timestamptz '2016-1-1 0:0+1', timestamptz '2016-1-1 0:0+1', 'n', 'u', 'e')
    , ('2016-03-12 02:47:56+01', '2016-03-12 02:47:56+01', 'n1', 'u3', 'e3')
      -- more (type cast only needed in 1st row)
      ) v(created, modified, name, url, email)
   )
, ins AS (
   INSERT INTO feeds_person (created, modified, name, url, email)
   SELECT created, modified, name, url, email FROM val
   ON     CONFLICT (name, url, email) DO NOTHING
   RETURNING id, name, url, email
   )
SELECT 'inserted' AS how, id FROM ins  -- inserted
UNION  ALL
SELECT 'selected' AS how, f.id         -- not inserted
FROM   val v
JOIN   feeds_person f USING (name, url, email);

La complexité supplémentaire devrait payer pour les grandes tables où INSERT est la règle et SELECT l'exception.

A l'origine, j'avais ajouté un NOT EXISTS prédicat sur le dernier SELECT pour éviter les doublons dans le résultat. Mais c'était redondant. Tous les CTE d'une même requête voient les mêmes instantanés de tables. L'ensemble est retourné avec ON CONFLICT (name, url, email) DO NOTHING est mutuellement exclusif à l'ensemble renvoyé après le INNER JOIN sur les mêmes colonnes.

Malheureusement, cela ouvre également une petite fenêtre pour une condition de concurrence . Si ...

  • une transaction simultanée insère des lignes en conflit
  • ne s'est pas encore engagé
  • mais s'engage finalement

... certaines lignes peuvent être perdues.

Vous pouvez simplement INSERT .. ON CONFLICT DO NOTHING , suivi d'un SELECT distinct requête pour toutes les lignes - dans la même transaction pour surmonter cela. Ce qui à son tour ouvre une autre petite fenêtre pour une condition de concurrence si des transactions simultanées peuvent valider des écritures dans la table entre INSERT et SELECT (par défaut READ COMMITTED niveau d'isolement). Peut être évité avec REPEATABLE READ isolation des transactions (ou plus stricte). Ou avec un verrou en écriture (éventuellement coûteux voire inacceptable) sur toute la table. Vous pouvez obtenir n'importe quel comportement dont vous avez besoin, mais il peut y avoir un prix à payer.

Connexe :

  • Comment utiliser RETURNING avec ON CONFLICT dans PostgreSQL ?
  • Renvoyer les lignes de INSERT avec ON CONFLICT sans avoir besoin de mettre à jour