psycopg2: Fügen Sie mehrere Zeilen mit einer Abfrage ein

141

Ich muss mehrere Zeilen mit einer Abfrage einfügen (die Anzahl der Zeilen ist nicht konstant), daher muss ich eine Abfrage wie diese ausführen:

INSERT INTO t (a, b) VALUES (1, 2), (3, 4), (5, 6);

Der einzige Weg, den ich kenne, ist

args = [(1,2), (3,4), (5,6)]
args_str = ','.join(cursor.mogrify("%s", (x, )) for x in args)
cursor.execute("INSERT INTO t (a, b) VALUES "+args_str)

aber ich möchte einen einfacheren Weg.

Sergey Fedoseev
quelle

Antworten:

219

Ich habe ein Programm erstellt, das mehrere Zeilen in einen Server einfügt, der sich in einer anderen Stadt befindet.

Ich fand heraus, dass die Verwendung dieser Methode etwa zehnmal schneller war als executemany. In meinem Fall tuphandelt es sich um ein Tupel mit etwa 2000 Zeilen. Bei dieser Methode dauerte es ungefähr 10 Sekunden:

args_str = ','.join(cur.mogrify("(%s,%s,%s,%s,%s,%s,%s,%s,%s)", x) for x in tup)
cur.execute("INSERT INTO table VALUES " + args_str) 

und 2 Minuten bei Verwendung dieser Methode:

cur.executemany("INSERT INTO table VALUES(%s,%s,%s,%s,%s,%s,%s,%s,%s)", tup)
ant32
quelle
15
Fast zwei Jahre später immer noch sehr relevant. Eine heutige Erfahrung zeigt, dass es mit zunehmender Anzahl von Zeilen, die Sie verschieben möchten, umso besser ist, die executeStrategie zu verwenden. Dank dessen habe ich eine Beschleunigung von ungefähr 100x gesehen!
Rob Watts
4
Möglicherweise wird executemanynach jeder Einfügung ein Commit ausgeführt. Wenn Sie stattdessen das Ganze in eine Transaktion einwickeln, würde dies möglicherweise die Dinge beschleunigen?
Richard
4
Ich habe diese Verbesserung gerade selbst bestätigt. Nach dem, was ich gelesen habe, executemanymacht psycopg2's nichts Optimales, nur Schleifen und macht viele executeAussagen. Mit dieser Methode wurde eine Einfügung von 700 Zeilen auf einen Remote-Server von 60 Sekunden auf <2 Sekunden verschoben.
Nelson
5
Vielleicht bin ich paranoid, aber die Verkettung der Abfrage mit einem +scheint sich für eine SQL-Injektion zu öffnen. Ich denke, die @ Clodoaldo Neto- execute_values()Lösung ist sicherer.
Will Munn
26
Falls jemand auf den folgenden Fehler stößt: [TypeError: Sequenzelement 0: erwartete str-Instanz, gefundene Bytes] Führen Sie stattdessen diesen Befehl aus [args_str = ','. join (cur.mogrify ("(% s,% s)", x ) .decode ("utf-8") für x in tup)]
mrt
146

Neue execute_valuesMethode in Psycopg 2.7:

data = [(1,'x'), (2,'y')]
insert_query = 'insert into t (a, b) values %s'
psycopg2.extras.execute_values (
    cursor, insert_query, data, template=None, page_size=100
)

Die pythonische Methode in Psycopg 2.6:

data = [(1,'x'), (2,'y')]
records_list_template = ','.join(['%s'] * len(data))
insert_query = 'insert into t (a, b) values {}'.format(records_list_template)
cursor.execute(insert_query, data)

Erläuterung: Wenn die einzufügenden Daten als Liste von Tupeln wie in angegeben sind

data = [(1,'x'), (2,'y')]

dann ist es schon im genau gewünschten format als

  1. Die valuesSyntax der insertKlausel erwartet eine Liste von Datensätzen wie in

    insert into t (a, b) values (1, 'x'),(2, 'y')

  2. Psycopgpasst einen Python tuplean einen Postgresql an record.

Die einzige notwendige Arbeit besteht darin, eine Datensatzlistenvorlage bereitzustellen, die von psycopg ausgefüllt werden soll

# We use the data list to be sure of the template length
records_list_template = ','.join(['%s'] * len(data))

und platzieren Sie es in der insertAbfrage

insert_query = 'insert into t (a, b) values {}'.format(records_list_template)

Drucken der insert_queryAusgänge

insert into t (a, b) values %s,%s

Nun zu den üblichen PsycopgArgumentationsersetzungen

cursor.execute(insert_query, data)

