it-swarm.com.de

Masseneinfügung mit SQLAlchemy ORM

Gibt es eine Möglichkeit, SQLAlchemy dazu zu bringen, eine Masseneinfügung durchzuführen, anstatt jedes einzelne Objekt einzufügen. d.h.

tun:

INSERT INTO `foo` (`bar`) VALUES (1), (2), (3)

eher, als:

INSERT INTO `foo` (`bar`) VALUES (1)
INSERT INTO `foo` (`bar`) VALUES (2)
INSERT INTO `foo` (`bar`) VALUES (3)

Ich habe gerade etwas Code konvertiert, um sqlalchemy anstelle von raw sql zu verwenden, und obwohl es jetzt viel schöner ist, damit zu arbeiten, scheint es jetzt langsamer zu sein (bis zu einem Faktor von 10), frage ich mich, ob dies der Grund ist.

Möglicherweise könnte ich die Situation verbessern, indem Sitzungen effizienter genutzt werden. Im Moment habe ich autoCommit=False und mache eine session.commit() nachdem ich ein paar Sachen hinzugefügt habe. Obwohl dies scheinbar dazu führt, dass die Daten veraltet werden, wenn die Datenbank an anderer Stelle geändert wird. Wie auch wenn ich eine neue Abfrage stelle, bekomme ich immer noch alte Ergebnisse zurück.

Danke für Ihre Hilfe!

88
Nick Holden

SQLAlchemy hat das in Version 1.0.0 eingeführt:

Massenoperationen - SQLAlchemy-Dokumente

Mit diesen Vorgängen können Sie jetzt Bulk-Inserts oder Updates durchführen!

Zum Beispiel können Sie Folgendes tun:

s = Session()
objects = [
    User(name="u1"),
    User(name="u2"),
    User(name="u3")
]
s.bulk_save_objects(objects)
s.commit()

Hier wird ein Bulk Insert gemacht.

112
Pierre

Soweit ich weiß, gibt es keine Möglichkeit, den ORM dazu zu bringen, Masseneinlagen auszustellen. Ich glaube, der zugrunde liegende Grund ist, dass SQLAlchemy die Identität jedes Objekts (d. H. Neue Primärschlüssel) nachverfolgen muss, und Masseneinfügungen stören dies. Angenommen, Ihre foo-Tabelle enthält eine id-Spalte und ist einer Foo-Klasse zugeordnet:

x = Foo(bar=1)
print x.id
# None
session.add(x)
session.flush()
# BEGIN
# INSERT INTO foo (bar) VALUES(1)
# COMMIT
print x.id
# 1

Da SQLAlchemy den Wert für x.id übernommen hat, ohne eine weitere Abfrage auszuführen, können wir daraus schließen, dass der Wert direkt von der INSERT-Anweisung abgerufen wurde. Wenn Sie keinen späteren Zugriff auf die erstellten Objekte über die same -Instanzen benötigen, können Sie die ORM-Ebene für Ihre Einfügung überspringen:

Foo.__table__.insert().execute([{'bar': 1}, {'bar': 2}, {'bar': 3}])
# INSERT INTO foo (bar) VALUES ((1,), (2,), (3,))

SQLAlchemy kann diese neuen Zeilen nicht mit vorhandenen Objekten abgleichen. Daher müssen Sie sie für alle nachfolgenden Vorgänge erneut abfragen.

In Bezug auf veraltete Daten ist es hilfreich zu wissen, dass die Sitzung keine integrierte Möglichkeit hat, zu wissen, wann die Datenbank außerhalb der Sitzung geändert wird. Um auf extern modifizierte Daten über vorhandene Instanzen zugreifen zu können, müssen die Instanzen als abgelaufen gekennzeichnet sein. Dies geschieht standardmäßig für session.commit(), kann aber manuell durch Aufruf von session.expire_all() oder session.expire(instance) erfolgen. Ein Beispiel (ohne SQL):

x = Foo(bar=1)
session.add(x)
session.commit()
print x.bar
# 1
foo.update().execute(bar=42)
print x.bar
# 1
session.expire(x)
print x.bar
# 42

session.commit() verfällt x, daher öffnet die erste Druckanweisung implizit eine neue Transaktion und fragt die Attribute der x erneut ab. Wenn Sie die erste Druckanweisung auskommentieren, werden Sie feststellen, dass die zweite Anweisung nun den korrekten Wert aufnimmt, da die neue Abfrage erst nach der Aktualisierung ausgegeben wird.

Dies ist unter dem Gesichtspunkt der Transaktionsisolation sinnvoll - Sie sollten nur externe Modifikationen zwischen Transaktionen berücksichtigen. Wenn dies zu Problemen führt, empfiehlt es sich, die Transaktionsgrenzen Ihrer Anwendung zu klären oder zu überdenken, anstatt sofort nach session.expire_all() zu greifen.

27
dhaffey

In den sqlalchemy-Dokumenten wird die Leistung verschiedener Techniken, die für Masseneinfügungen verwendet werden können, groß geschrieben:

