Tại sao SQLAlchemy chèn bằng sqlite chậm hơn 25 lần so với sử dụng trực tiếp sqlite3?


81

Tại sao trường hợp thử nghiệm đơn giản này chèn 100.000 hàng bằng SQLAlchemy chậm hơn 25 lần so với việc sử dụng trình điều khiển sqlite3 trực tiếp? Tôi đã thấy sự chậm tương tự trong các ứng dụng trong thế giới thực. Tôi có làm điều gì sai?

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

Tôi đã thử nhiều biến thể (xem http://pastebin.com/zCmzDraU )

Câu trả lời:


189

SQLAlchemy ORM sử dụng đơn vị mẫu công việc khi đồng bộ hóa các thay đổi đối với cơ sở dữ liệu. Mô hình này vượt xa việc "chèn" dữ liệu đơn giản. Nó bao gồm việc các thuộc tính được gán trên các đối tượng được nhận bằng hệ thống thiết bị đo đạc thuộc tính theo dõi các thay đổi trên các đối tượng khi chúng được tạo ra, bao gồm tất cả các hàng được chèn vào đều được theo dõi trong một bản đồ nhận dạngcó tác dụng là đối với mỗi hàng SQLAlchemy phải truy xuất "id được chèn lần cuối" của nó nếu chưa được cung cấp và cũng liên quan đến việc các hàng được chèn sẽ được quét và sắp xếp cho các phụ thuộc khi cần thiết. Các đối tượng cũng phải tuân theo một mức độ kế toán hợp lý để giữ cho tất cả hoạt động này hoạt động, điều này đối với một số lượng rất lớn các hàng cùng một lúc có thể tạo ra một lượng thời gian không nhiều cho các cấu trúc dữ liệu lớn, do đó tốt nhất là bạn nên chia nhỏ chúng.

Về cơ bản, đơn vị công việc là một mức độ tự động hóa lớn nhằm tự động hóa nhiệm vụ cố định một biểu đồ đối tượng phức tạp thành một cơ sở dữ liệu quan hệ không có mã liên tục rõ ràng và sự tự động hóa này có giá.

Vì vậy, ORM về cơ bản không dành cho chèn số lượng lớn hiệu suất cao. Đây là toàn bộ lý do tại sao SQLAlchemy có hai thư viện riêng biệt, bạn sẽ lưu ý nếu xem tại http://docs.sqlalchemy.org/en/latest/index.html, bạn sẽ thấy hai phần riêng biệt của trang chỉ mục - một cho ORM và một cho Core. Bạn không thể sử dụng SQLAlchemy một cách hiệu quả nếu không hiểu cả hai.

Đối với trường hợp sử dụng chèn hàng loạt nhanh, SQLAlchemy cung cấp cốt lõi , là hệ thống tạo và thực thi SQL mà ORM xây dựng trên đó. Sử dụng hệ thống này một cách hiệu quả, chúng ta có thể tạo ra một INSERT cạnh tranh với phiên bản SQLite thô. Tập lệnh bên dưới minh họa điều này, cũng như phiên bản ORM chỉ định trước số nhận dạng khóa chính để ORM có thể sử dụng thi hành () để chèn hàng. Cả hai phiên bản ORM cũng phân chia khối lượng bản ghi 1000 bản ghi cùng một lúc, điều này có tác động đáng kể đến hiệu suất.

Runtimes quan sát được ở đây là:

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

kịch bản:

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)

Xem thêm: http://docs.sqlalchemy.org/en/latest/faq/performance.html


Cảm ơn vì lời giải thích. Engine.execute () có khác nhiều so với DBSession.execute () không? Tôi đã thử một biểu thức chèn bằng DBSession.execute () nhưng nó không nhanh hơn đáng kể so với phiên bản ORM đầy đủ.
braddock

4
engine.execute () và DBSession.execute () hầu hết giống nhau ngoại trừ DBSession.execute () sẽ bọc một chuỗi SQL thuần túy nhất định trong text (). Nó tạo ra sự khác biệt lớn nếu bạn đang sử dụng cú pháp thực thi / thực thi. pysqlite được viết hoàn toàn bằng C và hầu như không có độ trễ, vì vậy mọi chi phí Python được thêm vào lệnh gọi thực thi () của nó sẽ hiển thị rõ ràng trong hồ sơ. Ngay cả một lệnh gọi hàm thuần Python duy nhất cũng chậm hơn đáng kể so với lệnh gọi hàm C thuần túy như thực thi của pysqlite (). Bạn cũng cần phải xem xét rằng các cấu trúc biểu thức SQLAlchemy trải qua một bước biên dịch cho mỗi lần gọi thực thi ().
zzzeek