Oder testen Sie einfach, was an den Server gesendet wird

print (cursor.mogrify(insert_query, data).decode('utf8'))

Ausgabe:

insert into t (a, b) values (1, 'x'),(2, 'y')
Clodoaldo Neto
quelle
1
Wie ist die Leistung dieser Methode im Vergleich zu cur.copy_from?
Michael Goldshteyn
1
Hier ist ein Kern mit einem Benchmark . copy_from lässt sich auf meinem Computer mit 10 Millionen Datensätzen auf etwa das 6,5-fache schneller skalieren.
Joseph Sheedy
Sieht gut aus - ich denke, Sie haben am Ende Ihrer anfänglichen Definition von insert_query einen Streuner (es sei denn, Sie haben versucht, daraus ein Tupel zu machen?) Und fehlen wie nach% für% s auch in der anfänglichen Definition von insert_query.
Deadcode
2
Mit execute_valueskonnte ich mein System mit 1k Datensätzen pro Minute bis zu 128k Datensätzen pro Minute zum
Laufen bringen
66

Update mit psycopg2 2.7:

Der Klassiker executemany() ist ungefähr 60-mal langsamer als die Implementierung von @ ant32 ("gefaltet" genannt), wie in diesem Thread erläutert: https://www.postgresql.org/message-id/20170130215151.GA7081%40deb76.aryehleib.com

Diese Implementierung wurde in Version 2.7 zu psycopg2 hinzugefügt und heißt execute_values():

from psycopg2.extras import execute_values
execute_values(cur,
    "INSERT INTO test (id, v1, v2) VALUES %s",
    [(1, 2, 3), (4, 5, 6), (7, 8, 9)])

Vorherige Antwort:

Das Einfügen mehrerer Zeilen ist mit der Multirow- VALUESSyntax mit execute()etwa 10-mal schneller als mit psycopg2 executemany(). Tatsächlich,executemany() läuft nur viele einzelne INSERTAussagen.

Der Code von @ ant32 funktioniert perfekt in Python 2. In Python 3 werden jedoch cursor.mogrify()Bytes zurückgegeben, cursor.execute()entweder Bytes oder Zeichenfolgen verwendet, und','.join()str Instanz erwartet .

In Python 3 müssen Sie möglicherweise den Code von @ ant32 ändern, indem Sie Folgendes hinzufügen .decode('utf-8'):

args_str = ','.join(cur.mogrify("(%s,%s,%s,%s,%s,%s,%s,%s,%s)", x).decode('utf-8') for x in tup)
cur.execute("INSERT INTO table VALUES " + args_str)

Oder indem Sie nur Bytes (mit b''oder b"") verwenden:

args_bytes = b','.join(cur.mogrify("(%s,%s,%s,%s,%s,%s,%s,%s,%s)", x) for x in tup)
cur.execute(b"INSERT INTO table VALUES " + args_bytes) 
Antoine Dusséaux
quelle
26

cursor.copy_from ist die mit Abstand schnellste Lösung, die ich für Masseneinsätze gefunden habe. Hier ist eine Zusammenfassung, die ich mit einer Klasse namens IteratorFile erstellt habe, mit der ein Iterator, der Zeichenfolgen liefert, wie eine Datei gelesen werden kann. Wir können jeden Eingabedatensatz mithilfe eines Generatorausdrucks in eine Zeichenfolge konvertieren. Die Lösung wäre also

args = [(1,2), (3,4), (5,6)]
f = IteratorFile(("{}\t{}".format(x[0], x[1]) for x in args))
cursor.copy_from(f, 'table_name', columns=('a', 'b'))

Für diese triviale Größe von Argumenten macht es keinen großen Geschwindigkeitsunterschied, aber ich sehe große Beschleunigungen, wenn es um Tausende von Zeilen geht. Es ist außerdem speichereffizienter als das Erstellen einer riesigen Abfragezeichenfolge. Ein Iterator würde immer nur einen Eingabedatensatz gleichzeitig im Speicher halten, wobei Ihnen irgendwann der Speicher in Ihrem Python-Prozess oder in Postgres ausgeht, indem Sie die Abfragezeichenfolge erstellen.

Joseph Sheedy
quelle
3
Hier ist ein Benchmark , der copy_from / IteratorFile mit einer Query Builder-Lösung vergleicht. copy_from lässt sich auf meinem Computer mit 10 Millionen Datensätzen auf etwa das 6,5-fache schneller skalieren.
Joseph Sheedy
3
Müssen Sie mit entkommenden Strings und Zeitstempeln usw. herumhüpfen?
CpILL
Ja, Sie müssen sicherstellen, dass Sie über gut geformte TSV-Datensätze verfügen.
Joseph Sheedy
24