ORMs sind grundsätzlich nicht für Hochleistungs-Bulk-Inserts -.__ gedacht. Aus diesem Grund bietet SQLAlchemy den Core zusätzlich zu .__ an. ORM als erstklassige Komponente.

Für den Anwendungsfall schneller Masseneinfügungen die SQL-Generierung und Das Ausführungssystem, auf dem der ORM aufbaut, ist Teil des Core . Wenn Sie dieses System direkt verwenden, können wir ein INSERT erstellen, das heißt konkurrenzfähig mit der direkten Verwendung der Rohdatenbank-API.

Alternativ bietet der SQLAlchemy-ORM die Bulk Operations-Suite von Methoden, die Hooks in Unterabschnitten der Arbeitseinheit bereitstellen Prozess, um INSERT- und UPDATE-Konstrukte auf Core-Ebene mit .__ auszugeben. ein kleines Maß an ORM-basierter Automatisierung.

Das folgende Beispiel veranschaulicht zeitbasierte Tests für mehrere verschiedene Methoden zum Einfügen von Zeilen, von den am wenigsten automatisierten zum kleinsten . Bei cPython 2.7 wurden Laufzeiten beobachtet:

classics-MacBook-Pro:sqlalchemy classic$ python test.py
SQLAlchemy ORM: Total time for 100000 records 12.0471920967 secs
SQLAlchemy ORM pk given: Total time for 100000 records 7.06283402443 secs
SQLAlchemy ORM bulk_save_objects(): Total time for 100000 records 0.856323003769 secs
SQLAlchemy Core: Total time for 100000 records 0.485800027847 secs
sqlite3: Total time for 100000 records 0.487842082977 sec

Skript:

import time
import sqlite3

from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, String,  create_engine
from sqlalchemy.orm import scoped_session, sessionmaker

Base = declarative_base()
DBSession = scoped_session(sessionmaker())
engine = None


class Customer(Base):
    __table= "customer"
    id = Column(Integer, primary_key=True)
    name = Column(String(255))


def init_sqlalchemy(dbname='sqlite:///sqlalchemy.db'):
    global engine
    engine = create_engine(dbname, echo=False)
    DBSession.remove()
    DBSession.configure(bind=engine, autoflush=False, expire_on_commit=False)
    Base.metadata.drop_all(engine)
    Base.metadata.create_all(engine)


def test_sqlalchemy_orm(n=100000):
    init_sqlalchemy()
    t0 = time.time()
    for i in xrange(n):
        customer = Customer()
        customer.name = 'NAME ' + str(i)
        DBSession.add(customer)
        if i % 1000 == 0:
            DBSession.flush()
    DBSession.commit()
    print(
        "SQLAlchemy ORM: Total time for " + str(n) +
        " records " + str(time.time() - t0) + " secs")


def test_sqlalchemy_orm_pk_given(n=100000):
    init_sqlalchemy()
    t0 = time.time()
    for i in xrange(n):
        customer = Customer(id=i+1, name="NAME " + str(i))
        DBSession.add(customer)
        if i % 1000 == 0:
            DBSession.flush()
    DBSession.commit()
    print(
        "SQLAlchemy ORM pk given: Total time for " + str(n) +
        " records " + str(time.time() - t0) + " secs")


def test_sqlalchemy_orm_bulk_insert(n=100000):
    init_sqlalchemy()
    t0 = time.time()
    n1 = n
    while n1 > 0:
        n1 = n1 - 10000
        DBSession.bulk_insert_mappings(
            Customer,
            [
                dict(name="NAME " + str(i))
                for i in xrange(min(10000, n1))
            ]
        )
    DBSession.commit()
    print(
        "SQLAlchemy ORM bulk_save_objects(): Total time for " + str(n) +
        " records " + str(time.time() - t0) + " secs")


def test_sqlalchemy_core(n=100000):
    init_sqlalchemy()
    t0 = time.time()
    engine.execute(
        Customer.__table__.insert(),
        [{"name": 'NAME ' + str(i)} for i in xrange(n)]
    )
    print(
        "SQLAlchemy Core: Total time for " + str(n) +
        " records " + str(time.time() - t0) + " secs")


def init_sqlite3(dbname):
    conn = sqlite3.connect(dbname)
    c = conn.cursor()
    c.execute("DROP TABLE IF EXISTS customer")
    c.execute(
        "CREATE TABLE customer (id INTEGER NOT NULL, "
        "name VARCHAR(255), PRIMARY KEY(id))")
    conn.commit()
    return conn


def test_sqlite3(n=100000, dbname='sqlite3.db'):
    conn = init_sqlite3(dbname)
    c = conn.cursor()
    t0 = time.time()
    for i in xrange(n):
        row = ('NAME ' + str(i),)
        c.execute("INSERT INTO customer (name) VALUES (?)", row)
    conn.commit()
    print(
        "sqlite3: Total time for " + str(n) +
        " records " + str(time.time() - t0) + " sec")

