Dlaczego SQLAlchemy wstawia z sqlite 25 razy wolniej niż bezpośrednio przy użyciu sqlite3?

81

Dlaczego ten prosty przypadek testowy wstawia 100 000 wierszy 25 razy wolniej za pomocą SQLAlchemy niż bezpośrednio przy użyciu sterownika sqlite3? Widziałem podobne spowolnienia w rzeczywistych aplikacjach. czy robię coś źle?

#!/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)

Wypróbowałem wiele odmian (patrz http://pastebin.com/zCmzDraU )

Braddock
źródło

Odpowiedzi:

189

ORM SQLAlchemy używa wzorca jednostki pracy podczas synchronizowania zmian w bazie danych. Ten wzorzec wykracza daleko poza zwykłe „wstawianie” danych. Obejmuje to, że atrybuty przypisane do obiektów są odbierane za pomocą systemu oprzyrządowania atrybutów, który śledzi zmiany w obiektach w trakcie ich tworzenia, w tym wszystkie wstawione wiersze są śledzone na mapie tożsamościco powoduje, że dla każdego wiersza SQLAlchemy musi pobrać swój „ostatnio wstawiony identyfikator”, jeśli nie został jeszcze podany, a także powoduje, że wiersze do wstawienia są skanowane i sortowane pod kątem zależności w razie potrzeby. Obiekty podlegają również dość dużej księgowości, aby wszystko to działało, co w przypadku bardzo dużej liczby wierszy naraz może powodować nadmierną ilość czasu spędzanego z dużymi strukturami danych, dlatego najlepiej jest je podzielić.

Zasadniczo jednostka pracy to duży stopień automatyzacji w celu zautomatyzowania zadania utrwalania złożonego grafu obiektowego w relacyjnej bazie danych bez jawnego kodu trwałości, a ta automatyzacja ma swoją cenę.

Dlatego też ORM zasadniczo nie są przeznaczone do wysokowydajnych wkładek masowych. To jest cały powód, dla którego SQLAlchemy ma dwie oddzielne biblioteki, o których zauważysz, jeśli spojrzysz na http://docs.sqlalchemy.org/en/latest/index.html , zobaczysz dwie odrębne połówki strony indeksu - jeden dla ORM i jeden dla Core. Nie można efektywnie używać SQLAlchemy bez zrozumienia obu.

W przypadku użycia szybkiego wstawiania zbiorczego SQLAlchemy zapewnia rdzeń , czyli system generowania i wykonywania kodu SQL, na którym opiera się ORM. Korzystając z tego systemu skutecznie, możemy stworzyć INSERT, który jest konkurencyjny w stosunku do surowej wersji SQLite. Poniższy skrypt ilustruje to, a także wersję ORM, która wstępnie przypisuje identyfikatory kluczy podstawowych, dzięki czemu ORM może używać funkcji executemany () do wstawiania wierszy. Obie wersje ORM również dzielą rzuty na 1000 rekordów na raz, co ma znaczący wpływ na wydajność.

Obserwowane tutaj okresy działania to:

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

scenariusz:

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)

Zobacz także: http://docs.sqlalchemy.org/en/latest/faq/performance.html

