性能


为什么我的应用程序在升级到 1.4 和/或 2.x 后运行缓慢?


从 1.4 版本开始,SQLAlchemy 包含一个 SQL 编译缓存工具,它将允许 Core 和 ORM SQL 结构缓存其字符串化形式,以及用于从语句中获取结果的其他结构信息,从而允许在下次使用另一个结构等效的结构时跳过相对昂贵的字符串编译过程。该系统依赖于为所有 SQL 结构实现的功能,包括 Columnselect()TypeEngine 对象生成一个 cache 键,它完全表示它们的状态,以至于它影响 SQL 编译过程。


缓存系统允许 SQLAlchemy 1.4 及更高版本在重复将 SQL 构造转换为字符串所花费的时间方面比 SQLAlchemy 1.3 性能更高。但是,这仅在为正在使用的方言和 SQL 构造启用缓存时才有效;否则,字符串编译通常类似于 SQLAlchemy 1.3 的编译,在某些情况下速度会略有下降。


然而,在一种情况下,如果 SQLAlchemy 的新缓存系统已被禁用(原因如下),ORM 的性能实际上可能比 1.3 或其他先前版本差得多,这是由于 ORM 惰性加载器和对象刷新查询中缺少缓存,在 1.3 及更早版本中使用了现在遗留的 BakedQuery系统。如果应用程序在切换到 1.4 时发现性能显著(30% 或更高)下降(以作完成的时间衡量),则这可能是导致问题的原因,下面提供了缓解措施。


另请参阅


SQL Compilation Caching - 缓存系统概述


Object 不会生成缓存键,性能影响 - 有关为未启用缓存的元素生成的警告的其他信息。


第一步 - 打开 SQL 日志记录并确认缓存是否正常工作


在这里,我们想使用 engine 日志记录,查找带有 [无键] 指示器甚至 [dialect does not support caching] .对于成功参与缓存系统的 SQL 语句,我们将看到的指示符将指示 [在 Xs 生成] 语句,然后 [cached since Xs ago] 的绝大多数语句。如果 [no key] 特别对于 SELECT 语句普遍存在,或者如果由于 [dialect does not support caching] 而完全禁用缓存,这可能是性能显著下降的原因。


第二步 - 确定哪些结构阻止了缓存的启用¶


假设语句没有被缓存,那么在应用程序的日志(仅限 SQLAlchemy 1.4.28 及更高版本)的早期应该会发出警告,指示未参与缓存的方言、TypeEngine 对象和 SQL 构造。


对于用户定义的数据类型,例如扩展 TypeDecorator 的数据类型 和 UserDefinedType 的警告将如下所示:

sqlalchemy.ext.SAWarning: MyType will not produce a cache key because the
``cache_ok`` attribute is not set to True. This can have significant
performance implications including some performance degradations in
comparison to prior SQLAlchemy versions. Set this attribute to True if this
type object's state is safe to use in a cache key, or False to disable this
warning.


对于自定义和第三方 SQL 元素,例如使用自定义 SQL 构造和编译扩展中描述的技术构造的元素,这些警告将如下所示:

sqlalchemy.exc.SAWarning: Class MyClass will not make use of SQL
compilation caching as it does not set the 'inherit_cache' attribute to
``True``. This can have significant performance implications including some
performance degradations in comparison to prior SQLAlchemy versions. Set
this attribute to True if this object can make use of the cache key
generated by the superclass. Alternatively, this attribute may be set to
False which will disable this warning.


对于使用 Dialect 的自定义方言和第三方方言 class 层次结构中,警告将如下所示:

sqlalchemy.exc.SAWarning: Dialect database:driver will not make use of SQL
compilation caching as it does not set the 'supports_statement_cache'
attribute to ``True``. This can have significant performance implications
including some performance degradations in comparison to prior SQLAlchemy
versions. Dialect maintainers should seek to set this attribute to True
after appropriate development and testing for SQLAlchemy 1.4 caching
support. Alternatively, this attribute may be set to False which will
disable this warning.


第 3 步 - 为给定对象启用缓存和/或寻找替代方案


