web-dev-qa-db-fra.com

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

J'ai cette table (générée par Django):

CREATE TABLE feeds_person (
  id serial PRIMARY KEY,
  created timestamp with time zone NOT NULL,
  modified timestamp with time zone NOT NULL,
  name character varying(4000) NOT NULL,
  url character varying(1000) NOT NULL,
  email character varying(254) NOT NULL,
  CONSTRAINT feeds_person_name_ad8c7469_uniq UNIQUE (name, url, email)
);

J'essaie d'insérer en masse un grand nombre de données en utilisant INSERT avec un ON CONFLICT clause.

Le problème est que j'ai besoin de récupérer le id pour tous des lignes, qu'elles soient déjà existantes ou non.

Dans d'autres cas, je ferais quelque chose comme:

INSERT INTO feeds_person (created, modified, name, url, email)
VALUES blah blah blah
ON CONFLICT (name, url, email) DO UPDATE SET url = feeds_person.url
RETURNING id

Faire le UPDATE fait que l'instruction retourne le id de cette ligne. Sauf que cela ne fonctionne pas avec cette table. I pensez cela ne fonctionne pas parce que j'ai plusieurs champs uniques ensemble alors que dans d'autres cas, j'ai utilisé cette méthode, je n'ai eu qu'un seul champ unique.

J'obtiens cette erreur lorsque j'essaie d'exécuter le SQL via le curseur de Django:

Django.db.utils.ProgrammingError: ON CONFLICT DO UPDATE command cannot affect row a second time
HINT:  Ensure that no rows proposed for insertion within the same command have duplicate constrained values.

Comment puis-je faire l'insertion en bloc avec cette table et récupérer les identifiants insérés et existants?

14
Dustin Wyatt

L'erreur que vous obtenez:

La commande ON CONFLICT DO UPDATE ne peut pas affecter la ligne une deuxième fois

indique que vous essayez d'insérer plusieurs fois la même ligne dans une seule commande. En d'autres termes: vous avez des dupes sur (name, url, email) Dans votre liste VALUES. Pliez les doublons (si c'est une option) et cela devrait fonctionner. Mais vous devrez décider quelle ligne choisir parmi chaque jeu 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;

Comme nous utilisons maintenant une expression VALUES autonome, vous devez ajouter des transtypages de types explicites pour les types non par défaut. Comme:

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

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

Il existe des moyens de déterminer quelle ligne choisir parmi chaque jeu de dupes:

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

Notez que RETURNING ne rend pas visible l'alias "EXCLUDED.*" De UPDATE (seul l'alias générique "TARGET.*" Y est visible). On pense que cela crée une ambiguïté ennuyeuse 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 si RETURNING- des 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 involontaires. Vous n'avez pas strictement besoin d'UPSERT pour commencer, votre cas ressemble plus à "SELECT or INSERT". En relation:

One une manière plus propre d'insérer un ensemble de lignes serait avec des CTE modifiant les 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.

À l'origine, j'avais ajouté un prédicat NOT EXISTS 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 renvoyé 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 un 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'une requête SELECT distincte pour toutes les lignes - dans la même transaction pour résoudre ce problème. Ce qui à son tour en ouvre une autre petite fenêtre pour une condition de concurrence critique 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 strict). Ou avec un verrou d'écriture (éventuellement coûteux ou même inacceptable) sur toute la table. Vous pouvez obtenir tout comportement dont vous avez besoin, mais il peut y avoir un prix à payer.

En relation:

27
Erwin Brandstetter