Postgres: INSERT, falls noch nicht vorhanden

361

Ich verwende Python, um in eine Postgres-Datenbank zu schreiben:

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

Da jedoch einige meiner Zeilen identisch sind, wird der folgende Fehler angezeigt:

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

Wie kann ich eine SQL-Anweisung "INSERT" schreiben, wenn diese Zeile nicht bereits vorhanden ist?

Ich habe komplexe Aussagen wie diese empfohlen gesehen:

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

Aber erstens ist dies ein Overkill für das, was ich brauche, und zweitens, wie kann ich eine davon als einfache Zeichenfolge ausführen?

AP257
quelle
56
Unabhängig davon, wie Sie dieses Problem lösen, sollten Sie Ihre Abfrage nicht so generieren. Verwenden Sie Parameter in Ihrer Abfrage und übergeben Sie die Werte separat. siehe stackoverflow.com/questions/902408/…
Thomas Wouters
3
Warum nicht die Ausnahme abfangen und ignorieren?
Matthew Mitchell
5
Ab Posgres 9.5 (derzeit in Beta2) gibt es eine neue Upsert-ähnliche Funktion, siehe: postgresql.org/docs/9.5/static/sql-insert.html#SQL-ON-CONFLICT
Ezequiel Moreno
2
Haben Sie darüber nachgedacht, eine Antwort darauf zu akzeptieren? =]
Relequestual

Antworten:

513

Postgres 9.5 (veröffentlicht seit dem 07.01.2016) bietet INSERT einen "upsert" -Befehl, der auch als ON CONFLICT-Klausel bezeichnet wird :

INSERT ... ON CONFLICT DO NOTHING/UPDATE

Es löst viele der subtilen Probleme, auf die Sie bei gleichzeitiger Operation stoßen können, was einige andere Antworten vorschlagen.

Arie
quelle
14
9.5 wurde veröffentlicht.
Luckydonald
2
@TusharJain Vor PostgreSQL 9.5 können Sie ein "altmodisches" UPSERT (mit CTE) erstellen, es können jedoch Probleme mit den Rennbedingungen auftreten, und es wird nicht als 9.5-Stil ausgeführt. In diesem Blog (im aktualisierten Bereich unten) finden Sie ein gutes Detail zu Upsert, einschließlich einiger Links, wenn Sie mehr über die Details erfahren möchten.
Skyguard
16
Hier sind zwei einfache Beispiele. (1) EINFÜGEN, wenn nicht vorhanden, sonst NICHTS - INSERT INTO distributors (did, dname) VALUES (7, 'Redline GmbH') ON CONFLICT (did) DO NOTHING;(2) EINFÜGEN, falls nicht vorhanden, sonst UPDATE - INSERT INTO distributors (did, dname) VALUES (5, 'Gizmo Transglobal'), (6, 'Associated Computing, Inc') ON CONFLICT (did) DO UPDATE SET dname = EXCLUDED.dname;Diese Beispiele stammen aus dem Handbuch - postgresql.org/docs/9.5/static/sql-insert.html
AnnieFromTaiwan
13
Es gibt eine Einschränkung / Nebenwirkung. In einer Tabelle mit Sequenzspalte (seriell oder bigserial) wird die Sequenz bei jedem Einfügeversuch erhöht, auch wenn keine Zeile eingefügt wird.
Grzegorz Luczywo
2
Es ist besser, auf die INSERT-Dokumentation zu verlinken, als auf die Veröffentlichung zu verweisen. Doc Link: postgresql.org/docs/9.5/static/sql-insert.html
Borjagvo
379

Wie kann ich eine SQL-Anweisung "INSERT" schreiben, wenn diese Zeile nicht bereits vorhanden ist?

Es gibt eine gute Möglichkeit, bedingtes INSERT in PostgreSQL durchzuführen:

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

CAVEAT Dieser Ansatz ist jedoch für gleichzeitige Schreibvorgänge nicht 100% zuverlässig . Es gibt eine sehr kleine Rennbedingung zwischen dem SELECTim NOT EXISTSAnti-Semi-Join und dem INSERTselbst. Es kann unter solchen Bedingungen versagen.

John Doe
quelle
Wie sicher ist dies unter der Annahme, dass das Feld "Name" eine EINZIGARTIGE Einschränkung hat? Wird es jemals mit einer einzigartigen Verletzung scheitern?
Agnsaft
2
Das funktioniert gut. Das einzige Problem ist wohl die Kopplung: Was ist, wenn man die Tabelle so ändert, dass mehr Spalten eindeutig sind? In diesem Fall müssen alle Skripte geändert werden. Es wäre schön, wenn es einen allgemeineren Weg gäbe, dies zu tun ...
Willem Van Onsem
1
Ist es möglich, es RETURNS idzum Beispiel zu verwenden, um idfestzustellen , ob es eingefügt wurde oder nicht?
Olivier Pons
2
@OlivierPons ja, das ist möglich. Fügen Sie RETURNING idam und der Abfrage hinzu, und es wird entweder eine neue Zeilen-ID oder nichts zurückgegeben, wenn keine Zeile eingefügt wurde.
AlexM
4
Ich habe festgestellt, dass dies unzuverlässig ist. Es scheint, dass Postgres manchmal die Einfügung ausführt, bevor es die Auswahl ausgeführt hat, und ich am Ende eine doppelte Schlüsselverletzung habe, obwohl der Datensatz noch nicht eingefügt wurde. Versuchen Sie, Version => 9.5 mit ON CONFLICT zu verwenden.
Michael Silver
51

