Warum ist SQLAlchemy Insert mit SQLite 25-mal langsamer als die direkte Verwendung von SQLite3?

81

Warum fügt dieser einfache Testfall mit SQLAlchemy 100.000 Zeilen 25-mal langsamer ein als mit dem sqlite3-Treiber direkt? Ich habe ähnliche Verlangsamungen in realen Anwendungen gesehen. Mache ich etwas falsch?

#!/usr/bin/env python
# Why is SQLAlchemy with SQLite so slow?
# Output from this program:
# SqlAlchemy: Total time for 100000 records 10.74 secs
# sqlite3:    Total time for 100000 records  0.40 secs


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

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

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

def test_sqlalchemy(n=100000):
    init_sqlalchemy()
    t0 = time.time()
    for i in range(n):
        customer = Customer()
        customer.name = 'NAME ' + str(i)
        DBSession.add(customer)
    DBSession.commit()
    print "SqlAlchemy: 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 range(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 __name__ == '__main__':
    test_sqlalchemy(100000)
    test_sqlite3(100000)

Ich habe zahlreiche Variationen ausprobiert (siehe http://pastebin.com/zCmzDraU ).

Braddock
quelle

Antworten:

188

Das SQLAlchemy ORM verwendet das Arbeitseinheitsmuster beim Synchronisieren von Änderungen an der Datenbank. Dieses Muster geht weit über einfache "Einfügungen" von Daten hinaus. Es beinhaltet, dass Attribute, die Objekten zugewiesen sind, unter Verwendung eines Attributinstrumentierungssystems empfangen werden, das Änderungen an Objekten verfolgt, während sie vorgenommen werden, und dass alle eingefügten Zeilen in einer Identitätszuordnung verfolgt werdenDies hat zur Folge, dass SQLAlchemy für jede Zeile die "zuletzt eingefügte ID" abrufen muss, sofern dies nicht bereits angegeben ist, und dass die einzufügenden Zeilen nach Bedarf gescannt und nach Abhängigkeiten sortiert werden. Objekte unterliegen auch einem angemessenen Maß an Buchhaltung, um all dies am Laufen zu halten, was für eine sehr große Anzahl von Zeilen gleichzeitig einen übermäßig langen Zeitaufwand für große Datenstrukturen verursachen kann. Daher ist es am besten, diese zu zerlegen.

Grundsätzlich ist die Arbeitseinheit ein großer Automatisierungsgrad, um die Aufgabe zu automatisieren, ein komplexes Objektdiagramm in einer relationalen Datenbank ohne expliziten Persistenzcode zu speichern, und diese Automatisierung hat ihren Preis.

ORMs sind daher grundsätzlich nicht für Hochleistungs-Bulk-Einsätze gedacht. Dies ist der ganze Grund, warum SQLAlchemy über zwei separate Bibliotheken verfügt. Wenn Sie sich http://docs.sqlalchemy.org/en/latest/index.html ansehen, sehen Sie zwei unterschiedliche Hälften der Indexseite. eine für das ORM und eine für den Core. Sie können SQLAlchemy nicht effektiv einsetzen, ohne beide zu verstehen.

Für den Anwendungsfall schneller Masseneinfügungen stellt SQLAlchemy den Kern bereit , bei dem es sich um das SQL-Generierungs- und Ausführungssystem handelt, auf dem der ORM aufbaut. Mit diesem System können wir effektiv ein INSERT erstellen, das mit der SQLite-Rohversion konkurriert. Das folgende Skript veranschaulicht dies sowie eine ORM-Version, die Primärschlüssel-IDs vorab zuweist, damit der ORM Executemany () zum Einfügen von Zeilen verwenden kann. Beide ORM-Versionen teilen die Flushes ebenfalls auf 1000 Datensätze gleichzeitig, was erhebliche Auswirkungen auf die Leistung hat.

Die hier beobachteten Laufzeiten sind:

SqlAlchemy ORM: Total time for 100000 records 16.4133379459 secs
SqlAlchemy ORM pk given: Total time for 100000 records 9.77570986748 secs
SqlAlchemy Core: Total time for 100000 records 0.568737983704 secs
sqlite3: Total time for 100000 records 0.595796823502 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())

