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
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.
źródło
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.
źródło