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


81

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 참조 )

답변:


189

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


설명 해주셔서 감사합니다. engine.execute ()가 DBSession.execute ()와 크게 다른가요? DBSession.execute ()를 사용하여 삽입 식을 시도했지만 전체 ORM 버전보다 크게 빠르지는 않았습니다.
braddock 2012-08-02

4
engine.execute () 및 DBSession.execute ()는 DBSession.execute ()가 text ()에서 주어진 일반 SQL 문자열을 래핑한다는 점을 제외하면 거의 동일합니다. 그것은 만드는 거대한 당신이 / 실행 executemany 구문을 사용하는 경우의 차이를. pysqlite는 전적으로 C로 작성되었으며 대기 시간이 거의 없으므로 execute () 호출에 추가 된 Python 오버 헤드가 프로파일 링에 눈에 띄게 표시됩니다. 단일 순수 Python 함수 호출조차도 pysqlite의 execute ()와 같은 순수 C 함수 호출보다 훨씬 느립니다. 또한 SQLAlchemy 표현식 구문이 execute () 호출마다 컴파일 단계를 거침을 고려해야합니다.
zzzeek 2012-08-02

3
핵심 개념 증명이 작동 한 후 처음 몇 주 후에 (그리고 끔찍했습니다 ) ORM과 핵심이 그 시점부터 병렬로 개발 되었지만 핵심이 먼저 생성되었습니다 .
zzzeek

2
왜 누군가가 ORM 모델을 선택하는지 정말 모르겠습니다. 데이터베이스를 사용하는 대부분의 프로젝트에는 +10,000 개의 행이 있습니다. 2 개의 업데이트 방법 (하나는 단일 행용, 다른 하나는 대량 용)을 유지하는 것이 현명하게 들리지 않습니다.
Peter Moore

5
한 번에 한꺼번에 모두 삽입해야하는 행이 10000 개 있습니까? 특별히 아닙니다. 예를 들어 대부분의 웹 애플리케이션은 요청 당 6 개의 행을 교환 할 것입니다. 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를 사용하는 쿼리는 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 배 더 빠릅니다.

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

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


모든 것의 최신 버전을 사용하여 2019 년에 귀하의 타이밍에서 상당한 상대적 편차가 관찰되지 않음을 알려 드리고자합니다. 그래도 "트릭"을 놓쳤는 지 궁금합니다.
PascalVKooten

0

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

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

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


1
삽입 식을 사용하면 10 %더 빠릅니다 . 이유를 알았 으면 좋겠어요 : SqlAlchemy 삽입 : 총 100000 개 레코드 시간 9.47 초
braddock

이것으로 당신을 괴롭히는 것은 아니지만 관심이 있다면 삽입 후 timit을 사용하여 db 세션 관련 코드를 시간을 정하십시오. docs.python.org/library/timeit.html
Edmon

삽입 식에도 동일한 문제가 있습니다. 속도가 느립니다. stackoverflow.com/questions/11887895/를
dorvak
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.