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".
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
.
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 .
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:
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 INSERT
s; 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.
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;
MERGE
im PostgreSQL-WikiMERGE
?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.
INSERT ... ON DUPLICATE KEY UPDATE
in MySQLMERGE
von MS SQL Server (siehe oben zu MERGE
Problemen)MERGE
von Oracle (siehe oben zu MERGE
Problemen)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.
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;
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
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)
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 ).