629 votes

Postgres : INSERT si n'existe pas déjà

J'utilise Python pour écrire dans une base de données Postgres :

sql_string = "INSERT INTO hundred (name,name_slug,status) VALUES ("
sql_string += hundred + ", '" + hundred_slug + "', " + status + ");"
cursor.execute(sql_string)

Mais comme certaines de mes lignes sont identiques, j'obtiens l'erreur suivante :

psycopg2.IntegrityError: duplicate key value  
  violates unique constraint "hundred_pkey"

Comment puis-je écrire une instruction SQL "INSERT sauf si cette ligne existe déjà" ?

J'ai vu des déclarations complexes comme celle-ci recommandées :

IF EXISTS (SELECT * FROM invoices WHERE invoiceid = '12345')
UPDATE invoices SET billed = 'TRUE' WHERE invoiceid = '12345'
ELSE
INSERT INTO invoices (invoiceid, billed) VALUES ('12345', 'TRUE')
END IF

Mais d'abord, est-ce que c'est trop pour ce dont j'ai besoin, et ensuite, comment puis-je exécuter l'une d'entre elles comme une simple chaîne de caractères ?

79 votes

Quelle que soit la façon dont vous résolvez ce problème, vous ne devriez pas générer votre requête de cette façon. Utilisez des paramètres dans votre requête et passez les valeurs séparément ; voir stackoverflow.com/questions/902408/

3 votes

Pourquoi ne pas attraper l'exception et l'ignorer ?

8 votes

Depuis la version 9.5 de Posgres (actuellement en version bêta 2), il existe une nouvelle fonctionnalité de type upsert : postgresql.org/docs/9.5/static/sql-insert.html#SQL-ON-CONFLICT

467voto

John Doe Points 2035

Comment puis-je écrire une instruction SQL "INSERT sauf si cette ligne existe déjà" ?

Il existe une bonne façon d'effectuer des INSERTS conditionnels dans PostgreSQL :

INSERT INTO example_table
    (id, name)
SELECT 1, 'John'
WHERE
    NOT EXISTS (
        SELECT id FROM example_table WHERE id = 1
    );

CAVEAT Cette approche n'est pas fiable à 100% pour concurrentes les opérations d'écriture, cependant. Il existe une très petite condition de course entre le SELECT dans le NOT EXISTS anti-semi-join et le INSERT lui-même. Il s'agit de puede échouer dans de telles conditions.

0 votes

Dans quelle mesure cela est-il sûr, en supposant que le champ "nom" est soumis à une contrainte UNIQUE ? Y aura-t-il un échec avec une violation unique ?

0 votes

@invictus Si un autre nouvel identifiant correspondait au nom, qui est déjà dans la base de données, je pense que cela devrait échouer, ce serait un comportement évident.

2 votes

Cela fonctionne bien. Le seul problème est le couplage je suppose : que faire si on modifie la table de telle sorte que plus de colonnes soient uniques. Dans ce cas, tous les scripts doivent être modifiés. Ce serait bien s'il y avait un moyen plus générique de faire cela...

61voto

Bob Points 9217