3
lõi được tạo ra đầu tiên, mặc dù sau vài tuần đầu tiên khi khái niệm bằng chứng cốt lõi hoạt động (và thật là khủng khiếp ) ORM và lõi được phát triển song song từ thời điểm đó trở đi.
zzzeek

2
Tôi thực sự không biết tại sao mọi người lại chọn mô hình ORM lúc đó. Hầu hết các dự án sử dụng cơ sở dữ liệu sẽ có +10.000 hàng. duy trì 2 phương pháp cập nhật (một cho hàng đơn và một cho hàng loạt) nghe có vẻ không thông minh.
Peter Moore

5
sẽ có .... 10000 hàng mà họ cần phải chèn hàng loạt cùng một lúc? không đặc biệt. ví dụ, phần lớn các ứng dụng web có thể trao đổi nửa tá hàng cho mỗi yêu cầu. ORM khá phổ biến với một số trang web rất nổi tiếng và có lưu lượng truy cập cao.
zzzeek

21

Câu trả lời tuyệt vời từ @zzzeek. Đối với những người thắc mắc về cùng một số liệu thống kê cho các truy vấn, tôi đã sửa đổi một chút mã @zzzeek để truy vấn các bản ghi tương tự đó ngay sau khi chèn chúng, sau đó chuyển đổi các bản ghi đó thành danh sách các phần.

Đây là kết quả

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

Điều thú vị cần lưu ý là truy vấn bằng cách sử dụng bare sqlite3 vẫn nhanh hơn khoảng 3 lần so với sử dụng SQLAlchemy Core. Tôi đoán đó là cái giá bạn phải trả để có một ResultProxy được trả lại thay vì một hàng sqlite3 trống.

SQLAlchemy Core nhanh hơn khoảng 8 lần so với sử dụng ORM. Vì vậy, truy vấn bằng ORM chậm hơn rất nhiều.

Đây là mã tôi đã sử dụng:

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)

Tôi cũng đã thử nghiệm mà không chuyển đổi kết quả truy vấn thành số lần và số liệu thống kê tương tự:

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

Truy vấn với SQLAlchemy Core nhanh hơn khoảng 20 lần so với ORM.

Điều quan trọng cần lưu ý là những bài kiểm tra đó rất hời hợt và không nên quá coi trọng. Tôi có thể bỏ lỡ một số thủ thuật rõ ràng có thể thay đổi số liệu thống kê hoàn toàn.

Cách tốt nhất để đo lường sự cải thiện hiệu suất là trực tiếp trong ứng dụng của riêng bạn. Đừng coi số liệu thống kê của tôi là điều hiển nhiên.


Tôi chỉ muốn cho bạn biết rằng vào năm 2019 với các phiên bản mới nhất của mọi thứ, tôi không quan sát thấy sự sai lệch tương đối đáng kể so với thời gian của bạn. Tuy nhiên, tôi cũng tò mò không biết "mánh khóe" nào đó có bị bỏ qua không.
PascalVKooten

0

Tôi sẽ thử kiểm tra biểu thức chèn và sau đó là điểm chuẩn.

Nó có thể vẫn sẽ chậm hơn vì chi phí của trình lập bản đồ HOẶC nhưng tôi hy vọng sẽ không chậm hơn nhiều.

Bạn có phiền hãy thử và đăng kết quả. Đây là một công cụ rất thú vị.


1
Chỉ nhanh hơn 10% khi sử dụng biểu thức chèn. Tôi ước gì tôi biết lý do tại sao: SQLAlchemy Insert: Tổng thời gian cho 100000 hồ sơ 9,47 giây
Braddock

Không phải để làm phiền bạn về điều này, nhưng nếu bạn quan tâm có thể đặt thời gian mã liên quan đến phiên db sau khi chèn và sử dụng timit. docs.python.org/library/timeit.html
Edmon

Tôi có cùng một vấn đề với các biểu hiện chèn, Nó rất chết chậm, xem stackoverflow.com/questions/11887895/...
dorvak
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.