it-swarm.com.de

Wie kann ich in PostgreSQL UPSERT (MERGE, INSERT ... ON DUPLICATE UPDATE) durchführen?

Eine sehr häufig gestellte Frage ist hier, wie Sie einen Upsert durchführen. Dies nennt MySQL INSERT ... ON DUPLICATE UPDATE und wird vom Standard als Teil der MERGE-Operation unterstützt.

Da PostgreSQL dies nicht direkt unterstützt (vor Seite 9.5), wie können Sie dies tun? Folgendes berücksichtigen:

CREATE TABLE testtable (
    id integer PRIMARY KEY,
    somedata text NOT NULL
);

INSERT INTO testtable (id, somedata) VALUES
(1, 'fred'),
(2, 'bob');

Stellen Sie sich nun vor, Sie möchten die Tupel (2, 'Joe'), (3, 'Alan') "upsert", so dass der neue Tabelleninhalt folgendermaßen lautet:

(1, 'fred'),
(2, 'Joe'),    -- Changed value of existing Tuple
(3, 'Alan')    -- Added new Tuple

Darüber reden die Leute, wenn sie über eine upsert diskutieren. Entscheidend ist, dass jeder Ansatz sicher sein muss, wenn mehrere Transaktionen in derselben Tabelle arbeiten} - entweder durch explizites Sperren oder auf andere Weise gegen die resultierenden Race-Bedingungen.

Dieses Thema wird ausführlich unter Insert besprochen, bei doppelten Updates in PostgreSQL. , aber hier geht es um Alternativen zur MySQL-Syntax, und im Laufe der Zeit ist das Ganze ein wenig ohne Zusammenhang. Ich arbeite an definitiven Antworten.

Diese Techniken sind auch nützlich für "Einfügen, wenn nicht vorhanden, sonst nichts tun", d. H. "Einfügen ... bei doppeltem Schlüssel ignorieren".

215
Craig Ringer

9.5 und neuer:

PostgreSQL 9.5 und neuere Versionen unterstützen INSERT ... ON CONFLICT UPDATE (und ON CONFLICT DO NOTHING), d. H.

Vergleich mit ON DUPLICATE KEY UPDATE .

Kurze Erklärung .

Zur Verwendung siehe das Handbuch - speziell die Klausel conflict_action im Syntaxdiagramm und den erläuternden Text =.

Im Gegensatz zu den Lösungen für 9.4 und älter, die unten angegeben sind, funktioniert diese Funktion mit mehreren in Konflikt stehenden Zeilen und erfordert weder eine exklusive Sperrung noch eine Wiederholungsschleife.

Der Commit, der das Feature hinzufügt, ist hier und die Diskussion um seine Entwicklung ist hier .


Wenn Sie mit Version 9.5 arbeiten und nicht abwärtskompatibel sein müssen, können Sie jetzt aufhören zu lesen .


9.4 und älter:

PostgreSQL verfügt über keine eingebaute UPSERT (oder MERGE) - Funktion, und es ist sehr schwierig, dies bei gleichzeitiger Verwendung effizient durchzuführen.

Dieser Artikel beschreibt das Problem ausführlich .

Im Allgemeinen müssen Sie zwischen zwei Optionen wählen:

  • Einzelne Einfüge-/Aktualisierungsvorgänge in einer Wiederholungsschleife; oder
  • Tabelle sperren und Batch-Merge durchführen

Wiederholungsschleife für einzelne Zeilen

Die Verwendung einzelner Zeilenumbrüche in einer Wiederholungsschleife ist die sinnvolle Option, wenn viele Verbindungen gleichzeitig versuchen, Einfügungen durchzuführen.

Die PostgreSQL-Dokumentation enthält eine nützliche Prozedur, mit der Sie dies in einer Schleife innerhalb der Datenbank tun können. . Im Gegensatz zu den meisten naiven Lösungen schützt es vor verlorenen Updates und fügt Rennen ein. Es funktioniert nur im READ COMMITTED -Modus und ist nur dann sicher, wenn es das einzige ist, was Sie in der Transaktion tun. Die Funktion funktioniert nicht richtig, wenn Trigger oder sekundäre eindeutige Schlüssel eindeutige Verstöße verursachen.