Une approche consisterait à créer une table sans contrainte (pas d'index unique) pour y insérer toutes vos données et à effectuer une sélection distincte de cette table pour insérer vos données dans votre table cent.

Donc le haut niveau serait. Je suppose que les trois colonnes sont distinctes dans mon exemple, donc pour l'étape 3, changez la jointure NOT EXITS pour ne joindre que les colonnes uniques de la table cent.

  1. Créer une table temporaire. Voir la documentation aquí .

    CREATE TEMPORARY TABLE temp_data(name, name_slug, status);
  2. INSERER les données dans la table temporaire.

    INSERT INTO temp_data(name, name_slug, status); 
  3. Ajoutez tous les index à la table temporaire.

  4. Faire l'insertion de la table principale.

    INSERT INTO hundred(name, name_slug, status) 
        SELECT DISTINCT name, name_slug, status
        FROM hundred
        WHERE NOT EXISTS (
            SELECT 'X' 
            FROM temp_data
            WHERE 
                temp_data.name          = hundred.name
                AND temp_data.name_slug = hundred.name_slug
                AND temp_data.status    = status
        );

3 votes

C'est le moyen le plus rapide que j'ai trouvé pour effectuer des insertions en masse lorsque je ne sais pas si la ligne existe déjà.

1 votes

Sélectionner 'X' ? Quelqu'un peut-il préciser ? Il s'agit simplement d'une déclaration de sélection : SELECT name,name_slug,status o *

5 votes

Recherche d'une sous-requête corrélée. Le 'X' pourrait être remplacé par un 1 ou même par 'SadClown'. Le SQL exige qu'il y ait quelque chose et 'X' est une chose courante à utiliser. C'est petit et cela rend évident l'utilisation d'une sous-requête corrélée et répond aux exigences de SQL.

19voto

Quassnoi Points 191041

Malheureusement, PostgreSQL ne supporte ni MERGE ni ON DUPLICATE KEY UPDATE Vous devrez donc le faire en deux déclarations :

UPDATE  invoices
SET     billed = 'TRUE'
WHERE   invoices = '12345'

INSERT
INTO    invoices (invoiceid, billed)
SELECT  '12345', 'TRUE'
WHERE   '12345' NOT IN
        (
        SELECT  invoiceid
        FROM    invoices
        )

Vous pouvez l'intégrer dans une fonction :

CREATE OR REPLACE FUNCTION fn_upd_invoices(id VARCHAR(32), billed VARCHAR(32))
RETURNS VOID
AS
$$
        UPDATE  invoices
        SET     billed = $2
        WHERE   invoices = $1;

        INSERT
        INTO    invoices (invoiceid, billed)
        SELECT  $1, $2
        WHERE   $1 NOT IN
                (
                SELECT  invoiceid
                FROM    invoices
                );
$$
LANGUAGE 'sql';

et l'appeler simplement :

SELECT  fn_upd_invoices('12345', 'TRUE')

1 votes

En fait, ça ne marche pas : Je peux appeler INSERT INTO hundred (name, name_slug, status) SELECT 'Chichester', 'chichester', NULL WHERE 'Chichester' NOT IN (SELECT NAME FROM hundred); un nombre quelconque de fois, et il continue à insérer la ligne.

1 votes

@AP257 : CREATE TABLE hundred (name TEXT, name_slug TEXT, status INT); INSERT INTO hundred (name, name_slug, status) SELECT 'Chichester', 'chichester', NULL WHERE 'Chichester' NOT IN (SELECT NAME FROM hundred); INSERT INTO hundred (name, name_slug, status) SELECT 'Chichester', 'chichester', NULL WHERE 'Chichester' NOT IN (SELECT NAME FROM hundred); SELECT * FROM hundred . Il y a un enregistrement.

0 votes

Postgres fait soutien ON DUPLICATE KEY UPDATE . Il s'appelle ON CONFLICT (column) DO UPDATE SET

15voto

crististm Points 66

Vous pouvez faire usage de VALUES - disponible dans Postgres :

INSERT INTO person (name)
    SELECT name FROM person
    UNION 
    VALUES ('Bob')
    EXCEPT
    SELECT name FROM person;

16 votes

SELECT name FROM Person <--- Et s'il y a un milliard de lignes dans person ?

2 votes

Je pense que c'est un moyen rapide de résoudre le problème, mais seulement si vous êtes sûr que la table source ne deviendra jamais énorme. J'ai une table qui n'aura jamais plus de 1000 lignes, je peux donc utiliser cette solution.

1 votes

WOW, c'est exactement ce dont j'avais besoin. Je craignais de devoir créer une fonction ou une table temporaire, mais cela évite tout cela - merci !

12voto

khms Points 99

Si vous souhaitez simplement insérer ou ne pas insérer (et ne pas mettre à jour par ailleurs), vous pouvez procéder comme suit (en reprenant l'exemple de la facture) :

INSERT INTO invoices (invoiceid, billed) SELECT '12345', 'TRUE'
WHERE NOT EXISTS (SELECT 1 FROM invoices WHERE invoiceid = '12345')

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X