Ein Ansatz wäre, eine Tabelle ohne Einschränkungen (keine eindeutigen Indizes) zu erstellen, in die alle Ihre Daten eingefügt werden, und eine andere Auswahl zu treffen, um sie in Ihre Hundert-Tabelle einzufügen.

So hoch wäre das Niveau. Ich gehe davon aus, dass alle drei Spalten in meinem Beispiel unterschiedlich sind. Ändern Sie daher für Schritt 3 den Join NOT EXITS, um nur die eindeutigen Spalten in der Hundert-Tabelle zu verknüpfen.

  1. Erstellen Sie eine temporäre Tabelle. Siehe Dokumente hier .

    CREATE TEMPORARY TABLE temp_data(name, name_slug, status);
  2. INSERT Daten in temporäre Tabelle.

    INSERT INTO temp_data(name, name_slug, status); 
  3. Fügen Sie der temporären Tabelle alle Indizes hinzu.

  4. Haupttabelle einfügen.

    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
        );
Kuberchaun
quelle
3
Dies ist der schnellste Weg, Masseneinfügungen durchzuführen, wenn ich nicht weiß, ob die Zeile bereits vorhanden ist.
nate c
'X' auswählen? kann jemand klarstellen? Dies ist einfach eine ausgewählte Aussage richtig: SELECT name,name_slug,statusoder*
Roberthuttinger
3
Suchen Sie nach korrelierten Unterabfragen. 'X' kann in eine 1 oder sogar 'SadClown' geändert werden. SQL erfordert, dass es etwas gibt, und 'X' wird häufig verwendet. Es ist klein und macht deutlich, dass eine korrelierte Unterabfrage verwendet wird und die Anforderungen der SQL-Anforderungen erfüllt.
Kuberchaun
Sie haben erwähnt, dass Sie "alle Ihre Daten in (unter der Annahme einer temporären Tabelle) einfügen und eine andere Auswahl treffen". In diesem Fall sollte es nicht sein SELECT DISTINCT name, name_slug, status FROM temp_data?
Gibbz00
17

PostgreSQLUnterstützt leider weder MERGEnoch ON DUPLICATE KEY UPDATE, so dass Sie es in zwei Anweisungen tun müssen:

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

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

Sie können es in eine Funktion einschließen:

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

und nenne es einfach:

SELECT  fn_upd_invoices('12345', 'TRUE')
Quassnoi
quelle
1
Eigentlich funktioniert das nicht: Ich kann INSERT INTO hundred (name, name_slug, status) SELECT 'Chichester', 'chichester', NULL WHERE 'Chichester' NOT IN (SELECT NAME FROM hundred);beliebig oft anrufen und es wird immer wieder die Zeile eingefügt.
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. Es gibt einen Datensatz.
Quassnoi
12

Sie können VALUES verwenden - verfügbar in Postgres:

INSERT INTO person (name)
    SELECT name FROM person
    UNION 
    VALUES ('Bob')
    EXCEPT
    SELECT name FROM person;
crististm
quelle
12
SELECT name FROM Person <--- Was ist, wenn es eine Milliarde Zeilen in Person gibt?
Henley Chiu
1
Ich denke, dies ist ein guter schneller Weg, um das Problem zu lösen, aber nur, wenn Sie sicher sind, dass die Quelltabelle niemals riesig wird. Ich habe eine Tabelle, die niemals mehr als 1000 Zeilen enthalten wird, damit ich diese Lösung verwenden kann.
Leonard
WOW, genau das habe ich gebraucht. Ich hatte Angst, dass ich eine Funktion oder eine temporäre Tabelle erstellen müsste, aber das schließt das alles aus - danke!
Amalgovinus
8

Ich weiß, dass diese Frage von vor einiger Zeit stammt, dachte aber, dass dies jemandem helfen könnte. Ich denke, der einfachste Weg, dies zu tun, ist über einen Auslöser. Z.B:

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

Führen Sie diesen Code an einer psql-Eingabeaufforderung aus (oder wie auch immer Sie Abfragen direkt in der Datenbank ausführen möchten). Dann können Sie wie gewohnt aus Python einfügen. Z.B:

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

Beachten Sie, dass der obige Code, wie bereits erwähnt, bei @Thomas_Wouters die Parameter nutzt, anstatt die Zeichenfolge zu verketten.