if __== '__main__':
    test_sqlalchemy_orm(100000)
    test_sqlalchemy_orm_pk_given(100000)
    test_sqlalchemy_orm_bulk_insert(100000)
    test_sqlalchemy_core(100000)
    test_sqlite3(100000)
16
Grant Humphries

Normalerweise mache ich es mit add_all .

from app import session
from models import User

objects = [User(name="u1"), User(name="u2"), User(name="u3")]
session.add_all(objects)
session.commit()
12
reubano

SQLAlchemy wurde ab Version 0.8 direkt unterstützt

Laut docs sollte connection.execute(table.insert().values(data)) den Trick tun. (Beachten Sie, dass dies nicht dasselbe ist wie connection.execute(table.insert(), data), was zu vielen einzelnen Zeileneinfügungen über einen Aufruf von executemany führt). Bei etwas anderem als einer lokalen Verbindung kann der Leistungsunterschied enorm sein.

9
user3805082

SQLAlchemy hat das in Version 1.0.0 eingeführt:

Massenoperationen - SQLAlchemy-Dokumente

Mit diesen Vorgängen können Sie jetzt Bulk-Inserts oder Updates durchführen!

Zum Beispiel (wenn Sie den niedrigsten Aufwand für einfache Tabellen-INSERTs wünschen), können Sie Session.bulk_insert_mappings() verwenden:

loadme = [
        (1, 'a')
    ,   (2, 'b')
    ,   (3, 'c')
    ]

dicts = []
for i in range(len(loadme)):
    dicts.append(dict(bar=loadme[i][0], fly=loadme[i][1]))

s = Session()
s.bulk_insert_mappings(Foo, dicts)
s.commit()

Wenn Sie möchten, überspringen Sie die loadme-Tupel und schreiben Sie die Wörterbücher direkt in dicts.

6
juanitogan

Die Antwort von Piere ist korrekt, aber ein Problem ist, dass bulk_save_objects standardmäßig die Primärschlüssel der Objekte nicht zurückgibt, wenn dies für Sie von Belang ist. Setzen Sie return_defaults auf True, um dieses Verhalten zu erhalten.

Die Dokumentation ist hier .

foos = [Foo(bar='a',), Foo(bar='b'), Foo(bar='c')]
session.bulk_save_objects(foos, return_defaults=True)
for foo in foos:
    assert foo.id is not None
session.commit()
5
Matthew Moisen

Dies ist ein Weg:

values = [1, 2, 3]
Foo.__table__.insert().execute([{'bar': x} for x in values])

Dies wird wie folgt eingefügt:

INSERT INTO `foo` (`bar`) VALUES (1), (2), (3)

Referenz: Die SQLAlchemy FAQ enthält Benchmarks für verschiedene Commit-Methoden.

4
Eefret

Alle Wege führen nach Rom, aber einige von ihnen überqueren die Berge. Sie benötigen Fähren. Wenn Sie jedoch schnell dorthin gelangen möchten, nehmen Sie die Autobahn.


In diesem Fall muss die Autobahn die Funktion execute_batch () von psycopg2 verwenden. Die Dokumentation sagt es am besten:

Die derzeitige Implementierung von executemany() ist (unter Verwendung einer äußerst karitativen Untertreibung) nicht besonders leistungsfähig. Mit diesen Funktionen kann die wiederholte Ausführung einer Anweisung anhand eines Parametersatzes beschleunigt werden. Durch die Reduzierung der Anzahl der Server-Roundtrips kann die Leistung um Größenordnungen besser sein als mit executemany().

In meinem Test ist execute_batch()ungefähr doppelt so schnell wie executemany() und gibt die Option an, page_size für weitere Anpassungen zu konfigurieren (wenn Sie die letzten 2-3% der Leistung aus dem Treiber herausdrücken möchten).

Die gleiche Funktion kann einfach aktiviert werden, wenn Sie SQLAlchemy verwenden, indem Sie use_batch_mode=True als Parameter festlegen, wenn Sie die Engine mit create_engine() instanziieren.

4
chjortlund

Die beste Antwort, die ich bisher gefunden habe, war in der Dokumentation zu sqlalchemy:

http://docs.sqlalchemy.org/de/latest/faq/performance.html#i-m-inserting-400-000-rows-with-the-orm-and-it-sreally-slow

Es gibt ein vollständiges Beispiel für einen Benchmark möglicher Lösungen.

Wie in der Dokumentation gezeigt:

bulk_save_objects ist nicht die beste Lösung, aber die Leistung stimmt.

Die zweitbeste Implementierung in Bezug auf die Lesbarkeit war meines Erachtens der SQLAlchemy Core:

def test_sqlalchemy_core(n=100000):
    init_sqlalchemy()
    t0 = time.time()
    engine.execute(
        Customer.__table__.insert(),
            [{"name": 'NAME ' + str(i)} for i in xrange(n)]
    )

Der Kontext dieser Funktion ist im Dokumentationsartikel angegeben.

0
lelabo_m