comment émuler «insérer ignorer» et «sur la mise à jour de clé en double» (fusion sql) avec postgresql?


142

Certains serveurs SQL ont une fonctionnalité qui INSERTest ignorée si elle enfreint une contrainte de clé primaire / unique. Par exemple, MySQL a INSERT IGNORE.

Quelle est la meilleure façon d'émuler INSERT IGNOREet ON DUPLICATE KEY UPDATEavec PostgreSQL?




6
à partir de 9.5, c'est possible nativement: stackoverflow.com/a/34639631/4418
warren

Émuler MySQL: ON DUPLICATE KEY UPDATEsur PgSQL 9.5 est encore quelque peu impossible, car l' ON CLAUSEéquivalent de PgSQL vous oblige à fournir le nom de la contrainte, tandis que MySQL pourrait capturer n'importe quelle contrainte sans avoir besoin de la définir. Cela m'empêche «d'émuler» cette fonctionnalité sans réécrire les requêtes.
NeverEndingQueue

Réponses:


35

Essayez de faire une MISE À JOUR. S'il ne modifie aucune ligne, cela signifie qu'il n'existe pas, alors faites une insertion. Évidemment, vous faites cela dans une transaction.

Vous pouvez bien sûr envelopper cela dans une fonction si vous ne voulez pas mettre le code supplémentaire côté client. Vous avez également besoin d'une boucle pour la condition de course très rare dans cette réflexion.

Il y a un exemple de cela dans la documentation: http://www.postgresql.org/docs/9.3/static/plpgsql-control-structures.html , exemple 40-2 en bas.

C'est généralement le moyen le plus simple. Vous pouvez faire de la magie avec des règles, mais ce sera probablement beaucoup plus compliqué. Je recommanderais l'approche de la fonction enveloppante à tout moment.

