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 laUPDATE
(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