ktr
quelle
Wenn sich auch jemand anderes gefragt hat, aus den Dokumenten : "Trigger auf Zeilenebene , die VORHER ausgelöst wurden, können null zurückgeben, um dem Trigger-Manager zu signalisieren, den Rest des Vorgangs für diese Zeile zu überspringen (dh nachfolgende Trigger werden nicht ausgelöst, und INSERT / UPDATE) / DELETE tritt für diese Zeile nicht auf.) Wenn ein Nicht-Null-Wert zurückgegeben wird, wird die Operation mit diesem Zeilenwert fortgesetzt. "
Pete
Genau diese Antwort habe ich gesucht. Bereinigen Sie den Code mit Funktion + Trigger anstelle der Anweisung select. +1
Jacek Krawczyk
Ich liebe diese Antwort, benutze Funktion und Trigger. Jetzt finde ich einen anderen Weg, um den Deadlock mit Funktionen und Triggern zu
überwinden
7

Es gibt eine gute Möglichkeit, das bedingte EINFÜGEN in PostgreSQL mithilfe der WITH-Abfrage durchzuführen: Gefällt mir:

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 
Ritesh Jha
quelle
7

Dies ist genau das Problem, mit dem ich konfrontiert bin, und meine Version ist 9.5

Und ich löse es mit SQL-Abfrage unten.

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;

Hoffe, das hilft jemandem, der das gleiche Problem mit Version> = 9.5 hat.

Danke fürs Lesen.

tuanngocptn
quelle
5

EINFÜGEN .. WO NICHT EXISTIERT ist ein guter Ansatz. Und Rennbedingungen können durch Transaktion "Umschlag" vermieden werden:

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

Mit Regeln ist es einfach:

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

Aber es schlägt fehl bei gleichzeitigen Schreibvorgängen ...


quelle
1

Der Ansatz mit den meisten Upvotes (von John Doe) funktioniert irgendwie für mich, aber in meinem Fall von den erwarteten 422 Zeilen bekomme ich nur 180. Ich konnte nichts falsch finden und es gibt überhaupt keine Fehler, also habe ich nach einem anderen gesucht einfacher Ansatz.

Die Verwendung IF NOT FOUND THENnach einem SELECTfunktioniert einfach perfekt für mich.

(beschrieben in der PostgreSQL-Dokumentation )

Beispiel aus der Dokumentation:

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

Die Psycopgs-Cursor-Klasse hat das Attribut rowcount .

Dieses schreibgeschützte Attribut gibt die Anzahl der Zeilen an, die die letzte Ausführung * () erzeugt (für DQL-Anweisungen wie SELECT) oder betroffen (für DML-Anweisungen wie UPDATE oder INSERT) hat.

Sie können also zuerst UPDATE und INSERT nur versuchen, wenn die Zeilenanzahl 0 ist.

Abhängig von den Aktivitätsstufen in Ihrer Datenbank kann es jedoch vorkommen, dass zwischen UPDATE und INSERT eine Race-Bedingung vorliegt, bei der ein anderer Prozess diesen Datensatz in der Zwischenzeit erstellen kann.

Johnbaum
quelle
Vermutlich würde das Umschließen dieser Abfragen in eine Transaktion die Rennbedingung lindern.
Daniel Lyons
Danke, wirklich einfache und saubere Lösung
Alexander Malfait
1

Ihre Spalte "hundert" scheint als Primärschlüssel definiert zu sein und muss daher eindeutig sein, was nicht der Fall ist. Das Problem liegt nicht bei Ihren Daten.

Ich schlage vor, Sie geben eine ID als seriellen Typ ein, um den Primärschlüssel zu übergeben

Boodoo
quelle
1

Wenn Sie sagen, dass viele Ihrer Zeilen identisch sind, beenden Sie die Überprüfung mehrmals. Sie können sie senden, und die Datenbank bestimmt wie folgt, ob sie mit der ON CONFLICT-Klausel eingefügt wird oder nicht

  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);
Öffne ein
quelle
0

Ich suchte nach einer ähnlichen Lösung und versuchte, SQL zu finden, das sowohl in PostgreSQL als auch in HSQLDB funktioniert. (HSQLDB hat dies schwierig gemacht.) Anhand Ihres Beispiels als Basis ist dies das Format, das ich an anderer Stelle gefunden habe.

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 );"
Jeff Fairley
quelle
-1

Hier ist eine generische Python-Funktion, die unter Angabe eines Tabellennamens, von Spalten und Werten das Upsert-Äquivalent für postgresql generiert.

json importieren

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)
Patrick
quelle
-8

Die Lösung ist einfach, aber nicht sofort.
Wenn Sie diese Anweisung verwenden möchten, müssen Sie eine Änderung an der Datenbank vornehmen:

ALTER USER user SET search_path to 'name_of_schema';

Nach diesen Änderungen funktioniert "INSERT" korrekt.

el fuser
quelle