缓解缓存不足的问题的步骤包括:


  • 查看从 TypeDecorator 扩展的所有自定义类型的 ExternalType.cache_ok 并将其设置为 TrueUserDefinedType 以及这些子类(例如 PickleType 的 PickleType 中。仅当自定义类型不包含任何影响其呈现 SQL 方式的额外 state 属性时,才设置此项:

    class MyCustomType(TypeDecorator):
        cache_ok = True
        impl = String


    如果正在使用的类型来自第三方库,请咨询该库的维护者,以便对其进行调整和发布。


    另请参阅


    ExternalType.cache_ok - 有关为自定义数据类型启用缓存的要求的背景。


  • 确保设置 Dialect.supports_statement_cache 了第三方方言 设置为 True。这表明第三方方言的维护者已经确保他们的方言可以与 SQLAlchemy 1.4 或更高版本一起使用,并且他们的方言不包含任何可能妨碍缓存的编译功能。由于有一些常见的编译模式实际上会干扰缓存,因此 dialect 维护者仔细检查和测试这一点很重要,并针对任何不适用于缓存的遗留模式进行调整。


    另请参阅


    Caching for Third Party Dialects - 第三方方言参与 SQL 语句缓存的背景和示例。


  • 自定义 SQL 类,包括可能使用自定义 SQL 构造和编译扩展创建的所有 DQL/DML 构造,以及对象的临时子类,例如 Column。对于普通子类,HasCacheKey.inherit_cache 属性可以设置为 True,这些子类不包含任何影响 SQL 编译的特定于子类的状态信息。


另请参阅


SQL 编译缓存 - 缓存系统概述


对象不会生成缓存键,性能影响 - 未为特定构造和/或方言启用缓存时发出的警告的背景。


如何分析 SQLAlchemy 驱动的应用程序?


查找性能问题通常涉及两种策略。一种是查询分析,另一种是代码分析。


查询分析


有时,只是普通的 SQL 日志记录(通过 python 的日志记录模块或通过 create_engine() 上的 echo=True 参数启用)就可以知道事情需要多长时间。例如,如果您在 SQL作后立即记录某些内容,则会在日志中看到如下内容:

17:37:48,325 INFO  [sqlalchemy.engine.base.Engine.0x...048c] SELECT ...
17:37:48,326 INFO  [sqlalchemy.engine.base.Engine.0x...048c] {<params>}
17:37:48,660 DEBUG [myapp.somemessage]


如果您在作后立即记录了 myApp.SomeMessage,您就会知道完成 SQL 部分需要 334 毫秒。


记录 SQL 还将说明是否发出了数十/数百个查询,这些查询可以更好地组织成更少的查询。当使用 SQLAlchemy ORM 时,提供“预先加载”功能来部分 (contains_eager()) 或完全 (joinedload()subqueryload()) 自动化此活动,但如果没有 ORM,“预先加载”通常意味着使用连接,以便可以将多个表的结果加载到一个结果集中,而不是随着更多深度的增加(即 r + r*r2+ r*r2*r3 ...)


为了对查询进行更长期的分析,或实施应用程序端的“慢速查询”监控器,可以使用事件来拦截游标执行,使用如下配方:

from sqlalchemy import event
from sqlalchemy.engine import Engine
import time
import logging

logging.basicConfig()
logger = logging.getLogger("myapp.sqltime")
logger.setLevel(logging.DEBUG)


@event.listens_for(Engine, "before_cursor_execute")
def before_cursor_execute(conn, cursor, statement, parameters, context, executemany):
    conn.info.setdefault("query_start_time", []).append(time.time())
    logger.debug("Start Query: %s", statement)


@event.listens_for(Engine, "after_cursor_execute")
def after_cursor_execute(conn, cursor, statement, parameters, context, executemany):
    total = time.time() - conn.info["query_start_time"].pop(-1)
    logger.debug("Query Complete!")
    logger.debug("Total Time: %f", total)


在上面,我们使用 ConnectionEvents.before_cursor_execute() ConnectionEvents.after_cursor_execute() 事件来建立执行语句时的拦截点。我们使用 info 字典将计时器附加到连接上;我们在这里使用 Stack 来应对 Cursor Execute 事件可能嵌套的偶尔情况。


代码分析


如果日志记录显示单个查询花费的时间过长,则需要细分在数据库中处理查询、通过网络发送结果、由 DBAPI 处理以及最终由 SQLAlchemy 的结果集和/或 ORM 层接收所花费的时间。根据具体情况,这些阶段中的每一个都可能出现自己的瓶颈。


为此,您需要使用 Python 分析模块。下面是一个简单的方法,它可以将性能分析集成到上下文管理器中:

import cProfile
import io
import pstats
import contextlib


@contextlib.contextmanager
def profiled():
    pr = cProfile.Profile()
    pr.enable()
    yield
    pr.disable()
    s = io.StringIO()
    ps = pstats.Stats(pr, stream=s).sort_stats("cumulative")
    ps.print_stats()
    # uncomment this to see who's calling what
    # ps.print_callers()
    print(s.getvalue())


要分析一段代码:

with profiled():
    session.scalars(select(FooClass).where(FooClass.somevalue == 8)).all()


分析的输出可用于了解时间花在何处。分析输出的一部分如下所示:

13726 function calls (13042 primitive calls) in 0.014 seconds

Ordered by: cumulative time

ncalls  tottime  percall  cumtime  percall filename:lineno(function)
222/21    0.001    0.000    0.011    0.001 lib/sqlalchemy/orm/loading.py:26(instances)
220/20    0.002    0.000    0.010    0.001 lib/sqlalchemy/orm/loading.py:327(_instance)
220/20    0.000    0.000    0.010    0.000 lib/sqlalchemy/orm/loading.py:284(populate_state)
   20    0.000    0.000    0.010    0.000 lib/sqlalchemy/orm/strategies.py:987(load_collection_from_subq)
   20    0.000    0.000    0.009    0.000 lib/sqlalchemy/orm/strategies.py:935(get)
    1    0.000    0.000    0.009    0.009 lib/sqlalchemy/orm/strategies.py:940(_load)
   21    0.000    0.000    0.008    0.000 lib/sqlalchemy/orm/strategies.py:942(<genexpr>)
    2    0.000    0.000    0.004    0.002 lib/sqlalchemy/orm/query.py:2400(__iter__)
    2    0.000    0.000    0.002    0.001 lib/sqlalchemy/orm/query.py:2414(_execute_and_instances)
    2    0.000    0.000    0.002    0.001 lib/sqlalchemy/engine/base.py:659(execute)
    2    0.000    0.000    0.002    0.001 lib/sqlalchemy/sql/elements.py:321(_execute_on_connection)
    2    0.000    0.000    0.002    0.001 lib/sqlalchemy/engine/base.py:788(_execute_clauseelement)

...


在上面,我们可以看到 instances() SQLAlchemy 函数被调用了 222 次(递归地,从外部调用了 21 次),所有调用总共花费了 0.011 秒。


执行缓慢


这些电话的细节可以告诉我们时间花在哪里。例如,如果您看到在 cursor.execute() 中花费了时间,例如针对 DBAPI:

2    0.102    0.102    0.204    0.102 {method 'execute' of 'sqlite3.Cursor' objects}


这表明数据库需要很长时间才能开始返回结果,这意味着应该通过添加索引或重构查询和/或底层架构来优化查询。对于该任务,需要使用数据库后端提供的 EXPLAIN、SHOW PLAN 等系统对查询计划进行分析。


结果获取缓慢 - 核心


另一方面,如果你看到数千个与获取行相关的调用,或者对 fetchall() 的调用很长,这可能意味着你的查询返回的行数比预期的要多,或者获取行本身很慢。ORM 本身通常使用 fetchall() 来获取行(如果使用 Query.yield_per() 选项,则使用 fetchmany() )。


在 DBAPI 级别对 fetchall() 的调用非常慢,将指示行数过长:

2    0.300    0.600    0.300    0.600 {method 'fetchall' of 'sqlite3.Cursor' objects}


即使最终结果似乎没有很多行,出乎意料的大量行也可能是笛卡尔积的结果 - 当多组行组合在一起而没有适当地将表连接在一起时。如果在复杂查询中使用了错误的 Column 对象,则通常很容易使用 SQLAlchemy Core 或 ORM 查询产生此行为,从而引入其他意外的 FROM 子句。


另一方面,在 DBAPI 级别对 fetchall() 的快速调用,但在要求 SQLAlchemy 的 CursorResult 执行 fetchall() 时速度变慢,这可能表明数据类型的处理速度变慢,例如 Unicode 转换和类似类型:

# the DBAPI cursor is fast...
2    0.020    0.040    0.020    0.040 {method 'fetchall' of 'sqlite3.Cursor' objects}

...

# but SQLAlchemy's result proxy is slow, this is type-level processing
2    0.100    0.200    0.100    0.200 lib/sqlalchemy/engine/result.py:778(fetchall)


在某些情况下,后端可能正在执行不需要的类型级处理。更具体地说,在类型 API 中看到缓慢的调用是更好的指标 - 下面是我们使用此类类型时的外观:

from sqlalchemy import TypeDecorator
import time


class Foo(TypeDecorator):
    impl = String

    def process_result_value(self, value, thing):
        # intentionally add slowness for illustration purposes
        time.sleep(0.001)
        return value


这个故意放慢速度的作的分析输出可以看作是这样的:

200    0.001    0.000    0.237    0.001 lib/sqlalchemy/sql/type_api.py:911(process)
200    0.001    0.000    0.236    0.001 test.py:28(process_result_value)
200    0.235    0.001    0.235    0.001 {time.sleep}


也就是说,我们在 type_api 系统中看到许多昂贵的调用,而实际耗时的是 time.sleep() 调用。


请务必查看 Dialect 文档 有关此级别的已知性能优化建议的注释,尤其是 像 Oracle 这样的数据库。 可能存在与确保数字准确性相关的系统 或可能并非在所有情况下都需要的字符串处理。


可能还有更多的低级点,行获取性能受到影响;例如,如果花费的时间似乎集中在像 socket.receive() 这样的调用上,这可能表明除了实际的网络连接之外,一切都很快,并且花费了太多时间在通过网络移动数据上。


结果获取缓慢 - ORM


为了检测 ORM 获取行的缓慢情况(这是性能问题最常见的领域),像 populate_state()_instance() 这样的调用将说明各个 ORM 对象群:

# the ORM calls _instance for each ORM-loaded row it sees, and
# populate_state for each ORM-loaded row that results in the population
# of an object's attributes
220/20    0.001    0.000    0.010    0.000 lib/sqlalchemy/orm/loading.py:327(_instance)
220/20    0.000    0.000    0.009    0.000 lib/sqlalchemy/orm/loading.py:284(populate_state)


ORM 将行转换为 ORM 映射对象的速度很慢,这是此作的复杂性与 cPython 开销相结合的产物。缓解这种情况的常见策略包括:


  • 获取单个列而不是完整实体,即:

    select(User.id, User.name)


    而不是:

    select(User)

  • 使用 Bundle 对象来组织基于列的结果:

    u_b = Bundle("user", User.id, User.name)
    a_b = Bundle("address", Address.id, Address.email)
    
    for user, address in session.execute(select(u_b, a_b).join(User.addresses)):
        ...

  • 使用结果缓存 - 请参阅 Dogpile 缓存 有关此内容的深入示例。


  • 考虑使用更快的解释器,例如 PyPy。


配置文件的输出可能有点令人生畏,但经过一些练习后,它们非常容易阅读。


另请参阅


性能 - 一套具有捆绑性能分析功能的性能演示。


我用 ORM 插入了 400,000 行,它真的很慢!


ORM 插入的性质已经改变,因为从 SQLAlchemy 2.0 开始,大多数包含的驱动程序都使用带有 insertmanyvalues 支持的 RETURNING。有关详细信息,请参阅现在为 MySQL 以外的所有后端实施的优化 ORM 批量插入部分。


总体而言,除 MySQL 以外的 SQLAlchemy 内置驱动程序现在应该提供非常快的 ORM 批量插入性能。


第三方驱动程序也可以选择加入新的批量基础设施,只需更改一些小的代码,前提是它们的后端支持必要的语法。SQLAlchemy 开发人员会鼓励第三方方言的用户发布这些驱动程序的问题,以便他们可以联系 SQLAlchemy 开发人员寻求帮助。