Ein Ausschnitt aus der Tutorial-Seite von Psycopg2 auf Postgresql.org (siehe unten) :

Ein letzter Punkt, den ich Ihnen zeigen möchte, ist das Einfügen mehrerer Zeilen mithilfe eines Wörterbuchs. Wenn Sie Folgendes hatten:

namedict = ({"first_name":"Joshua", "last_name":"Drake"},
            {"first_name":"Steven", "last_name":"Foo"},
            {"first_name":"David", "last_name":"Bar"})

Sie können ganz einfach alle drei Zeilen in das Wörterbuch einfügen, indem Sie Folgendes verwenden:

cur = conn.cursor()
cur.executemany("""INSERT INTO bar(first_name,last_name) VALUES (%(first_name)s, %(last_name)s)""", namedict)

Es speichert nicht viel Code, sieht aber definitiv besser aus.

ptrn
quelle
35
Dadurch werden viele einzelne INSERTAnweisungen ausgeführt. Nützlich, aber nicht dasselbe wie ein einzelner Mehrfacheinsatz VALUE.
Craig Ringer
7

Alle diese Techniken werden in der Postgres-Terminologie als "Extended Inserts" bezeichnet. Ab dem 24. November 2016 ist sie immer noch eine Tonne schneller als die Executemany () von psychopg2 () und alle anderen in diesem Thread aufgeführten Methoden (die ich ausprobiert habe, bevor ich dazu gekommen bin) Antworten).

Hier ist ein Code, der cur.mogrify nicht verwendet und nett und einfach ist, um sich zurechtzufinden:

valueSQL = [ '%s', '%s', '%s', ... ] # as many as you have columns.
sqlrows = []
rowsPerInsert = 3 # more means faster, but with diminishing returns..
for row in getSomeData:
        # row == [1, 'a', 'yolo', ... ]
        sqlrows += row
        if ( len(sqlrows)/len(valueSQL) ) % rowsPerInsert == 0:
                # sqlrows == [ 1, 'a', 'yolo', 2, 'b', 'swag', 3, 'c', 'selfie' ]
                insertSQL = 'INSERT INTO "twitter" VALUES ' + ','.join(['(' + ','.join(valueSQL) + ')']*rowsPerInsert)
                cur.execute(insertSQL, sqlrows)
                con.commit()
                sqlrows = []
insertSQL = 'INSERT INTO "twitter" VALUES ' + ','.join(['(' + ','.join(valueSQL) + ')']*len(sqlrows))
cur.execute(insertSQL, sqlrows)
con.commit()

Es sollte jedoch beachtet werden, dass Sie copy_from () verwenden sollten, wenn Sie copy_from () verwenden können;)

JJ
quelle
Auferweckung von den Toten, aber was passiert in der Situation der letzten paar Reihen? Ich gehe davon aus, dass Sie diese letzte Klausel in den letzten verbleibenden Zeilen tatsächlich erneut ausführen, falls Sie eine gerade Anzahl von Zeilen haben.
Mcpeterson
Richtig, tut mir leid, ich muss vergessen haben, das zu tun, als ich das Beispiel schrieb - das ist ziemlich dumm von mir. Wenn ich das nicht getan hätte, hätte ich den Leuten keinen Fehler gemacht, was mich beunruhigt, wie viele Leute die Lösung kopiert / eingefügt und ihren Geschäften nachgegangen sind ..... Wie auch immer, sehr dankbar, mcpeterson - danke!
JJ
2

Ich benutze die obige Antwort von ant32 seit mehreren Jahren. Ich habe jedoch festgestellt, dass dies ein Fehler in Python 3 ist, da mogrifyeine Byte-Zeichenfolge zurückgegeben wird.

Das explizite Konvertieren in Bytse-Zeichenfolgen ist eine einfache Lösung, um Code Python 3 kompatibel zu machen.

args_str = b','.join(cur.mogrify("(%s,%s,%s,%s,%s,%s,%s,%s,%s)", x) for x in tup) 
cur.execute(b"INSERT INTO table VALUES " + args_str)
jprockbelly
quelle
1

Ein weiterer netter und effizienter Ansatz besteht darin, Zeilen zum Einfügen als 1 Argument zu übergeben, bei dem es sich um ein Array von JSON-Objekten handelt.

ZB Sie übergeben Argument:

[ {id: 18, score: 1}, { id: 19, score: 5} ]

Es ist ein Array, das eine beliebige Anzahl von Objekten enthalten kann. Dann sieht Ihr SQL so aus:

INSERT INTO links (parent_id, child_id, score) 
SELECT 123, (r->>'id')::int, (r->>'score')::int 
FROM unnest($1::json[]) as r 

Hinweis: Ihr Postgress muss neu genug sein, um json zu unterstützen

Daniel Garmoshka
quelle
1

Die Lösung cursor.copyfrom , bereitgestellt von @ jopseph.sheedy ( https://stackoverflow.com/users/958118/joseph-sheedy ) oben ( https://stackoverflow.com/a/30721460/11100064) ) ist in der Tat blitzschnell.

Das Beispiel, das er gibt, ist jedoch nicht generisch für einen Datensatz mit einer beliebigen Anzahl von Feldern verwendbar, und ich habe einige Zeit gebraucht, um herauszufinden, wie man es richtig verwendet.

Die Iterator-Datei muss mit solchen durch Tabulatoren getrennten Feldern instanziiert werden ( rist eine Liste von Diktaten, in denen jedes Diktat ein Datensatz ist):

    f = IteratorFile("{0}\t{1}\t{2}\t{3}\t{4}".format(r["id"],
        r["type"],
        r["item"],
        r["month"],
        r["revenue"]) for r in records)

Um eine beliebige Anzahl von Feldern zu verallgemeinern, erstellen wir zuerst eine Zeilenzeichenfolge mit der richtigen Anzahl von Registerkarten und Feldplatzhaltern: "{}\t{}\t{}....\t{}"und geben dann .format()die Feldwerte ein *list(r.values())) for r in records::

        line = "\t".join(["{}"] * len(records[0]))

        f = IteratorFile(line.format(*list(r.values())) for r in records)

vollständige Funktion im Kern hier .

Bart Jonk
quelle
0

Wenn Sie SQLAlchemy verwenden, müssen Sie sich nicht mit der Handarbeit der Zeichenfolge herumschlagen, da SQLAlchemy das Generieren einer mehrzeiligen VALUESKlausel für eine einzelne INSERTAnweisung unterstützt :

rows = []
for i, name in enumerate(rawdata):
    row = {
        'id': i,
        'name': name,
        'valid': True,
    }
    rows.append(row)
if len(rows) > 0:  # INSERT fails if no rows
    insert_query = SQLAlchemyModelName.__table__.insert().values(rows)
    session.execute(insert_query)
Jeff Widman
quelle
Unter der Haube verwendet SQLAlchemy für solche Anrufe die execememany () von psychopg2. Daher hat diese Antwort schwerwiegende Leistungsprobleme bei großen Abfragen. Siehe die Ausführungsmethode docs.sqlalchemy.org/en/latest/orm/session_api.html .
sage88
2
Ich glaube nicht, dass das der Fall ist. Es ist ein bisschen her, seit ich mir das angeschaut habe, aber IIRC, das baut tatsächlich eine einzelne Einfügeanweisung in der insert_queryZeile auf. Dann session.execute()wird nur die Anweisung von psycopg2 execute()mit einer einzigen massiven Zeichenfolge aufgerufen . Der "Trick" besteht also darin, zuerst das gesamte Objekt der Einfügeanweisung zu erstellen. Ich verwende dies, um 200.000 Zeilen gleichzeitig einzufügen, und habe mit diesem Code massive Leistungssteigerungen im Vergleich zum Normalcode festgestellt executemany().
Jeff Widman
1
Das SQLAlchemy-Dokument, mit dem Sie verlinkt haben, enthält einen Abschnitt, der genau zeigt, wie dies funktioniert, und der sogar sagt: "Es ist wichtig zu beachten, dass das Übergeben mehrerer Werte NICHT mit der Verwendung des herkömmlichen Formulars executemany () identisch ist." Es wird also ausdrücklich darauf hingewiesen, dass dies funktioniert.
Jeff Widman
1
Ich stehe korrigiert. Ich habe Ihre Verwendung der values ​​() -Methode nicht bemerkt (ohne sie führt SQLAlchemy nur viele ausführende Dateien aus). Ich würde sagen, bearbeiten Sie die Antwort, um einen Link zu diesem Dokument einzufügen, damit ich meine Stimme ändern kann, aber offensichtlich haben Sie ihn bereits eingefügt. Erwähnen Sie vielleicht, dass dies nicht dasselbe ist wie das Aufrufen von insert () mit execute () mit einer Liste von Diktaten?
sage88
Wie funktioniert es im Vergleich zu execute_values?
MrR
0

execute_batch wurde zu psycopg2 hinzugefügt, seit diese Frage gestellt wurde.

