Postgres: INSERISCI se non esiste già


361

Sto usando Python per scrivere su un database postgres:

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

Ma poiché alcune delle mie righe sono identiche, ottengo il seguente errore:

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

Come posso scrivere un'istruzione SQL "INSERT a meno che questa riga non esista già"?

Ho visto dichiarazioni complesse come questa raccomandate:

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

Ma in primo luogo, questo è eccessivo per quello che mi serve, e in secondo luogo, come posso eseguire uno di quelli come una semplice stringa?


56
Indipendentemente da come risolvi questo problema, non dovresti generare la tua query in questo modo. Usa i parametri nella tua query e passa i valori separatamente; vedi stackoverflow.com/questions/902408/…
Thomas Wouters,

3
Perché non catturare l'eccezione e ignorarla?
Matthew Mitchell,

5
A partire da Posgres 9.5 (attualmente su beta2) c'è una nuova funzionalità simile a upsert, vedi: postgresql.org/docs/9.5/static/sql-insert.html#SQL-ON-CONFLICT
Ezequiel Moreno

2
Hai considerato di accettare una risposta per questo? =]
Relequestual

Risposte:


513

Postgres 9.5 (rilasciato dal 07-01-2016) offre un comando "upsert" , noto anche come clausola ON CONFLICT per INSERT :

INSERT ... ON CONFLICT DO NOTHING/UPDATE

Risolve molti dei problemi sottili che si possono incontrare quando si utilizza un'operazione simultanea, che alcune altre risposte propongono.


14
9.5 è stato rilasciato.
luckydonald,

2
@TusharJain prima di PostgreSQL 9.5 puoi eseguire un UPSERT "vecchio stile" (con CTE) ma potresti riscontrare problemi con le condizioni di gara e non funzionerà come in stile 9.5. C'è un buon dettaglio su upsert su questo blog (nell'area aggiornata in fondo) inclusi alcuni link se vuoi leggere di più sui dettagli.
Skyguard

17
Per quelli necessari, ecco due semplici esempi. (1) INSERISCI se non esiste altro NULLA - INSERT INTO distributors (did, dname) VALUES (7, 'Redline GmbH') ON CONFLICT (did) DO NOTHING;(2) INSERISCI se non esiste altro AGGIORNAMENTO - INSERT INTO distributors (did, dname) VALUES (5, 'Gizmo Transglobal'), (6, 'Associated Computing, Inc') ON CONFLICT (did) DO UPDATE SET dname = EXCLUDED.dname;Questi esempi sono tratti dal manuale - postgresql.org/docs/9.5/static/sql-insert.html
AnnieFromTaiwan

13
C'è un avvertimento / effetto collaterale. In una tabella con colonna di sequenza (seriale o bigserial), anche se nessuna riga è inserita, la sequenza viene incrementata ad ogni tentativo di inserimento.
Grzegorz Luczywo,

2
Sarebbe meglio collegarsi alla documentazione INSERT invece di indicare il rilascio. Doc link: postgresql.org/docs/9.5/static/sql-insert.html
borjagvo,

379

Come posso scrivere un'istruzione SQL "INSERT a meno che questa riga non esista già"?

C'è un buon modo di fare INSERT condizionale in PostgreSQL:

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

CAVEAT Tuttavia, questo approccio non è affidabile al 100% per le operazioni di scrittura simultanea . C'è una minuscola condizione di competizione tra SELECTl' NOT EXISTSanti-semi-join e lo INSERTstesso. Si può non riuscire in tali condizioni.


Quanto è sicuro supporre che il campo "name" abbia un vincolo UNIQUE? Fallirà mai con una violazione unica?
agnsaft,

2
Funziona benissimo. L'unico problema è l'accoppiamento credo: cosa succede se si modifica la tabella in modo che più colonne siano uniche. In tal caso, tutti gli script devono essere modificati. Sarebbe bello se ci fosse un modo più generico per farlo ...
Willem Van Onsem

