Postgres: INSÉRER si n'existe pas déjà


362

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

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 premièrement, est-ce exagéré pour ce dont j'ai besoin, et deuxièmement, comment puis-je exécuter l'un d'eux comme une simple chaîne?


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

3
Pourquoi ne pas attraper l'exception et l'ignorer?
Matthew Mitchell

5
Depuis Posgres 9.5 (actuellement sur beta2), il existe une nouvelle fonctionnalité similaire à upsert, voir: postgresql.org/docs/9.5/static/sql-insert.html#SQL-ON-CONFLICT
Ezequiel Moreno

2
Avez-vous envisagé d'accepter une réponse à cela? =]
Relequestual

Réponses:


514

Postgres 9.5 (publié depuis le 2016-01-07) propose une commande "upsert" , également connue sous le nom de clause ON CONFLICT à INSERT :

INSERT ... ON CONFLICT DO NOTHING/UPDATE

Il résout de nombreux problèmes subtils que vous pouvez rencontrer lors de l'utilisation d'une opération simultanée, ce que d'autres réponses proposent.


14
9.5 est sorti.
luckydonald

2
@TusharJain avant PostgreSQL 9.5, vous pouvez faire un UPSERT "à l'ancienne" (avec CTE) mais vous pouvez rencontrer des problèmes avec les conditions de course et il ne sera pas performant en tant que style 9.5. Il y a un bon détail sur upsert sur ce blog (dans la zone mise à jour en bas), y compris quelques liens si vous souhaitez en savoir plus sur les détails.
Skyguard

17
Pour ceux qui en ont besoin, voici deux exemples simples. (1) INSÉRER s'il n'existe pas d'autre RIEN - INSERT INTO distributors (did, dname) VALUES (7, 'Redline GmbH') ON CONFLICT (did) DO NOTHING;(2) INSÉRER s'il n'existe pas d'autre MISE À JOUR - INSERT INTO distributors (did, dname) VALUES (5, 'Gizmo Transglobal'), (6, 'Associated Computing, Inc') ON CONFLICT (did) DO UPDATE SET dname = EXCLUDED.dname;Ces exemples sont tirés du manuel - postgresql.org/docs/9.5/static/sql-insert.html
AnnieFromTaiwan

13
Il y a une mise en garde / effet secondaire. Dans une table avec une colonne de séquence (série ou grande série), même si aucune ligne n'est insérée, la séquence est incrémentée à chaque tentative d'insertion.
Grzegorz Luczywo

2
Il serait préférable de créer un lien vers la documentation INSERT au lieu de pointer vers la version. Lien vers le doc: postgresql.org/docs/9.5/static/sql-insert.html
borjagvo

379

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

Il y a une bonne façon de faire INSERT conditionnel 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 cependant pas fiable à 100% pour les opérations d'écriture simultanées . Il y a une très petite condition de course entre SELECTl' NOT EXISTSanti-semi-jointure et le INSERTlui - même. Il peut échouer dans de telles conditions.


Dans quelle mesure cela suppose-t-il que le champ "nom" a une contrainte UNIQUE? Va-t-il jamais échouer avec une violation unique?
agnsaft

2
Cela fonctionne bien. Le seul problème est le couplage je suppose: que se passe-t-il 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 une façon plus générique de le faire ...
Willem Van Onsem

1
Est-il possible de l'utiliser avec RETURNS idpar exemple pour idsavoir si est inséré ou non?
Olivier Pons

2
@OlivierPons oui, c'est possible. Ajoutez RETURNING idau et de la requête et il renverra soit un nouvel identifiant de ligne, soit rien, si aucune ligne n'a été insérée.
AlexM

4
J'ai trouvé que ce n'était pas fiable. Il semble que Postgres exécute parfois l'insertion avant d'avoir exécuté la sélection et je me retrouve avec une violation de clé en double même si l'enregistrement n'a pas encore été inséré. Essayez d'utiliser la version => 9.5 avec ON CONFLICT.
Michael Silver