Diese Strategie ist sehr ineffizient. Wann immer es sinnvoll ist, sollten Sie die Arbeit in die Warteschlange stellen und stattdessen einen Bulk-Upsert durchführen, wie unten beschrieben.

Viele Lösungsversuche für dieses Problem berücksichtigen Rollbacks nicht und führen daher zu unvollständigen Aktualisierungen. Zwei Transaktionen rennen miteinander; einer von ihnen erfolgreich INSERTs; der andere erhält einen doppelten Schlüsselfehler und führt stattdessen ein UPDATE aus. Das UPDATE blockiert das Warten auf das INSERT, um ein Rollback oder ein Commit durchzuführen. Wenn es zurückgesetzt wird, stimmt die erneute Prüfung der UPDATE -Bedingung mit null Zeilen überein. Obwohl die UPDATE -Zeichen festschreiben, hat sie den erwarteten Fehler nicht tatsächlich verursacht. Sie müssen die Anzahl der Ergebniszeilen überprüfen und gegebenenfalls erneut versuchen.

Einige Lösungsversuche berücksichtigen auch SELECT-Rennen nicht. Wenn Sie das Offensichtliche und Einfache versuchen:

-- THIS IS WRONG. DO NOT COPY IT. It's an EXAMPLE.

BEGIN;

UPDATE testtable
SET somedata = 'blah'
WHERE id = 2;

-- Remember, this is WRONG. Do NOT COPY IT.

INSERT INTO testtable (id, somedata)
SELECT 2, 'blah'
WHERE NOT EXISTS (SELECT 1 FROM testtable WHERE testtable.id = 2);

COMMIT;

wenn dann zwei gleichzeitig laufen, gibt es mehrere Fehlermodi. Eines ist das bereits diskutierte Problem mit einem erneuten Update-Check. Eine andere ist, wo beide UPDATE gleichzeitig übereinstimmen, null Zeilen und fortfahren. Dann machen beide den EXISTS -Test, der vor dem INSERT stattfindet. Beide erhalten null Zeilen, also beide das INSERT. Ein Fehler mit einem doppelten Schlüsselfehler.

Aus diesem Grund benötigen Sie eine Wiederholungsschleife. Sie könnten denken, dass Sie doppelte Schlüsselfehler oder verlorene Aktualisierungen mit cleverem SQL verhindern können, aber Sie können nicht. Sie müssen die Zeilenanzahl überprüfen oder doppelte Schlüsselfehler behandeln (je nach gewähltem Ansatz) und es erneut versuchen.

Bitte rollen Sie nicht Ihre eigene Lösung dafür. Wie bei der Nachrichtenwarteschlange ist es wahrscheinlich falsch.

Bulk-Upsert mit Schloss

Manchmal möchten Sie ein Bulk-Upsert durchführen, bei dem Sie einen neuen Datensatz haben, den Sie in einen älteren vorhandenen Datensatz zusammenführen möchten. Dies ist wesentlich effizienter als einzelne Zeilenumbrüche und sollte nach Möglichkeit bevorzugt werden.

In diesem Fall befolgen Sie normalerweise den folgenden Prozess:

  • CREATE eine TEMPORARY Tabelle

  • COPY oder fügen Sie die neuen Daten in die temporäre Tabelle ein

  • LOCK die Zieltabelle IN EXCLUSIVE MODE. Dadurch können andere Transaktionen SELECT, aber keine Änderungen an der Tabelle vornehmen.

  • Führen Sie einen UPDATE ... FROM der vorhandenen Datensätze mit den Werten in der temporären Tabelle aus.

  • Führen Sie ein INSERT von Zeilen aus, die noch nicht in der Zieltabelle vorhanden sind.

  • COMMIT, Freigabe der Sperre.

Beispiel: Verwenden Sie für das in der Frage angegebene Beispiel INSERT mit mehreren Werten, um die temporäre Tabelle zu füllen:

BEGIN;

CREATE TEMPORARY TABLE newvals(id integer, somedata text);

INSERT INTO newvals(id, somedata) VALUES (2, 'Joe'), (3, 'Alan');

LOCK TABLE testtable IN EXCLUSIVE MODE;

UPDATE testtable
SET somedata = newvals.somedata
FROM newvals
WHERE newvals.id = testtable.id;

INSERT INTO testtable
SELECT newvals.id, newvals.somedata
FROM newvals
LEFT OUTER JOIN testtable ON (testtable.id = newvals.id)
WHERE testtable.id IS NULL;

COMMIT;

Verwandte Lesung

Was ist mit MERGE?

Der SQL-Standard MERGE weist tatsächlich eine schlecht definierte Parallelitätssemantik auf und eignet sich nicht zum Aktualisieren, ohne zuvor eine Tabelle zu sperren.

Es ist eine wirklich nützliche OLAP Anweisung für das Zusammenführen von Daten, aber es ist eigentlich keine nützliche Lösung für das gleichzeitige Sichern von Problemen. Es gibt viele Ratschläge für Leute, die andere DBMS verwenden, um MERGE für Upserts zu verwenden, aber es ist tatsächlich falsch.

Andere DBs:

365
Craig Ringer

Ich versuche, mit den PostgreSQL-Versionen vor 9.5 einen Beitrag zu einer anderen Lösung für das Problem des einmaligen Einfügens zu leisten. Die Idee besteht einfach darin, zuerst das Einfügen durchzuführen und, falls der Datensatz bereits vorhanden ist, ihn zu aktualisieren:

do $$
begin 
  insert into testtable(id, somedata) values(2,'Joe');
exception when unique_violation then
  update testtable set somedata = 'Joe' where id = 2;
end $$;

Beachten Sie, dass diese Lösung angewendet werden kann nur wenn keine Zeilen der Tabelle gelöscht wurden.

Ich weiß nichts über die Effizienz dieser Lösung, aber sie scheint mir vernünftig genug zu sein.

29
Renzo

Hier einige Beispiele für insert ... on conflict ... (pg 9.5+): 

  • Einfügen, bei Konflikt - nichts tun .
    insert into dummy(id, name, size) values(1, 'new_name', 3) on conflict do nothing; 

  • Einfügen bei Konflikt - Update durchführen , Konfliktziel über Spalte angeben .
    insert into dummy(id, name, size) values(1, 'new_name', 3) on conflict(id) do update set name = 'new_name', size = 3; 

  • Einfügen bei Konflikt - Aktualisierung durchführen , Konfliktziel über Einschränkungsname angeben.
    insert into dummy(id, name, size) values(1, 'new_name', 3) on conflict on constraint dummy_pkey do update set name = 'new_name', size = 4;

7
Eric Wang
WITH UPD AS (UPDATE TEST_TABLE SET SOME_DATA = 'Joe' WHERE ID = 2 
RETURNING ID),
INS AS (SELECT '2', 'Joe' WHERE NOT EXISTS (SELECT * FROM UPD))
INSERT INTO TEST_TABLE(ID, SOME_DATA) SELECT * FROM INS

Getestet auf Postgresql 9.3

3
aristar

SQLAlchemy Upsert für Postgres> = 9.5

Da der große Beitrag oben viele verschiedene SQL-Ansätze für Postgres-Versionen abdeckt (nicht nur Nicht-9.5 wie in der Frage), möchte ich hinzufügen, wie dies in SQLAlchemy geschieht, wenn Sie Postgres 9.5 verwenden. Anstelle eines eigenen Upserts können Sie auch die Funktionen von SQLAlchemy verwenden (die in SQLAlchemy 1.1 hinzugefügt wurden). Ich persönlich würde empfehlen, diese zu verwenden, wenn möglich. Nicht nur wegen der Bequemlichkeit, sondern auch weil PostgreSQL alle Race-Bedingungen handhaben kann.