1
È possibile usarlo con RETURNS idad esempio per ottenere idse è stato inserito o no?
Olivier Pons,

2
@OlivierPons sì, è possibile. Aggiungi RETURNING ida e della query e restituirà un nuovo ID riga o nulla, se non è stata inserita alcuna riga.
AlexM,

4
Ho trovato questo inaffidabile. Sembra che Postgres a volte esegua l'inserimento prima che abbia eseguito la selezione e finisco con una violazione della chiave duplicata anche se il record non è stato ancora inserito. Prova a utilizzare version => 9.5 con ON CONFLICT.
Michael Silver,

51

Un approccio sarebbe quello di creare una tabella non vincolata (senza indici univoci) in cui inserire tutti i dati e fare una selezione distinta da quella per eseguire l'inserimento nella tua centinaia di tabelle.

Quindi sarebbe di alto livello. Presumo che tutte e tre le colonne siano distinte nel mio esempio, quindi per il passaggio 3 modificare il join NOT EXITS per unire solo le colonne univoche nella tabella cento.

  1. Crea una tabella temporanea. Vedi i documenti qui .

    CREATE TEMPORARY TABLE temp_data(name, name_slug, status);
  2. INSERIRE i dati nella tabella temporanea.

    INSERT INTO temp_data(name, name_slug, status); 
  3. Aggiungi eventuali indici alla tabella temporanea.

  4. Esegui l'inserimento della tabella 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
Questo è il modo più veloce che ho trovato per eseguire inserimenti di massa quando non so se la riga esiste già.
c nate

seleziona 'X'? qualcuno può chiarire? Questa è semplicemente una frase selezionata a destra: SELECT name,name_slug,statusoppure*
roberthuttinger,

3
Cerca sottoquery correlata. 'X' potrebbe essere cambiato in 1 o anche 'SadClown'. SQL richiede che ci sia qualcosa e 'X' è una cosa comune da usare. È piccolo e rende evidente che viene utilizzata una sottoquery correlata e soddisfa i requisiti di ciò che richiede SQL.
Kuberchaun,

Hai menzionato "inserisci tutti i tuoi dati (assumendo la tabella temporanea) e fai una selezione distinta da quella". In tal caso, non dovrebbe essere SELECT DISTINCT name, name_slug, status FROM temp_data?
gibbz00,

17

Sfortunatamente, PostgreSQLnon supporta né MERGEON DUPLICATE KEY UPDATE, quindi dovrai farlo in due affermazioni:

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

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

Puoi avvolgerlo in una funzione:

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';

e chiamalo semplicemente:

SELECT  fn_upd_invoices('12345', 'TRUE')

1
In realtà, questo non funziona: posso chiamare INSERT INTO hundred (name, name_slug, status) SELECT 'Chichester', 'chichester', NULL WHERE 'Chichester' NOT IN (SELECT NAME FROM hundred);un numero qualsiasi di volte e continua a inserire la riga.
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. C'è un record.
Quassnoi,

12

Puoi utilizzare VALORI - disponibili in Postgres:

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

12
SELEZIONA nome DA Persona <--- cosa succede se ci sono un miliardo di righe di persona?
Henley Chiu,

1
Penso che questo sia un bel modo rapido per risolvere il problema, ma solo quando sei sicuro che la tabella dei sorgenti non diventerà mai enorme. Ho una tabella che non avrà mai più di 1000 righe, quindi posso usare questa soluzione.
Leonard,

WOW, questo è esattamente ciò di cui avevo bisogno. Ero preoccupato che avrei dovuto creare una funzione o una tabella temporanea, ma ciò impedisce tutto ciò - grazie!
Amalgovinus,

8

So che questa domanda è di qualche tempo fa, ma ho pensato che ciò potesse aiutare qualcuno. Penso che il modo più semplice per farlo sia tramite un trigger. Per esempio:

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();