51

Une approche serait de créer une table non contrainte (pas d'index unique) pour insérer toutes vos données dans et faire une sélection distincte de celle pour faire votre insertion dans votre table de cent.

Si 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 des cent.

  1. Créez une table temporaire. Voir les documents ici .

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

    INSERT INTO temp_data(name, name_slug, status); 
  3. Ajoutez des 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
C'est le moyen le plus rapide que j'ai trouvé pour effectuer des insertions de masse lorsque je ne sais pas si la ligne existe déjà.
nate c

sélectionnez 'X'? quelqu'un peut-il clarifier? Ceci est tout simplement une déclaration à droite: SELECT name,name_slug,statusou*
roberthuttinger

3
Rechercher une sous-requête corrélée. «X» pourrait être changé en 1 ou même «SadClown». SQL nécessite qu'il y ait quelque chose et «X» est une chose courante à utiliser. Il est petit et il rend évident qu'une sous-requête corrélée est utilisée et répond aux exigences de ce que SQL requiert.
Kuberchaun

Vous avez mentionné "insérer toutes vos données dans (en supposant une table temporaire) et faire une sélection distincte de cela". Dans ce cas, ne devrait-il pas en être ainsi SELECT DISTINCT name, name_slug, status FROM temp_data?
gibbz00

17

Malheureusement, PostgreSQLne prend en charge ni MERGEni 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'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 il suffit de l'appeler:

SELECT  fn_upd_invoices('12345', 'TRUE')

1
En fait, cela ne fonctionne pas: je peux appeler INSERT INTO hundred (name, name_slug, status) SELECT 'Chichester', 'chichester', NULL WHERE 'Chichester' NOT IN (SELECT NAME FROM hundred);n'importe quel nombre de fois et cela continue d'insérer la ligne.
AP257

1
@ 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.
Quassnoi

12

Vous pouvez utiliser les VALEURS - disponibles dans Postgres:

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

12
CHOISIR le nom DE LA PERSONNE <--- et s'il y a un milliard de lignes en personne?
Henley Chiu

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

WOW, c'est exactement ce dont j'avais besoin. J'avais peur d'avoir besoin de créer une fonction ou une table temporaire, mais cela exclut tout cela - merci!
Amalgovinus

8

Je sais que cette question remonte à un certain temps, mais j'ai pensé que cela pourrait aider quelqu'un. Je pense que la façon la plus simple de le faire est via 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 voulez 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 déjà mentionné, le code ci-dessus tire parti des paramètres plutôt que de concaténer la chaîne.


Si quelqu'un d'autre se posait également la question, d'après les documents : "Les déclencheurs de niveau ligne déclenchés AVANT peuvent renvoyer la valeur null pour signaler au gestionnaire de déclencheurs d'ignorer le reste de l'opération pour cette ligne (c'est-à-dire que les déclencheurs suivants ne sont pas déclenchés, et le INSERT / UPDATE / DELETE ne se produit pas pour cette ligne. Si une valeur non nulle est renvoyée, l'opération se poursuit avec cette valeur de ligne. "
Pete

Exactement cette réponse que je cherchais. Nettoyez le code, en utilisant la fonction + déclencheur au lieu de l'instruction select. +1
Jacek Krawczyk

J'adore cette réponse, j'utilise la fonction et le déclencheur. Maintenant, je trouve une autre façon de sortir de l'impasse en utilisant des fonctions et des déclencheurs ...
Sukma Saputra

7

Il existe une bonne façon de faire INSERT conditionnel dans PostgreSQL en utilisant la requête WITH:

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 

7

C'est exactement le problème auquel je suis confronté et ma version est la 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.


5

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

BEGIN;
LOCK TABLE hundred IN SHARE ROW EXCLUSIVE MODE;
INSERT ... ;
COMMIT;

2

C'est simple 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 il échoue avec les écritures simultanées ...


1

L'approche avec le plus de votes positifs (de John Doe) fonctionne en quelque sorte pour moi, mais dans mon cas, sur les 422 lignes attendues, je n'en reçois que 180. Je n'ai rien trouvé de mal et il n'y a aucune erreur, alors j'ai cherché un autre approche simple.

L'utilisation IF NOT FOUND THENaprès un SELECTfonctionne parfaitement pour moi.

(décrit dans la 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

La classe de curseur psycopgs a l'attribut rowcount .

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

Vous pouvez donc d'abord essayer UPDATE et INSERT uniquement si le nombre de lignes est 0.

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


Vraisemblablement, le fait de regrouper ces requêtes dans une transaction atténuerait la condition de concurrence.
Daniel Lyons

Merci, solution vraiment simple et propre
Alexander Malfait

1

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, c'est avec vos données.

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


1

Si vous dites que plusieurs de vos lignes sont identiques, vous terminerez la vérification plusieurs fois. Vous pouvez les envoyer et la base de données déterminera si l'insérer 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);

0

Je cherchais une solution similaire, en essayant de trouver du SQL qui fonctionne aussi bien dans PostgreSQL que HSQLDB. (HSQLDB a rendu cela difficile.) En utilisant votre exemple comme base, c'est 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 );"

-1

Voici une fonction python générique qui, étant donné un nom de table, des colonnes et des valeurs, génère l'équivalent upsert pour postgresql.

importer json

def upsert(table_name, id_column, other_columns, values_hash):

    template = """
    WITH new_values ($$ALL_COLUMNS$$) as (
      values
         ($$VALUES_LIST$$)
    ),
    upsert as
    (
        update $$TABLE_NAME$$ m
            set
                $$SET_MAPPINGS$$
        FROM new_values nv
        WHERE m.$$ID_COLUMN$$ = nv.$$ID_COLUMN$$
        RETURNING m.*
    )
    INSERT INTO $$TABLE_NAME$$ ($$ALL_COLUMNS$$)
    SELECT $$ALL_COLUMNS$$
    FROM new_values
    WHERE NOT EXISTS (SELECT 1
                      FROM upsert up
                      WHERE up.$$ID_COLUMN$$ = new_values.$$ID_COLUMN$$)
    """

    all_columns = [id_column] + other_columns
    all_columns_csv = ",".join(all_columns)
    all_values_csv = ','.join([query_value(values_hash[column_name]) for column_name in all_columns])
    set_mappings = ",".join([ c+ " = nv." +c for c in other_columns])

    q = template
    q = q.replace("$$TABLE_NAME$$", table_name)
    q = q.replace("$$ID_COLUMN$$", id_column)
    q = q.replace("$$ALL_COLUMNS$$", all_columns_csv)
    q = q.replace("$$VALUES_LIST$$", all_values_csv)
    q = q.replace("$$SET_MAPPINGS$$", set_mappings)

    return q


def query_value(value):
    if value is None:
        return "NULL"
    if type(value) in [str, unicode]:
        return "'%s'" % value.replace("'", "''")
    if type(value) == dict:
        return "'%s'" % json.dumps(value).replace("'", "''")
    if type(value) == bool:
        return "%s" % value
    if type(value) == int:
        return "%s" % value
    return value


if __name__ == "__main__":

    my_table_name = 'mytable'
    my_id_column = 'id'
    my_other_columns = ['field1', 'field2']
    my_values_hash = {
        'id': 123,
        'field1': "john",
        'field2': "doe"
    }
    print upsert(my_table_name, my_id_column, my_other_columns, my_values_hash)

-8

La solution en simple, mais pas immédiatement.
Si vous souhaitez utiliser cette instruction, vous devez apporter une modification à la base de données:

ALTER USER user SET search_path to 'name_of_schema';

après ces modifications, "INSERT" fonctionnera correctement.

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.