【问题标题】:Why is SQLAlchemy insert with sqlite 25 times slower than using sqlite3 directly?为什么使用 sqlite 插入 SQLAlchemy 比直接使用 sqlite3 慢 25 倍?
【发布时间】:2012-07-30 22:42:54
【问题描述】:

为什么这个简单的测试用例使用 SQLAlchemy 插入 100,000 行比直接使用 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

【问题讨论】:

    标签: python orm sqlite sqlalchemy


    【解决方案1】:

    SQLAlchemy ORM 在同步对数据库的更改时使用unit of work 模式。这种模式远远超出了简单的数据“插入”。它包括使用属性检测系统接收分配给对象的属性,该系统跟踪对象所做的更改,包括在identity map 中跟踪插入的所有行,其效果是对于每一行 SQLAlchemy 必须检索其“最后插入的 id”(如果尚未给出),并且还涉及根据需要对要插入的行进行扫描和排序以查找依赖项。对象还需要进行相当程度的簿记,以保持所有这些运行,对于大量的行来说,一次可能会花费大量的时间来处理大型数据结构,因此最好将它们分块。

    基本上,工作单元是高度自动化的,目的是自动化将复杂对象图持久化到关系数据库中的任务,无需显式持久化代码,这种自动化是有代价的。

    所以 ORM 基本上不适用于高性能批量插入。这就是为什么 SQLAlchemy 有 两个 单独的库的全部原因,如果您查看 http://docs.sqlalchemy.org/en/latest/index.html,您会注意到索引页面有两个不同的部分 - 一个用于 ORM,一个用于核心。如果不了解两者,您将无法有效地使用 SQLAlchemy。

    对于快速批量插入的用例,SQLAlchemy 提供了core,这是 ORM 在其之上构建的 SQL 生成和执行系统。有效地使用这个系统,我们可以生成一个与原始 SQLite 版本竞争的 INSERT。下面的脚本说明了这一点,以及预分配主键标识符的 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 版本快得多。
    • engine.execute() 和 DBSession.execute() 基本相同,只是 DBSession.execute() 会将给定的纯 SQL 字符串包装在 text() 中。如果您使用的是 execute/executemany 语法,它会产生 巨大的 差异。 pysqlite 完全用 C 语言编写,几乎没有延迟,因此添加到它的 execute() 调用中的任何 Python 开销都将在分析中明显出现。即使是单个纯 Python 函数调用也比 pysqlite 的 execute() 之类的纯 C 函数调用慢得多。您还需要考虑 SQLAlchemy 表达式构造在每个 execute() 调用中都经过一个编译步骤。
    • 核心是首先创建的,但在最初的几周后,一旦核心概念证明工作(这可怕),ORM 和核心是从那并行开发的指向前方。
    • 我真的不知道为什么有人会选择ORM模型。大多数使用数据库的项目将有 +10,000 行。维护 2 种更新方法(一种用于单行,一种用于批量)听起来并不聪明。
    • 将有....10000 行,他们需要一直批量插入所有行?不是特别。例如,绝大多数 Web 应用程序可能每个请求交换六行。 ORM 在一些非常著名和高流量的网站中非常受欢迎。
    【解决方案2】:

    来自@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 倍。我想这就是你为返回 ResultProxy 而不是裸的 sqlite3 行所付出的代价。

    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 年,随着一切的最新版本,我没有观察到与您的时间安排有明显的相对偏差。不过,我也很好奇是否错过了一些“技巧”。
    【解决方案3】:

    我会尝试 insert expression 测试,然后进行基准测试。

    由于 OR 映射器开销,它可能仍然会更慢,但我希望不会那么慢。

    您介意尝试并发布结果吗?这是非常有趣的东西。

    【讨论】:

    • 使用插入表达式仅快 10%。我希望我知道为什么:SqlAlchemy 插入:100000 条记录的总时间 9.47 秒
    • 不是为了给您带来麻烦,但是如果您有兴趣,可以在插入和使用 timit 之后计时 db session 相关代码。 docs.python.org/library/timeit.html
    • 插入表达式也有同样的问题,速度太慢了,见stackoverflow.com/questions/11887895/…
    猜你喜欢
    • 1970-01-01
    • 2020-09-05
    • 1970-01-01
    • 2021-04-12
    • 1970-01-01
    • 2020-08-12
    • 2014-12-23
    • 2018-08-12
    • 2022-11-28
    相关资源
    最近更新 更多