Es ist langsamer als execute_values, aber einfacher zu verwenden.

gerardw
quelle
2
Siehe andere Kommentare. Die Methode von psycopg2 execute_valuesist schneller alsexecute_batch
Fierr
0

Executemany akzeptieren Array von Tupeln

https://www.postgresqltutorial.com/postgresql-python/insert/

    """ array of tuples """
    vendor_list = [(value1,)]

    """ insert multiple vendors into the vendors table  """
    sql = "INSERT INTO vendors(vendor_name) VALUES(%s)"
    conn = None
    try:
        # read database configuration
        params = config()
        # connect to the PostgreSQL database
        conn = psycopg2.connect(**params)
        # create a new cursor
        cur = conn.cursor()
        # execute the INSERT statement
        cur.executemany(sql,vendor_list)
        # commit the changes to the database
        conn.commit()
        # close communication with the database
        cur.close()
    except (Exception, psycopg2.DatabaseError) as error:
        print(error)
    finally:
        if conn is not None:
            conn.close()
Grigory
quelle
-1

Wenn Sie mehrere Zeilen in eine Einfügestatistik einfügen möchten (vorausgesetzt, Sie verwenden kein ORM), ist es für mich bisher am einfachsten, eine Liste von Wörterbüchern zu verwenden. Hier ist ein Beispiel:

 t = [{'id':1, 'start_date': '2015-07-19 00:00:00', 'end_date': '2015-07-20 00:00:00', 'campaignid': 6},
      {'id':2, 'start_date': '2015-07-19 00:00:00', 'end_date': '2015-07-20 00:00:00', 'campaignid': 7},
      {'id':3, 'start_date': '2015-07-19 00:00:00', 'end_date': '2015-07-20 00:00:00', 'campaignid': 8}]

conn.execute("insert into campaign_dates
             (id, start_date, end_date, campaignid) 
              values (%(id)s, %(start_date)s, %(end_date)s, %(campaignid)s);",
             t)

Wie Sie sehen, wird nur eine Abfrage ausgeführt:

INFO sqlalchemy.engine.base.Engine insert into campaign_dates (id, start_date, end_date, campaignid) values (%(id)s, %(start_date)s, %(end_date)s, %(campaignid)s);
INFO sqlalchemy.engine.base.Engine [{'campaignid': 6, 'id': 1, 'end_date': '2015-07-20 00:00:00', 'start_date': '2015-07-19 00:00:00'}, {'campaignid': 7, 'id': 2, 'end_date': '2015-07-20 00:00:00', 'start_date': '2015-07-19 00:00:00'}, {'campaignid': 8, 'id': 3, 'end_date': '2015-07-20 00:00:00', 'start_date': '2015-07-19 00:00:00'}]
INFO sqlalchemy.engine.base.Engine COMMIT
Alex
quelle
Das Anzeigen der Protokollierung von der sqlalchemy-Engine ist KEINE Demonstration, dass nur eine einzelne Abfrage ausgeführt wird. Dies bedeutet lediglich, dass die sqlalchemy-Engine einen Befehl ausgeführt hat. Unter der Haube wird die ausführende Firma von psychopg2 verwendet, die sehr ineffizient ist. Siehe die Ausführungsmethode docs.sqlalchemy.org/en/latest/orm/session_api.html .
sage88
-3

Verwenden von Aiopg - Das folgende Snippet funktioniert einwandfrei

    # items = [10, 11, 12, 13]
    # group = 1
    tup = [(gid, pid) for pid in items]
    args_str = ",".join([str(s) for s in tup])
    # insert into group values (1, 10), (1, 11), (1, 12), (1, 13)
    yield from cur.execute("INSERT INTO group VALUES " + args_str)
Nihal Sharma
quelle
10
Diese Methode ist vor SQL-Injektionen nicht sicher. In der psycopg2-Dokumentation heißt es (auf die aiopg2 verweist ): '
Michał Pawłowski
-4

Schließlich wird in der SQLalchemy1.2-Version diese neue Implementierung hinzugefügt, um psycopg2.extras.execute_batch () anstelle von executemany zu verwenden, wenn Sie Ihre Engine mit use_batch_mode = True initialisieren, wie:

engine = create_engine(
    "postgresql+psycopg2://scott:tiger@host/dbname",
    use_batch_mode=True)

http://docs.sqlalchemy.org/en/latest/changelog/migration_12.html#change-4109

Dann müsste jemand SQLalchmey verwenden, um verschiedene Kombinationen von sqla und psycopg2 auszuprobieren und SQL gemeinsam zu steuern.

user2189731
quelle