Cross-Posting aus einer anderen Antwort, die ich gestern gegeben habe ( https://stackoverflow.com/a/44395983/2156909 )

SQLAlchemy unterstützt ON CONFLICT jetzt mit zwei Methoden on_conflict_do_update() und on_conflict_do_nothing():

Kopieren aus der Dokumentation:

from sqlalchemy.dialects.postgresql import insert

stmt = insert(my_table).values(user_email='[email protected]', data='inserted data')
stmt = stmt.on_conflict_do_update(
    index_elements=[my_table.c.user_email],
    index_where=my_table.c.user_email.like('%@gmail.com'),
    set_=dict(data=stmt.excluded.data)
    )
conn.execute(stmt)

http://docs.sqlalchemy.org/de/latest/dialects/postgresql.html?highlight=conflict#insert-on-conflict-upsert

2
P.R.

Da diese Frage geschlossen wurde, poste ich hier, wie Sie SQLAlchemy verwenden. Durch Rekursion wird eine Masseneinfügung oder ein Update wiederholt, um race-Bedingungen und Validierungsfehler zu bekämpfen.

Zuerst die Importe 

import itertools as it

from functools import partial
from operator import itemgetter

from sqlalchemy.exc import IntegrityError
from app import session
from models import Posts

Jetzt funktioniert ein paar Helfer

def chunk(content, chunksize=None):
    """Groups data into chunks each with (at most) `chunksize` items.
    https://stackoverflow.com/a/22919323/408556
    """
    if chunksize:
        i = iter(content)
        generator = (list(it.islice(i, chunksize)) for _ in it.count())
    else:
        generator = iter([content])

    return it.takewhile(bool, generator)


def gen_resources(records):
    """Yields a dictionary if the record's id already exists, a row object 
    otherwise.
    """
    ids = {item[0] for item in session.query(Posts.id)}

    for record in records:
        is_row = hasattr(record, 'to_dict')

        if is_row and record.id in ids:
            # It's a row but the id already exists, so we need to convert it 
            # to a dict that updates the existing record. Since it is duplicate,
            # also yield True
            yield record.to_dict(), True
        Elif is_row:
            # It's a row and the id doesn't exist, so no conversion needed. 
            # Since it's not a duplicate, also yield False
            yield record, False
        Elif record['id'] in ids:
            # It's a dict and the id already exists, so no conversion needed. 
            # Since it is duplicate, also yield True
            yield record, True
        else:
            # It's a dict and the id doesn't exist, so we need to convert it. 
            # Since it's not a duplicate, also yield False
            yield Posts(**record), False

Und zum Schluss die Aufwärtsfunktion

def upsert(data, chunksize=None):
    for records in chunk(data, chunksize):
        resources = gen_resources(records)
        sorted_resources = sorted(resources, key=itemgetter(1))

        for dupe, group in it.groupby(sorted_resources, itemgetter(1)):
            items = [g[0] for g in group]

            if dupe:
                _upsert = partial(session.bulk_update_mappings, Posts)
            else:
                _upsert = session.add_all

            try:
                _upsert(items)
                session.commit()
            except IntegrityError:
                # A record was added or deleted after we checked, so retry
                # 
                # modify accordingly by adding additional exceptions, e.g.,
                # except (IntegrityError, ValidationError, ValueError)
                db.session.rollback()
                upsert(items)
            except Exception as e:
                # Some other error occurred so reduce chunksize to isolate the 
                # offending row(s)
                db.session.rollback()
                num_items = len(items)

                if num_items > 1:
                    upsert(items, num_items // 2)
                else:
                    print('Error adding record {}'.format(items[0]))

So verwenden Sie es

>>> data = [
...     {'id': 1, 'text': 'updated post1'}, 
...     {'id': 5, 'text': 'updated post5'}, 
...     {'id': 1000, 'text': 'new post1000'}]
... 
>>> upsert(data)

Der Vorteil gegenüber bulk_save_objects besteht darin, dass Beziehungen, Fehlerprüfungen usw. beim Einfügen verarbeitet werden können (im Gegensatz zu bulk-Vorgängen ).

0
reubano