zzzeek
źródło
Dziękuję za wyjaśnienie. Czy engine.execute () znacząco różni się od DBSession.execute ()? Próbowałem wstawić wyrażenie przy użyciu DBSession.execute (), ale nie było to znacznie szybsze niż pełna wersja ORM.
Braddock
4
engine.execute () i DBSession.execute () są w większości takie same, z wyjątkiem tego, że DBSession.execute () zawija dany zwykły ciąg SQL w text (). To robi ogromną różnicę, jeśli używasz składni execute / executemany. pysqlite jest napisany w całości w C i nie ma prawie żadnych opóźnień, więc każdy narzut Pythona dodany do jego wywołania execute () będzie widoczny w profilowaniu. Nawet pojedyncze wywołanie funkcji w czystym Pythonie jest znacznie wolniejsze niż wywołanie czystej funkcji C, takie jak execute () pysqlite. Należy również wziąć pod uwagę, że konstrukcje wyrażeń SQLAlchemy przechodzą przez krok kompilacji na każde wywołanie funkcji execute ().
zzzeek
3
rdzeń został stworzony jako pierwszy, chociaż po kilku pierwszych tygodniach, gdy rdzeń sprawdził koncepcję (i to było straszne ), ORM i rdzeń zostały opracowane równolegle od tego momentu.
zzzeek
2
Naprawdę nie wiem, dlaczego ktoś miałby wtedy wybrać model ORM. Większość projektów korzystających z bazy danych będzie miała ponad 10000 wierszy. utrzymywanie 2 metod aktualizacji (jednej dla pojedynczego wiersza i jednej dla zbiorczej) po prostu nie brzmi mądrze.
Peter Moore,
5
będzie mieć .... 10000 wierszy, które będą musiały wstawiać wszystkie naraz przez cały czas? niezbyt. na przykład ogromna większość aplikacji internetowych prawdopodobnie wymienia pół tuzina wierszy na żądanie. ORM jest dość popularny w niektórych bardzo znanych witrynach o dużym ruchu.
zzzeek
21

Doskonała odpowiedź od @zzzeek. Dla tych, którzy zastanawiają się nad tymi samymi statystykami dla zapytań, zmodyfikowałem nieco kod @zzzeek, ​​aby wyszukiwać te same rekordy zaraz po ich wstawieniu, a następnie przekonwertować te rekordy na listę dykt.

Oto wyniki

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

Warto zauważyć, że wykonywanie zapytań przy użyciu samego sqlite3 jest nadal około 3 razy szybsze niż przy użyciu SQLAlchemy Core. Wydaje mi się, że to cena, jaką płacisz za zwrócenie elementu ResultProxy zamiast pustego wiersza sqlite3.

SQLAlchemy Core jest około 8 razy szybszy niż użycie ORM. Więc odpytywanie przy użyciu ORM jest dużo wolniejsze bez względu na wszystko.

Oto kod, którego użyłem:

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)

Testowałem również bez konwersji wyniku zapytania na dykty, a statystyki są podobne:

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

Wykonywanie zapytań za pomocą SQLAlchemy Core jest około 20 razy szybsze w porównaniu z ORM.

Należy pamiętać, że testy te są bardzo powierzchowne i nie należy ich traktować zbyt poważnie. Mogę pominąć kilka oczywistych sztuczek, które mogłyby całkowicie zmienić statystyki.

Najlepszym sposobem pomiaru poprawy wydajności jest bezpośrednio we własnej aplikacji. Nie bierz moich statystyk za pewnik.

Alex
źródło
Chciałem tylko powiedzieć, że w 2019 roku, przy najnowszych wersjach wszystkiego, nie obserwuję znaczących względnych odchyleń od waszych czasów. Mimo wszystko ciekawi mnie też, czy nie brakuje jakiejś „sztuczki”.
PascalVKooten
0

Chciałbym wypróbować test wyrażenia wstawiania, a następnie benchmark.

Prawdopodobnie nadal będzie wolniejszy ze względu na obciążenie OR mappera, ale mam nadzieję, że nie będzie tak dużo wolniej.

Czy mógłbyś spróbować i opublikować wyniki. To jest bardzo interesująca rzecz.

Edmon
źródło
1
Tylko 10% szybciej przy użyciu wyrażenia wstawiania. Chciałbym wiedzieć dlaczego: SqlAlchemy Insert: Całkowity czas dla 100000 rekordów 9,47 sekundy
braddock
Nie po to, żeby cię tym zaniepokoić, ale jeśli jesteś zainteresowany, może czas na kod związany z sesją db po wstawieniu i użyciu timit. docs.python.org/library/timeit.html
Edmon
Mam ten sam problem z wyrażeniem wstawiania, jest śmiertelnie powolne, patrz stackoverflow.com/questions/11887895/ ...
dorvak