class Customer(Base):
    __tablename__ = "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 range(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 range(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_core(n=100000):
    init_sqlalchemy()
    t0 = time.time()
    engine.execute(
        Customer.__table__.insert(),
        [{"name":'NAME ' + str(i)} for i in range(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 range(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 __name__ == '__main__':
    test_sqlalchemy_orm(100000)
    test_sqlalchemy_orm_pk_given(100000)
    test_sqlalchemy_core(100000)
    test_sqlite3(100000)

Siehe auch: http://docs.sqlalchemy.org/en/latest/faq/performance.html

zzzeek
quelle
Danke für die Erklärung. Unterscheidet sich engine.execute () signifikant von DBSession.execute ()? Ich hatte versucht, einen Einfügungsausdruck mit DBSession.execute () einzufügen, aber er war nicht wesentlich schneller als die vollständige ORM-Version.
Braddock
4
engine.execute () und DBSession.execute () sind größtenteils identisch, außer dass DBSession.execute () eine bestimmte einfache SQL-Zeichenfolge in text () umschließt. Es macht einen großen Unterschied, ob Sie die Syntax execute / executeemany verwenden. pysqlite ist vollständig in C geschrieben und hat fast keine Latenz, sodass jeder Python-Overhead, der dem Aufruf execute () hinzugefügt wird, in der Profilerstellung spürbar angezeigt wird. Selbst ein einzelner reiner Python-Funktionsaufruf ist erheblich langsamer als ein reiner C-Funktionsaufruf wie pysqlites execute (). Sie müssen auch berücksichtigen, dass SQLAlchemy-Ausdruckskonstrukte pro execute () -Aufruf einen Kompilierungsschritt durchlaufen.
Zzzeek
3
Der Kern wurde zuerst erstellt, obwohl nach den ersten Wochen, nachdem der Core Proof of Concept funktioniert hatte (und es schrecklich war ), ORM und Kern von diesem Zeitpunkt an parallel entwickelt wurden.
Zzzeek
2
Ich weiß wirklich nicht, warum sich dann jemand für das ORM-Modell entscheiden würde. Die meisten Projekte, die eine Datenbank verwenden, haben +10.000 Zeilen. Das Beibehalten von zwei Aktualisierungsmethoden (eine für einzelne Zeilen und eine für Massen) klingt einfach nicht klug.
Peter Moore
5
wird ... 10000 Zeilen haben, die sie alle auf einmal in großen Mengen die ganze Zeit einfügen müssen? nicht besonders. Die überwiegende Mehrheit der Webanwendungen tauscht beispielsweise wahrscheinlich ein halbes Dutzend Zeilen pro Anforderung aus. Das ORM ist bei einigen sehr bekannten und stark frequentierten Websites sehr beliebt.
Zzzeek
21

Hervorragende Antwort von @zzzeek. Für diejenigen, die sich über dieselben Statistiken für Abfragen wundern, habe ich den @ zzezeek-Code leicht geändert, um dieselben Datensätze direkt nach dem Einfügen abzufragen und diese Datensätze dann in eine Liste von Diktaten zu konvertieren.

Hier sind die Ergebnisse

SqlAlchemy ORM: Total time for 100000 records 11.9210000038 secs
SqlAlchemy ORM query: Total time for 100000 records 2.94099998474 secs
SqlAlchemy ORM pk given: Total time for 100000 records 7.51800012589 secs
SqlAlchemy ORM pk given query: Total time for 100000 records 3.07699990273 secs
SqlAlchemy Core: Total time for 100000 records 0.431999921799 secs
SqlAlchemy Core query: Total time for 100000 records 0.389000177383 secs
sqlite3: Total time for 100000 records 0.459000110626 sec
sqlite3 query: Total time for 100000 records 0.103999853134 secs

Interessanterweise ist das Abfragen mit Bare SQLite3 immer noch etwa dreimal schneller als mit SQLAlchemy Core. Ich denke, das ist der Preis, den Sie für die Rückgabe eines ResultProxy anstelle einer bloßen sqlite3-Zeile zahlen.

SQLAlchemy Core ist ungefähr 8-mal schneller als ORM. Das Abfragen mit ORM ist also viel langsamer, egal was passiert.

Hier ist der Code, den ich verwendet habe:

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
from sqlalchemy.sql import select

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

class Customer(Base):
    __tablename__ = "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 range(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"
    t0 = time.time()
    q = DBSession.query(Customer)
    dict = [{'id':r.id, 'name':r.name} for r in q]
    print "SqlAlchemy ORM query: Total time for " + str(len(dict)) + " records " + str(time.time() - t0) + " secs"


def test_sqlalchemy_orm_pk_given(n=100000):
    init_sqlalchemy()
    t0 = time.time()
    for i in range(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"
    t0 = time.time()
    q = DBSession.query(Customer)
    dict = [{'id':r.id, 'name':r.name} for r in q]
    print "SqlAlchemy ORM pk given query: Total time for " + str(len(dict)) + " 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 range(n)]
    )
    print "SqlAlchemy Core: Total time for " + str(n) + " records " + str(time.time() - t0) + " secs"
    conn = engine.connect()
    t0 = time.time()
    sql = select([Customer.__table__])
    q = conn.execute(sql)
    dict = [{'id':r[0], 'name':r[0]} for r in q]
    print "SqlAlchemy Core query: Total time for " + str(len(dict)) + " 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 range(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"
    t0 = time.time()
    q = conn.execute("SELECT * FROM customer").fetchall()
    dict = [{'id':r[0], 'name':r[0]} for r in q]
    print "sqlite3 query: Total time for " + str(len(dict)) + " records " + str(time.time() - t0) + " secs"


if __name__ == '__main__':
    test_sqlalchemy_orm(100000)
    test_sqlalchemy_orm_pk_given(100000)
    test_sqlalchemy_core(100000)
    test_sqlite3(100000)

Ich habe auch getestet, ohne das Abfrageergebnis in Diktate umzuwandeln, und die Statistiken sind ähnlich:

SqlAlchemy ORM: Total time for 100000 records 11.9189999104 secs
SqlAlchemy ORM query: Total time for 100000 records 2.78500008583 secs
SqlAlchemy ORM pk given: Total time for 100000 records 7.67199993134 secs
SqlAlchemy ORM pk given query: Total time for 100000 records 2.94000005722 secs
SqlAlchemy Core: Total time for 100000 records 0.43700003624 secs
SqlAlchemy Core query: Total time for 100000 records 0.131000041962 secs
sqlite3: Total time for 100000 records 0.500999927521 sec
sqlite3 query: Total time for 100000 records 0.0859999656677 secs

Das Abfragen mit SQLAlchemy Core ist im Vergleich zu ORM etwa 20-mal schneller.

Es ist wichtig zu beachten, dass diese Tests sehr oberflächlich sind und nicht zu ernst genommen werden sollten. Möglicherweise fehlen mir einige offensichtliche Tricks, die die Statistiken vollständig ändern könnten.

Der beste Weg, um Leistungsverbesserungen zu messen, ist direkt in Ihrer eigenen Anwendung. Nimm meine Statistiken nicht als selbstverständlich an.

Alex
quelle
Ich wollte Sie nur wissen lassen, dass ich 2019 mit den neuesten Versionen von allem keine signifikanten relativen Abweichungen von Ihren Timings feststelle. Trotzdem bin ich auch neugierig, ob ein "Trick" übersehen wird.
PascalVKooten
0

Ich würde den Insert-Expression- Test und dann den Benchmark versuchen .

Es wird wahrscheinlich immer noch langsamer sein, weil der OP-Mapper-Overhead ist, aber ich würde hoffen, dass es nicht viel langsamer ist.

Würde es Ihnen etwas ausmachen, Ergebnisse zu versuchen und zu veröffentlichen? Das ist sehr interessantes Zeug.

Edmon
quelle
1
Nur 10% schneller mit einem Insert-Ausdruck. Ich wünschte, ich wüsste warum: SqlAlchemy Einfügen: Gesamtzeit für 100000 Datensätze 9,47 Sekunden
Braddock
Nicht um Sie damit zu nerven, aber wenn Sie interessiert sind, können Sie den Code für die DB-Sitzung nach den Einfügungen und unter Verwendung von Timit zeitlich festlegen. docs.python.org/library/timeit.html
Edmon
Ich habe das gleiche Problem mit dem Insert-Ausdruck, er ist absolut langsam, siehe stackoverflow.com/questions/11887895/…
dorvak