Eseguire questo codice da un prompt di psql (o comunque si desideri eseguire query direttamente sul database). Quindi puoi inserire normalmente da Python. Per esempio:

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

Si noti che come già menzionato @Thomas_Wouters, il codice sopra utilizza i parametri anziché concatenare la stringa.


Se anche qualcun altro si chiedeva, dai documenti : "Trigger a livello di riga attivati ​​PRIMA di poter restituire null per segnalare al trigger manager di saltare il resto dell'operazione per questa riga (ovvero, i trigger successivi non vengono attivati ​​e INSERT / UPDATE / DELETE non si verifica per questa riga. Se viene restituito un valore non null, l'operazione procede con quel valore di riga. "
Pete,

Esattamente questa risposta che stavo cercando. Codice pulito, usando la funzione + trigger invece dell'istruzione select. +1
Jacek Krawczyk,

Adoro questa risposta, utilizzare la funzione e il trigger. Ora trovo un altro modo per rompere lo stallo usando le funzioni e i trigger ...
Sukma Saputra

7

C'è un buon modo di fare INSERT condizionale in PostgreSQL usando la query 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

Questo è esattamente il problema che devo affrontare e la mia versione è 9.5

E lo risolvo con la query SQL di seguito.

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;

Spero che possa aiutare qualcuno che ha lo stesso problema con la versione> = 9.5.

Grazie per aver letto.


5

INSERIRE .. DOVE NON ESISTE è un buon approccio. E le condizioni di gara possono essere evitate dalla "busta" della transazione:

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

2

È facile con le regole:

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

Ma fallisce con le scritture simultanee ...


1

L'approccio con il maggior numero di voti (da John Doe) funziona in qualche modo per me, ma nel mio caso dalle 422 file previste ne ottengo solo 180. Non sono riuscito a trovare nulla di sbagliato e non ci sono errori, quindi ho cercato un altro approccio semplice.

Usando IF NOT FOUND THENdopo aSELECT a funziona perfettamente per me.

(descritto nella documentazione PostgreSQL )

Esempio dalla documentazione:

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

1

La classe di cursori psycopgs ha l'attributo rowcount .

Questo attributo di sola lettura specifica il numero di righe che l'ultimo ha eseguito * () prodotto (per istruzioni DQL come SELECT) o interessato (per istruzioni DML come UPDATE o INSERT).

Quindi potresti provare prima UPDATE e INSERT solo se il conteggio delle righe è 0.

Ma a seconda dei livelli di attività nel database è possibile che si verifichi una condizione di competizione tra UPDATE e INSERT in cui un altro processo può creare quel record nel frattempo.


Presumibilmente il wrapping di queste query in una transazione allevierebbe le condizioni di gara.
Daniel Lyons,

Grazie, soluzione davvero semplice e pulita
Alexander Malfait,

1

La tua colonna "cento" sembra essere definita come chiave primaria e quindi deve essere unica, il che non è il caso. Il problema non è con, è con i tuoi dati.

Ti suggerisco di inserire un ID come tipo seriale per gestire la chiave primaria


1

Se dici che molte delle tue righe sono identiche, finirai per controllare più volte. Puoi inviarli e il database determinerà se inserirlo o meno con la clausola ON CONFLICT come segue

  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

Stavo cercando una soluzione simile, cercando di trovare SQL che funzionasse in PostgreSQL e HSQLDB. (HSQLDB è stato ciò che ha reso questo difficile.) Usando il tuo esempio come base, questo è il formato che ho trovato altrove.

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

Ecco una generica funzione Python che, dato un tablename, colonne e valori, genera l'equivalente upsert per postgresql.

import 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 soluzione in modo semplice, ma non immediato.
Se si desidera utilizzare questa istruzione, è necessario apportare una modifica al db:

ALTER USER user SET search_path to 'name_of_schema';

dopo queste modifiche "INSERT" funzionerà correttamente.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.