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

81

Почему этот простой тестовый пример с SQLAlchemy вставляет 100 000 строк в 25 раз медленнее, чем при использовании драйвера 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 )

Braddock
источник

Ответы:

189

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

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

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

Для случая использования быстрой массовой вставки SQLAlchemy предоставляет ядро , которое представляет собой систему генерации и выполнения SQL, поверх которой строится ORM. Эффективно используя эту систему, мы можем создать INSERT, который конкурирует с необработанной версией SQLite. Приведенный ниже сценарий иллюстрирует это, а также версию ORM, которая предварительно назначает идентификаторы первичного ключа, чтобы ORM мог использовать executemany () для вставки строк. Обе версии 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

сценарий:

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

Zzzeek
источник
Спасибо за объяснение. Engine.execute () значительно отличается от DBSession.execute ()? Я пробовал вставить выражение с помощью DBSession.execute (), но оно было ненамного быстрее полной версии ORM.
braddock
4
engine.execute () и DBSession.execute () в основном одинаковы, за исключением того, что DBSession.execute () переносит заданную простую строку SQL в text (). Это имеет огромное значение, если вы используете синтаксис execute / executemany. pysqlite полностью написан на C и почти не имеет задержки, поэтому любые накладные расходы Python, добавленные к его вызову execute (), будут заметно проявляться при профилировании. Даже один вызов функции на чистом Python выполняется значительно медленнее, чем вызов чистой C-функции, такой как execute () в pysqlite. Вы также должны учитывать, что конструкции выражений SQLAlchemy проходят этап компиляции для каждого вызова execute ().
zzzeek 02
3
ядро было создано первым, хотя после первых нескольких недель, когда основное доказательство концепции сработало (и это было ужасно ), с этого момента ORM и ядро ​​разрабатывались параллельно.
zzzeek
2
Я действительно не знаю, почему тогда кто-то выбрал модель ORM. Большинство проектов, использующих базу данных, будут иметь +10 000 строк. поддержка двух методов обновления (один для одной строки и один для массового) звучит неразумно.
Питер Мур
5
будет .... 10000 строк нужно вставлять сразу все сразу навалом? не особенно. например, подавляющее большинство веб-приложений, вероятно, обмениваются полдюжиной строк за один запрос. ORM довольно популярен на некоторых очень известных сайтах с высокой посещаемостью.
zzzeek
21

Отличный ответ от @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.

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

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

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

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

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

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

Эдмон
источник
1
Только на 10% быстрее при использовании выражения вставки. Хотел бы я знать, почему: SqlAlchemy Insert: Общее время для 100000 записей 9,47 секунды
braddock
Не для того, чтобы вас это беспокоить, но если вам интересно, возможно, время кода, связанного с сеансом db, после вставок и использования timit. docs.python.org/library/timeit.html
Эдмон,
У меня такая же проблема с выражением вставки, подмигнул мертвые медленно, см stackoverflow.com/questions/11887895/...
dorvak