Cela fonctionne pour les valeurs d'une seule ligne ou de quelques lignes. Si vous avez affaire à de grandes quantités de lignes, par exemple à partir d'une sous-requête, il est préférable de la diviser en deux requêtes, une pour INSERT et une pour UPDATE (comme une jointure / sous-sélection appropriée bien sûr - pas besoin d'écrire votre main filtrer deux fois)


4
«Si vous avez affaire à de grandes quantités de lignes», c'est exactement mon cas. Je veux mettre à jour / insérer des lignes en masse et avec mysql, je peux le faire avec une seule requête sans aucune boucle. Maintenant, je me demande si cela est également possible avec postgresql: utiliser une seule requête pour mettre à jour en bloc OU insérer. Vous dites: "il vaut mieux le diviser en deux requêtes, une pour INSERT et une pour UPDATE" mais comment puis-je faire une insertion qui ne génère pas d'erreurs sur les clés en double? (ie. "INSERT IGNORE")
gpilotino

4
Magnus signifiait que vous utilisiez une requête comme celle-ci: "démarrer la transaction; créer une table temporaire table_temporaire comme select * from test où false; copier la table_temporaire depuis 'data_file.csv'; verrouiller le test de la table; mettre à jour l'ensemble de test data = table_temporaire.data de la table_temporaire où test.id = table_temporaire.id; insérer dans le test sélectionnez * à partir de la table_temporaire où l'ID n'est pas dans (sélectionnez l'ID du test) comme un "
Tometzky

25
Mise à jour: avec PostgreSQL 9.5, c'est maintenant aussi simple que INSERT ... ON CONFLICT DO NOTHING;. Voir également la réponse stackoverflow.com/a/34639631/2091700 .
Alphaaa

Important, le standard SQL MERGEn'est pas un upsert sécurisé pour l'accès concurrentiel, sauf si vous prenez un LOCK TABLEpremier. Les gens l'utilisent de cette façon, mais c'est faux.
Craig Ringer

1
Avec la v9.5, c'est maintenant une fonctionnalité `` native '', donc s'il vous plaît vérifier le commentaire de @Alphaaa (il suffit d'annoncer le commentaire qui annonce la réponse)
Camilo Delvasto

179

Avec PostgreSQL 9.5, il s'agit désormais d' une fonctionnalité native (comme MySQL l'a depuis plusieurs années):

INSÉRER ... EN CAS DE CONFLIT NE RIEN / METTRE À JOUR ("UPSERT")

9.5 apporte un support pour les opérations "UPSERT". INSERT est étendu pour accepter une clause ON CONFLICT DO UPDATE / IGNORE. Cette clause spécifie une autre action à entreprendre en cas de violation potentielle de duplication.

...

Autre exemple de nouvelle syntaxe:

INSERT INTO user_logins (username, logins)
VALUES ('Naomi',1),('James',1) 
ON CONFLICT (username)
DO UPDATE SET logins = user_logins.logins + EXCLUDED.logins;

100

Edit: au cas où vous auriez manqué la réponse de warren , PG9.5 l'a maintenant nativement; il est temps de mettre à niveau!


S'appuyant sur la réponse de Bill Karwin, pour expliquer à quoi ressemblerait une approche basée sur des règles (transfert d'un autre schéma dans la même base de données et avec une clé primaire à plusieurs colonnes):

CREATE RULE "my_table_on_duplicate_ignore" AS ON INSERT TO "my_table"
  WHERE EXISTS(SELECT 1 FROM my_table 
                WHERE (pk_col_1, pk_col_2)=(NEW.pk_col_1, NEW.pk_col_2))
  DO INSTEAD NOTHING;
INSERT INTO my_table SELECT * FROM another_schema.my_table WHERE some_cond;
DROP RULE "my_table_on_duplicate_ignore" ON "my_table";

Remarque: la règle s'applique à toutes les INSERTopérations jusqu'à ce que la règle soit supprimée, donc pas tout à fait ad hoc.


@sema vous voulez dire si another_schema.my_tablecontient des doublons selon les contraintes de my_table?
EoghanM

2
@EoghanM J'ai testé la règle dans postgresql 9.3 et je pouvais toujours insérer des doublons avec des instructions d'insertion de plusieurs lignes comme par exemple INSERT INTO "my_table" (a, b), (a, b); (En supposant que la ligne (a, b) n'existait pas encore dans "ma_table".)
sema

@sema, gotcha - cela doit signifier que la règle est exécutée au début sur toutes les données à insérer, et non réexécutée après l'insertion de chaque ligne. Une approche serait d'insérer d'abord vos données dans une autre table temporaire qui n'a aucune contrainte, puis de faireINSERT INTO "my_table" SELECT DISTINCT ON (pk_col_1, pk_col_2) * FROM the_tmp_table;
EoghanM

@EoghanM Une autre approche consiste à assouplir temporairement les contraintes de duplication et à accepter les doublons lors de l'insertion, mais à supprimer les doublons par la suite avecDELETE FROM my_table WHERE ctid IN (SELECT ctid FROM (SELECT ctid,ROW_NUMBER() OVER (PARTITION BY pk_col_1,pk_col_2) AS rn FROM my_table) AS dups WHERE dups.rn > 1);
sema

J'ai le problème décrit par @sema. Si je fais un insert (a, b), (a, b), cela génère une erreur. Existe-t-il un moyen de supprimer les erreurs, également dans ce cas?
Diogo Melo

36

Pour ceux d'entre vous qui ont Postgres 9.5 ou supérieur, la nouvelle syntaxe ON CONFLICT DO NOTHING devrait fonctionner:

INSERT INTO target_table (field_one, field_two, field_three ) 
SELECT field_one, field_two, field_three
FROM source_table
ON CONFLICT (field_one) DO NOTHING;

Pour ceux d'entre nous qui ont une version antérieure, cette jointure droite fonctionnera à la place:

INSERT INTO target_table (field_one, field_two, field_three )
SELECT source_table.field_one, source_table.field_two, source_table.field_three
FROM source_table 
LEFT JOIN target_table ON source_table.field_one = target_table.field_one
WHERE target_table.field_one IS NULL;

La deuxième approche ne fonctionne pas lors de la création d'un gros insert dans un environnement concurrent. Vous obtenez Unique violation: 7 ERROR: duplicate key value violates unique constraintquand une target_tableautre ligne a été insérée pendant l' exécution de cette requête, si leurs clés se dupliquent effectivement. Je pense que le verrouillage target_tableaidera, mais la concurrence en souffrira évidemment.
G. Kashtanov

1
ON CONFLICT (field_one) DO NOTHINGest la meilleure partie de la réponse.
Abel Callejo

24

Pour obtenir la logique d' insertion ignorée , vous pouvez faire quelque chose comme ci-dessous. J'ai trouvé que l'insertion à partir d'une instruction de sélection de valeurs littérales fonctionnait mieux, puis vous pouvez masquer les clés en double avec une clause NOT EXISTS. Pour obtenir la mise à jour sur la logique dupliquée, je soupçonne qu'une boucle pl / pgsql serait nécessaire.

INSERT INTO manager.vin_manufacturer
(SELECT * FROM( VALUES
  ('935',' Citroën Brazil','Citroën'),
  ('ABC', 'Toyota', 'Toyota'),
  ('ZOM',' OM','OM')
  ) as tmp (vin_manufacturer_id, manufacturer_desc, make_desc)
  WHERE NOT EXISTS (
    --ignore anything that has already been inserted
    SELECT 1 FROM manager.vin_manufacturer m where m.vin_manufacturer_id = tmp.vin_manufacturer_id)
)

Que faire si tmp contient une ligne en double, ce qui peut arriver?
Henley Chiu

Vous pouvez toujours sélectionner avec le mot-clé distinct.
Keyo

5
Tout comme un FYI, l'astuce "WHERE NOT EXISTS" ne fonctionne pas sur plusieurs transactions car les différentes transactions ne peuvent pas voir les données nouvellement ajoutées des autres transactions.
Dave Johansen

21
INSERT INTO mytable(col1,col2) 
    SELECT 'val1','val2' 
    WHERE NOT EXISTS (SELECT 1 FROM mytable WHERE col1='val1')

Quel est l'impact de plusieurs transactions essayant toutes de faire la même chose? Est-il possible qu'entre l'exécution où n'existe pas et l'insertion exécutant une autre transaction insère une ligne? Et si Postgres peut empêcher cela, alors postgres n'introduit-il pas un point de synchronisation entre toutes ces transactions lorsqu'elles atteignent cela?
Καrτhικ

Cela ne fonctionne pas avec plusieurs transactions, car les données nouvellement ajoutées ne sont pas visibles pour les autres transactions.
Dave Johansen

12

On dirait que PostgreSQL prend en charge un objet de schéma appelé règle .

http://www.postgresql.org/docs/current/static/rules-update.html

Vous pouvez créer une règle ON INSERTpour une table donnée, en la faisant faire NOTHINGsi une ligne existe avec la valeur de clé primaire donnée, ou bien en la faisant faire une UPDATEau lieu de INSERTsi une ligne existe avec la valeur de clé primaire donnée.

Je n'ai pas essayé cela moi-même, donc je ne peux pas parler d'expérience ou donner un exemple.


1
si j'ai bien compris, ces règles sont des déclencheurs qui sont exécutés chaque fois qu'une instruction est appelée. et si je veux appliquer la règle pour une seule requête? Je dois créer la règle puis la détruire immédiatement? (qu'en est-il des conditions de course?)
gpilotino

3
Oui, j'aurais aussi les mêmes questions. Le mécanisme de règle est la chose la plus proche que j'ai pu trouver dans PostgreSQL à INSERT IGNORE ou ON DUPLICATE KEY UPDATE de MySQL. Si nous recherchons sur Google "postgresql sur la mise à jour de la clé en double", vous trouvez d'autres personnes recommandant le mécanisme de règle, même si une règle s'appliquerait à n'importe quel INSERT, pas seulement de manière ad hoc.
Bill Karwin

4
PostgreSQL prend en charge le DDL transactionnel, ce qui signifie que si vous créez une règle et la supprimez dans une seule transaction, la règle n'aura jamais été visible en dehors de (et n'aura donc jamais eu d'effet en dehors de) cette transaction.
cdhowie

6

Comme @hanmari l'a mentionné dans son commentaire. lors de l'insertion dans une table postgres, on conflict (..) do Nothing est le meilleur code à utiliser pour ne pas insérer de données en double:

query = "INSERT INTO db_table_name(column_name)
         VALUES(%s) ON CONFLICT (column_name) DO NOTHING;"

La ligne de code ON CONFLICT permettra à l'instruction d'insertion d'insérer toujours des lignes de données. Le code de requête et de valeurs est un exemple de date insérée depuis un Excel dans une table de base de données postgres. J'ai des contraintes ajoutées à une table postgres que j'utilise pour m'assurer que le champ ID est unique. Au lieu d'exécuter une suppression sur des lignes de données identiques, j'ajoute une ligne de code sql qui renumérote la colonne ID à partir de 1. Exemple:

q = 'ALTER id_column serial RESTART WITH 1'

Si mes données ont un champ ID, je ne l'utilise pas comme ID principal / ID de série, je crée une colonne ID et je la mets en série. J'espère que ces informations seront utiles à tout le monde. * Je n'ai pas de diplôme universitaire en développement / codage de logiciels. Tout ce que je sais en codage, je l'étudie seul.


cela ne fonctionne pas sur les indices uniques composites!
Nulik le

4

Cette solution évite d'utiliser des règles:

BEGIN
   INSERT INTO tableA (unique_column,c2,c3) VALUES (1,2,3);
EXCEPTION 
   WHEN unique_violation THEN
     UPDATE tableA SET c2 = 2, c3 = 3 WHERE unique_column = 1;
END;

mais il a un inconvénient en termes de performances (voir PostgreSQL.org ):

Un bloc contenant une clause EXCEPTION est beaucoup plus coûteux à entrer et à sortir qu'un bloc sans clause. Par conséquent, n'utilisez pas EXCEPTION sans besoin.


1

En bloc, vous pouvez toujours supprimer la ligne avant l'insertion. La suppression d'une ligne qui n'existe pas ne provoque pas d'erreur, elle est donc ignorée en toute sécurité.


2
Cette approche sera assez sujette à des conditions de course étranges, je ne la recommanderais pas ...
Steven Schlansker

1
+1 C'est simple et générique. Si utilisé avec précaution, cela peut en fait être une solution simple.
Wouter van Nifterick

1
Cela ne fonctionnera pas non plus lorsque les données existantes ont été modifiées après l'insertion (mais pas sur la clé en double) et que nous souhaitons conserver les mises à jour. C'est le scénario lorsque des scripts SQL sont écrits pour un certain nombre de systèmes légèrement différents, comme les mises à jour de base de données qui s'exécutent sur des systèmes de production, d'assurance qualité, de développement et de test.
Hanno Fietz

1
Les clés étrangères peuvent ne poser aucun problème si vous les créez avec des DEFERRABLE INITIALLY DEFERREDindicateurs.
temoto

-1

Pour les scripts d'importation de données, pour remplacer "IF NOT EXISTS", d'une certaine manière, il existe une formulation un peu maladroite qui fonctionne néanmoins:

DO
$do$
BEGIN
PERFORM id
FROM whatever_table;

IF NOT FOUND THEN
-- INSERT stuff
END IF;
END
$do$;
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.