[python] SQLAlchemy 삽입이 sqlite3를 직접 사용하는 것보다 25 배 느린 이유는 무엇입니까?

SQLAlchemy를 사용하면이 간단한 테스트 케이스가 SQLAlchemy를 사용하여 직접 sqlite3 드라이버를 사용하는 것보다 25 배 더 느린 행을 삽입하는 이유는 무엇입니까? 실제 응용 프로그램에서 비슷한 속도 저하를 보았습니다. 내가 뭘 잘못하고 있니?

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



답변

SQLAlchemy ORM은 데이터베이스에 대한 변경 사항을 동기화 할 때 작업 단위 패턴을 사용 합니다. 이 패턴은 단순한 데이터 “삽입”을 훨씬 뛰어 넘습니다. 여기에는 개체에 할당 된 속성이 개체의 변경 내용을 추적하는 속성 계측 시스템을 사용하여 수신되고 삽입 된 모든 행이 ID 맵 에서 추적된다는 것을 포함합니다.이는 각 행에 대해 SQLAlchemy가 아직 제공되지 않은 경우 “마지막 삽입 된 ID”를 검색해야한다는 효과가 있으며 삽입 될 행이 필요에 따라 종속성에 대해 스캔되고 정렬되는 것도 포함합니다. 객체는 또한이 모든 실행을 유지하기 위해 상당한 수준의 부기의 대상이되는데, 이는 한 번에 매우 많은 수의 행에 대해 대규모 데이터 구조에 소요되는 과도한 시간을 생성 할 수 있으므로 이들을 청크하는 것이 가장 좋습니다.

기본적으로 작업 단위는 복잡한 개체 그래프를 명시적인 지속성 코드없이 관계형 데이터베이스에 지속하는 작업을 자동화하기위한 대규모 자동화이며이 자동화에는 대가가 따릅니다.

따라서 ORM은 기본적으로 고성능 벌크 인서트 용이 아닙니다. 이것이 SQLAlchemy에 두 개의 개별 라이브러리 가있는 전체 이유입니다 . http://docs.sqlalchemy.org/en/latest/index.html 을 보면 색인 페이지에 두 개의 별개의 반쪽이 표시됩니다. 하나는 ORM 용이고 다른 하나는 코어 용입니다. 둘 다 이해하지 않으면 SQLAlchemy를 효과적으로 사용할 수 없습니다.

빠른 대량 삽입의 사용 사례를 위해 SQLAlchemy는 ORM이 기반으로하는 SQL 생성 및 실행 시스템 인 core를 제공합니다 . 이 시스템을 효과적으로 사용하면 원시 SQLite 버전과 경쟁하는 INSERT를 생성 할 수 있습니다. 아래 스크립트는 ORM이 executemany ()를 사용하여 행을 삽입 할 수 있도록 기본 키 식별자를 사전 할당하는 ORM 버전과이를 설명합니다. 두 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의 훌륭한 답변입니다. 쿼리에 대한 동일한 통계에 대해 궁금한 사람들을 위해 @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를 사용하는 쿼리는 SQLAlchemy Core를 사용하는 것보다 여전히 약 3 배 더 빠릅니다. 베어 sqlite3 행 대신 ResultProxy가 반환 되는 데 지불하는 가격이라고 생각합니다 .

SQLAlchemy Core는 ORM을 사용하는 것보다 약 8 배 빠릅니다. 따라서 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를 사용한 쿼리는 ORM에 비해 약 20 배 더 빠릅니다.

이러한 테스트는 매우 피상적이고 너무 심각하게 받아 들여서는 안된다는 점에 유의해야합니다. 통계를 완전히 바꿀 수있는 몇 가지 명백한 트릭이 누락되었을 수 있습니다.

성능 향상을 측정하는 가장 좋은 방법은 응용 프로그램에서 직접 수행하는 것입니다. 내 통계를 당연시하지 마십시오.


답변

삽입 식 테스트를 시도한 다음 벤치마킹합니다.

OR 매퍼 오버 헤드로 인해 여전히 느려질 수 있지만 그다지 느리지 않기를 바랍니다.

시도하고 결과를 게시 해 주시겠습니까? 이것은 매우 흥미로운 것입니다.


답변