web-dev-qa-db-fra.com

Pourquoi SQLAlchemy insert avec sqlite est-il 25 fois plus lent que l'utilisation directe de sqlite3?

Pourquoi ce scénario de test simple insère-t-il 100 000 lignes 25 fois plus lentement avec SQLAlchemy qu'avec le pilote sqlite3 directement? J'ai vu des ralentissements similaires dans des applications du monde réel. Est-ce que je fais quelque chose de mal?

#!/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):
    __table= "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 __== '__main__':
    test_sqlalchemy(100000)
    test_sqlite3(100000)

J'ai essayé de nombreuses variantes (voir http://Pastebin.com/zCmzDra )

74
braddock

L'ORM SQLAlchemy utilise le modèle nité d'oeuvre lors de la synchronisation des modifications dans la base de données. Ce modèle va bien au-delà de simples "insertions" de données. Il comprend que les attributs attribués aux objets sont reçus à l'aide d'un système d'instrumentation d'attributs qui suit les changements sur les objets au fur et à mesure qu'ils sont effectués, comprend que toutes les lignes insérées sont suivies dans un carte d'identité qui a pour effet que pour chaque ligne SQLAlchemy doit récupérer son "dernier identifiant inséré" si ce n'est déjà fait, et implique également que les lignes à insérer sont analysées et triées pour les dépendances selon les besoins. Les objets sont également soumis à un bon niveau de tenue de livres afin de maintenir tout cela en marche, ce qui pour un très grand nombre de lignes à la fois peut créer une quantité excessive de temps passé avec de grandes structures de données, il est donc préférable de les segmenter.

Fondamentalement, l'unité de travail est un degré élevé d'automatisation afin d'automatiser la tâche de persister un graphe d'objet complexe dans une base de données relationnelle sans code de persistance explicite, et cette automatisation a un prix.

Les ORM ne sont donc fondamentalement pas destinés aux inserts en vrac hautes performances. C'est la raison pour laquelle SQLAlchemy a deux bibliothèques distinctes, que vous remarquerez si vous regardez http: //docs.sqlalchemy .org/en/latest/index.html vous verrez deux moitiés distinctes sur la page d'index - une pour l'ORM et une pour le Core. Vous ne pouvez pas utiliser SQLAlchemy efficacement sans comprendre les deux.

Pour le cas d'utilisation des insertions en vrac rapides, SQLAlchemy fournit le core , qui est le système de génération et d'exécution SQL que l'ORM construit par-dessus. En utilisant ce système efficacement, nous pouvons produire un INSERT qui est compétitif avec la version brute de SQLite. Le script ci-dessous illustre cela, ainsi qu'une version ORM qui pré-attribue des identificateurs de clé primaire afin que l'ORM puisse utiliser executemany () pour insérer des lignes. Les deux versions d'ORM regroupent également les flux à 1000 enregistrements à la fois, ce qui a un impact significatif sur les performances.

Les temps d'exécution observés ici sont:

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

scénario:

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):
    __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 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 __== '__main__':
    test_sqlalchemy_orm(100000)
    test_sqlalchemy_orm_pk_given(100000)
    test_sqlalchemy_core(100000)
    test_sqlite3(100000)

Voir aussi: http://docs.sqlalchemy.org/en/latest/faq/performance.html

177
zzzeek

Excellente réponse de @zzzeek. Pour ceux qui se demandent quelles sont les mêmes statistiques pour les requêtes, j'ai légèrement modifié le code @zzzeek pour interroger ces mêmes enregistrements juste après les avoir insérés, puis convertir ces enregistrements en une liste de dict.

Voici les résultats

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

Il est intéressant de noter que l'interrogation à l'aide de sqlite3 nu est toujours environ 3 fois plus rapide que l'utilisation de SQLAlchemy Core. Je suppose que c'est le prix à payer pour avoir un ResultProxy retourné au lieu d'une ligne sqlite3 nue.

SQLAlchemy Core est environ 8 fois plus rapide que l'utilisation d'ORM. L'interrogation à l'aide d'ORM est donc beaucoup plus lente, quoi qu'il arrive.

Voici le code que j'ai utilisé:

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):
    __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 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 __== '__main__':
    test_sqlalchemy_orm(100000)
    test_sqlalchemy_orm_pk_given(100000)
    test_sqlalchemy_core(100000)
    test_sqlite3(100000)

J'ai également testé sans convertir le résultat de la requête en dict et les statistiques sont similaires:

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

L'interrogation avec SQLAlchemy Core est environ 20 fois plus rapide par rapport à ORM.

Il est important de noter que ces tests sont très superficiels et ne doivent pas être pris trop au sérieux. Je pourrais manquer quelques astuces évidentes qui pourraient changer complètement les statistiques.

La meilleure façon de mesurer les améliorations de performances est directement dans votre propre application. Ne prenez pas mes statistiques pour acquises.

19
Alex

J'essaierais le test insérer l'expression puis le test de performance.

Il sera probablement encore plus lent à cause de OR surcharge du mappeur mais j'espère que ce ne sera pas beaucoup plus lent.

Pourriez-vous essayer et publier des résultats? Ce sont des choses très intéressantes.

0
Edmon