Чому вставка SQLAlchemy із sqlite в 25 разів повільніша, ніж безпосередньо за допомогою sqlite3?


81

Чому цей простий тестовий приклад вставляє 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 )

Відповіді:


189

SQLAlchemy ORM використовує шаблон одиниці роботи під час синхронізації змін до бази даних. Ця закономірність виходить далеко за рамки простих "вставок" даних. Він включає в себе те, що атрибути, які присвоєні об’єктам, отримуються за допомогою системи вимірювань атрибутів, яка відстежує зміни на об’єктах під час їх створення, включає те, що всі вставлені рядки відстежуються на ідентифікаційній картіщо має наслідком, що для кожного рядка SQLAlchemy має отримати свій "останній вставний ідентифікатор", якщо він ще не вказаний, а також передбачає, що рядки, які потрібно вставити, скануються та сортуються за залежностями за необхідності. Об’єкти також підлягають достатній мірі бухгалтерського обліку, щоб зберегти весь цей процес, що для дуже великої кількості рядків одночасно може створити непомірну кількість часу, проведеного з великими структурами даних, тому найкраще їх розділити.

В основному одиниця роботи - це великий ступінь автоматизації для автоматизації завдання збереження складного графічного об’єкта у реляційній базі даних без явного коду стійкості, і ця автоматизація має ціну.

Отже, ORM в основному не призначені для високопродуктивних сипучих вставок. Це вся причина, чому SQLAlchemy має дві окремі бібліотеки, на що ви звернете увагу, якщо подивитесь на http://docs.sqlalchemy.org/en/latest/index.html, то на сторінці індексу ви побачите дві різні половинки - один для ORM і один для Core. Ви не можете ефективно використовувати 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


Дякую за пояснення. Чи значно відрізняється engine.execute () від DBSession.execute ()? Я спробував вставити вираз, використовуючи DBSession.execute (), але це було не значно швидше, ніж повна версія ORM.
braddock

4
engine.execute () та DBSession.execute () здебільшого однакові, за винятком того, що DBSession.execute () оберне заданий звичайний рядок SQL у текст (). Це робить величезну різницю , якщо ви використовуєте виконати / executemany синтаксис. pysqlite повністю написаний на мові C і майже не має латентності, тому будь-які накладні витрати на Python, додані до його виклику execute (), відчутно відображатимуться в профілюванні. Навіть один виклик функції pure-Python значно повільніший, ніж виклик чистої функції C, такий як pysqlite's execute (). Також потрібно врахувати, що конструкції виразів SQLAlchemy проходять крок компіляції за кожен виклик execute ().
zzzeek

3
ядро було створено першим, хоча після перших кількох тижнів, як тільки основний доказ концепції спрацював (і це було жахливо ), ORM і ядро ​​розроблялися паралельно з цього моменту.
zzzeek

2
Я справді не знаю, чому тоді хтось обрав би модель ORM. Більшість проектів, що використовують базу даних, матимуть +10 000 рядків. ведення 2 методів оновлення (одного для одного рядка та одного для масового використання) просто не здається розумним.
Пітер Мур,

5
матимуть .... 10000 рядків, які їм потрібно вставляти всі відразу оптом весь час? не особливо. переважна більшість веб-додатків, наприклад, можливо обмінюється півдюжиною рядків на запит. ORM досить популярний серед деяких дуже відомих веб-сайтів із високим трафіком.
zzzeek

21

Відмінна відповідь від @zzzeek. Для тих, хто цікавиться тією ж статистикою для запитів, я трохи змінив код @zzzeek, ​​щоб здійснити запит до тих самих записів відразу після їх вставки, а потім перетворити ці записи в список диктів.

Ось результати

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)

Я також тестував, не перетворюючи результат запиту на дикти, і статистика схожа:

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.

Важливо зазначити, що ці тести дуже поверхневі і не слід сприймати їх надто серйозно. Можливо, мені не вистачає явних хитрощів, які можуть повністю змінити статистику.

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


Просто хотів повідомити, що в 2019 році з останніми версіями всього, я не спостерігаю значних відносних відхилень від ваших термінів. Тим не менше, мені також цікаво, якщо якусь "хитрість" пропустять.
PascalVKooten

0

Я спробував би тест вставки виразів, а потім еталон.

Можливо, це все одно буде повільніше через накладні витрати АБО, але я сподіваюся, що не настільки повільніше.

Не могли б ви спробувати та опублікувати результати. Це дуже цікава річ.


1
Тільки на 10% швидше за допомогою вставного виразу. Мені б хотілося, щоб я знав чому: Вставка SqlAlchemy: Загальний час для 100000 записів 9,47 сек
braddock

Не для того, щоб вас з цим помиляти, але якщо ви зацікавлені, можливо, час, пов'язаний з сеансом db, після вставлення та використання timit. docs.python.org/library/timeit.html
Едмон

У мене така ж проблема з виразом вставки, підморгнув мертві повільно, см stackoverflow.com/questions/11887895 / ...
dorvak
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.