Почему SQLAlchemy вставка с sqlite в 25 раз медленнее, чем использование sqlite3 напрямую?

63

Почему этот простой тестовый пример вставляет 100 000 строк в 25 раз медленнее с SQLAlchemy, чем напрямую использует драйвер sqlite3? Я видел подобные замедление в реальных приложениях. Я что-то делаю неправильно?

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

Я пробовал многочисленные варианты (см. http://pastebin.com/zCmzDraU)

Теги:
sqlite3
orm
sqlalchemy

3 ответа

161
Лучший ответ

SQLAlchemy ORM использует элемент работы при синхронизации изменений в базе данных. Этот шаблон выходит далеко за рамки простых "вставок" данных. Он включает в себя то, что атрибуты, назначенные объектам, принимаются с использованием системы инструментов атрибутов, которая отслеживает изменения объектов по мере их создания, включает в себя, что все вставленные строки отслеживаются в что для каждой строки SQLAlchemy должен получить свой "последний вставленный идентификатор" , если он еще не задан, а также включает, что строки, которые нужно вставить, сканируются и сортируются по зависимостям по мере необходимости. Объекты также подвержены достоверной степени бухгалтерского учета, чтобы сохранить все это, что для очень большого количества строк сразу может создать чрезмерное количество времени, затрачиваемого на большие структуры данных, поэтому лучше всего их обрезать.

В основном, единица работы - это большая степень автоматизации, чтобы автоматизировать задачу сохранения сложного графа объектов в реляционную базу данных без явного кода сохранения, и эта автоматизация имеет цену.

Таким образом, ORM в основном не предназначены для высокопроизводительных объемных вставок. Вот почему SQLAlchemy имеет две отдельные библиотеки, которые вы заметите, если вы посмотрите на http://docs.sqlalchemy.org/en/latest/index.html, вы увидите две разные половины на индексную страницу - одну для ORM и одну для Core. Вы не можете эффективно использовать SQLAlchemy, не понимая оба.

В случае использования быстрых объемных вставок SQLAlchemy предоставляет core, который представляет собой систему генерации и выполнения SQL, которую строят ORM на вершине. Используя эту систему, мы можем создать INSERT, который является конкурентоспособным с исходной версией SQLite. Ниже приведено описание script, а также версия ORM, которая предварительно назначает идентификаторы первичного ключа, чтобы ORM могла использовать функцию executeemany() для вставки строк. Обе версии ORM объединяют флеши с 1000 записями за раз, что имеет значительное влияние на производительность.

Наблюдаемые здесь промежутки времени:

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

script:

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)

Смотрите также: http://docs.sqlalchemy.org/en/latest/faq/performance.html

  • 0
    Спасибо за объяснение. Значительно ли engine.execute () отличается от DBSession.execute ()? Я пытался вставить выражение, используя DBSession.execute (), но оно не было значительно быстрее, чем полная версия ORM.
  • 4
    engine.execute () и DBSession.execute () в основном одинаковы, за исключением того, что DBSession.execute () переносит данную обычную строку SQL в text (). Это имеет огромное значение, если вы используете синтаксис execute / executemany. pysqlite полностью написан на C и почти не имеет задержки, поэтому любые накладные расходы Python, добавленные к его вызову execute (), будут ощутимо проявляться при профилировании. Даже один вызов функции чисто Python значительно медленнее, чем вызов чистой функции C, такой как execute () pysqlite. Также необходимо учитывать, что конструкции выражений SQLAlchemy проходят этап компиляции для вызова execute ().
Показать ещё 7 комментариев
17

Отличный ответ от @zzzeek. Для тех, кто интересуется одной и той же статистикой запросов, я немного изменил код @zzzeek, ​​чтобы запросить те же записи сразу после их вставки, а затем преобразовать эти записи в список dicts.

Здесь результаты

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

Интересно отметить, что запрос с использованием голого sqlite3 все еще примерно в 3 раза быстрее, чем использование SQLAlchemy Core. Я предполагаю, что цена, которую вы платите за ResultProxy, возвращается вместо годовой строки sqlite3.

SQLAlchemy Core примерно в 8 раз быстрее, чем использование ORM. Таким образом, запрос с использованием ORM намного медленнее, независимо от того, что.

Вот код, который я использовал:

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)

Я также тестировал, не преобразовывая результат запроса в dicts, и статистика аналогична:

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

Запрос с помощью SQLAlchemy Core примерно в 20 раз быстрее по сравнению с ORM.

Важно отметить, что эти тесты очень поверхностны и не должны восприниматься слишком серьезно. Я мог бы пропустить некоторые очевидные трюки, которые могли бы полностью изменить статистику.

Лучший способ измерения производительности - прямо в вашем собственном приложении. Не принимайте мою статистику как должное.

  • 0
    Просто хотел сообщить вам, что в 2019 году с последними версиями всего я не наблюдаю значительных относительных отклонений от вашего времени. Тем не менее, мне также любопытно, пропущен ли какой-то «трюк».
0

Я бы попробовал вставить выражение, а затем проверить.

Вероятно, он все еще будет медленнее из-за чрезмерных издержек OR, но я надеюсь, что это будет намного медленнее.

Не могли бы вы попытаться и опубликовать результаты. Это очень интересный материал.

  • 1
    Только на 10% быстрее, используя выражение вставки. Я хотел бы знать, почему: SqlAlchemy Вставка: Общее время для 100000 записей 9,47 секунд
  • 1
    Код для вставки выражения: pastebin.com/LwYqzJgi
Показать ещё 2 комментария

Ещё вопросы

Сообщество Overcoder
Наверх
Меню