web-dev-qa-db-fra.com

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 parce que 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’une part, c’est excessif pour ce dont j’ai besoin, et d’autre part, comment puis-je exécuter l’une d’elles comme une simple chaîne?

279
AP257

Postgres 9.5 (publié depuis le 07/01/2016) propose une commande "upsert" , également appelée clause ON CONFLICT à INSERT :

INSERT ... ON CONFLICT DO NOTHING/UPDATE

Il résout bon nombre des problèmes subtils que vous pouvez rencontrer lorsque vous utilisez une opération simultanée, ce que proposent d’autres réponses.

363
Arie

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

Il existe un bon moyen de faire des INSERT conditionnels dans PostgreSQL:

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

MISE EN GARDE Cette approche n'est toutefois pas fiable à 100% pour les opérations d'écriture concurrentes . Il existe une très petite condition de concurrence entre le SELECT du NOT EXISTS anti-semi-join et le INSERT lui-même. Il peut échouer dans de telles conditions.

349
John Doe

Une approche consisterait à créer une table non contrainte (aucun index unique) dans laquelle insérer toutes vos données et à effectuer une sélection distincte de celle nécessaire pour effectuer votre insertion dans votre table cent.

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

  1. Créer une table temporaire. Voir docs ici .

    CREATE TEMPORARY TABLE temp_data(name, name_slug, status);
    
  2. INSERT Data 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
        );
    
45
Kuberchaun

Malheureusement, PostgreSQL ne prend en charge ni MERGE ni ON DUPLICATE KEY UPDATE, vous devrez donc le faire en deux étapes:

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'envelopper 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 juste l'appeler:

SELECT  fn_upd_invoices('12345', 'TRUE')
16
Quassnoi

Vous pouvez utiliser VALUES - disponible dans Postgres:

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

Je sais que cette question remonte à un moment donné, mais je pensais que cela pourrait aider quelqu'un. Je pense que le moyen le plus simple de procéder consiste à utiliser un déclencheur. Par exemple.:

Create Function ignore_dups() Returns Trigger
As $$
Begin
    If Exists (
        Select
            *
        From
            hundred h
        Where
            -- Assuming all three fields are primary key
            h.name = NEW.name
            And h.hundred_slug = NEW.hundred_slug
            And h.status = NEW.status
    ) Then
        Return NULL;
    End If;
    Return NEW;
End;
$$ Language plpgsql;

Create Trigger ignore_dups
    Before Insert On hundred
    For Each Row
    Execute Procedure ignore_dups();

Exécutez ce code à partir d'une invite psql (ou comme vous souhaitez exécuter des requêtes directement sur la base de données). Ensuite, vous pouvez insérer comme d'habitude à partir de Python. Par exemple.:

sql = "Insert Into hundreds (name, name_slug, status) Values (%s, %s, %s)"
cursor.execute(sql, (hundred, hundred_slug, status))

Notez que, comme @Thomas_Wouters l'a déjà mentionné, le code ci-dessus tire parti des paramètres plutôt que de concaténer la chaîne.

7
ktr

Il existe un bon moyen de faire un INSERT conditionnel dans PostgreSQL en utilisant la requête WITH: Like:

WITH a as(
select 
 id 
from 
 schema.table_name 
where 
 column_name = your_identical_column_value
)
INSERT into 
 schema.table_name
(col_name1, col_name2)
SELECT
    (col_name1, col_name2)
WHERE NOT EXISTS (
     SELECT
         id
     FROM
         a
        )
  RETURNING id 
6
Ritesh Jha

INSÉRER .. LORSQUE N'EXISTE PAS est une bonne approche. Et les conditions de concurrence peuvent être évitées par transaction "enveloppe":

BEGIN;
LOCK TABLE hundred IN SHARE ROW EXCLUSIVE MODE;
INSERT ... ;
COMMIT;
5
Pavel Francírek

C'est facile avec les règles:

CREATE RULE file_insert_defer AS ON INSERT TO file
WHERE (EXISTS ( SELECT * FROM file WHERE file.id = new.id)) DO INSTEAD NOTHING

Mais cela échoue avec des écritures concurrentes ...

2
user6578371

Si vous dites que plusieurs de vos lignes sont identiques, vous finirez par vérifier plusieurs fois. Vous pouvez les envoyer et la base de données déterminera si vous l'insérez ou non avec la clause ON CONFLICT comme suit

  INSERT INTO Hundred (name,name_slug,status) VALUES ("sql_string += hundred  
  +",'" + hundred_slug + "', " + status + ") ON CONFLICT ON CONSTRAINT
  hundred_pkey DO NOTHING;" cursor.execute(sql_string);
1
opena

la classe de curseur psycopgs a l'attribut nombre de lignes .

Cet attribut en lecture seule spécifie le nombre de lignes que la dernière execute * () a produites (pour les instructions DQL telles que SELECT) ou affectées (pour les instructions DML telles que UPDATE ou INSERT).

Vous pouvez donc essayer d'abord UPDATE et INSERT uniquement si rowcount est à 0.

Mais selon les niveaux d'activité de votre base de données, vous pouvez rencontrer une situation critique entre UPDATE et INSERT, un autre processus pouvant créer cet enregistrement dans l'intervalle.

1
johnbaum

Votre colonne "cent" semble être définie comme clé primaire et doit donc être unique, ce qui n'est pas le cas. Le problème n'est pas avec, mais avec vos données.

Je vous suggère d'insérer un identifiant de type série pour transférer manuellement la clé primaire

1
Boodoo

L’approche avec la plupart des votes positifs (de John Doe) fonctionne d’une manière ou d’une autre pour moi, mais dans mon cas, sur 422 lignes attendues, je n’en ai que 180. Je n’ai rien trouvé de mal et il n’ya aucune erreur, alors j’ai cherché une solution différente. approche simple.

Utiliser IF NOT FOUND THEN après une SELECT fonctionne parfaitement pour moi.

(décrit dans Documentation PostgreSQL) )

Exemple de documentation:

SELECT * INTO myrec FROM emp WHERE empname = myname;
IF NOT FOUND THEN
  RAISE EXCEPTION 'employee % not found', myname;
END IF;
1
vchrizz

Je recherchais une solution similaire, en essayant de trouver du SQL fonctionnant à la fois dans PostgreSQL et HSQLDB. (HSQLDB était ce qui rendait cela difficile.) En vous basant sur votre exemple, voici le format que j'ai trouvé ailleurs.

sql = "INSERT INTO hundred (name,name_slug,status)"
sql += " ( SELECT " + hundred + ", '" + hundred_slug + "', " + status
sql += " FROM hundred"
sql += " WHERE name = " + hundred + " AND name_slug = '" + hundred_slug + "' AND status = " + status
sql += " HAVING COUNT(*) = 0 );"
0
Jeff Fairley

C'est exactement le problème que je rencontre et ma version est 9.5

Et je le résous avec la requête SQL ci-dessous.

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

J'espère que cela aidera quelqu'un qui a le même problème avec la version> = 9.5.

Merci d'avoir lu.

0
NgocLocCoc