【问题标题】:SqlAlchemy optimizations for read-only object models只读对象模型的 SqlAlchemy 优化
【发布时间】:2010-02-23 23:01:10
【问题描述】:

我有一个使用 sqlalchemy ORM 映射从 sqlite 数据库生成的复杂对象网络。我有不少嵌套很深:

for parent in owner.collection: 
    for child in parent.collection: 
        for foo in child.collection: 
            do lots of calcs with foo.property 

我的分析显示 sqlalchemy 工具在这个用例中花费了很多时间。

问题是:我永远不会在运行时更改对象模型(映射属性),因此一旦加载它们,我就不需要检测,或者实际上根本不需要任何 sqlalchemy 开销。经过大量研究,我想我可能必须从我已经加载的“仪表对象”中克隆一组“纯 python”对象,但这会很痛苦。

性能在这里非常重要(它是一个模拟器),所以直接使用 sqlite api 将这些层编写为 C 扩展可能是最好的。有什么想法吗?

【问题讨论】:

    标签: python performance sqlalchemy readonly


    【解决方案1】:

    如果您多次引用单个实例的单个属性,一个简单的技巧是将其存储在局部变量中。

    如果你想要一种方法来创建廉价的纯 python 克隆,请将 dict 对象与原始对象共享:

    class CheapClone(object):
        def __init__(self, original):
            self.__dict__ = original.__dict__
    

    创建这样的副本会花费大约一半的已检测属性访问,并且属性查找与正常情况一样快。

    可能还有一种方法可以让映射器创建未检测类的实例,而不是检测类的实例。如果我有时间,我可能会看看填充实例与检测类具有相同类型的假设有多根深蒂固。


    找到了一种快速而肮脏的方式,似乎至少在 0.5.8 和 0.6 上有些工作。没有使用继承或其他可能交互不良的功能对其进行测试。另外,这涉及到一些非公开的API,所以在更改版本时要小心损坏。

    from sqlalchemy.orm.attributes import ClassManager, instrumentation_registry
    
    class ReadonlyClassManager(ClassManager):
        """Enables configuring a mapper to return instances of uninstrumented 
        classes instead. To use add a readonly_type attribute referencing the
        desired class to use instead of the instrumented one."""
        def __init__(self, class_):
            ClassManager.__init__(self, class_)
            self.readonly_version = getattr(class_, 'readonly_type', None)
            if self.readonly_version:
                # default instantiation logic doesn't know to install finders
                # for our alternate class
                instrumentation_registry._dict_finders[self.readonly_version] = self.dict_getter()
                instrumentation_registry._state_finders[self.readonly_version] = self.state_getter()
    
        def new_instance(self, state=None):
            if self.readonly_version:
                instance = self.readonly_version.__new__(self.readonly_version)
                self.setup_instance(instance, state)
                return instance
            return ClassManager.new_instance(self, state)
    
    Base = declarative_base()
    Base.__sa_instrumentation_manager__ = ReadonlyClassManager
    

    使用示例:

    class ReadonlyFoo(object):
        pass
    
    class Foo(Base, ReadonlyFoo):
        __tablename__ = 'foo'
        id = Column(Integer, primary_key=True)
        name = Column(String(32))
    
        readonly_type = ReadonlyFoo
    
    assert type(session.query(Foo).first()) is ReadonlyFoo
    

    【讨论】:

    • 不幸的是,使用模式是跨许多小对象进行多次计算,因此本地缓存没有那么有用。克隆的想法听起来确实像要走的路,感谢您的快速提示。您的最终评论正是我想要的:让映射器创建一个“未检测”类,因为我知道它是只读的。
    • 非常感谢!我迫不及待想试试这个。
    • 我已经对建议的 mapper hack 进行了一些初步工作,时间差异令人鼓舞。对于一个简单的循环: for i in xrange(500000): foo = readonlyobj.attr_bar with normal instrumentation: 2.663 secs with readonly mapper hack: 0.078 secs 这是一个非常重要的结果 imo,所以再次感谢。我仍在尝试真正了解它是如何工作的,并且它被证明是更深入地学习 sqlalchemy 的好方法。
    【解决方案2】:

    您应该能够对相关关系禁用延迟加载,并且 sqlalchemy 将在单个查询中获取它们。

    【讨论】:

    • 与其说是查询的速度,不如说是对对象属性(即“foo.property”)进行数千次“插装”访问的简单开销。
    • 这种使用模式,当延迟加载时,通常会为每个循环的每次迭代生成一个单独的选择语句。 (如果您在测试运行期间打开 SQL 输出,通常可见。)这就是为什么我的第一反应是这样的。
    • 好的,我会仔细检查一下:上次我进行调试时,我记得在前面看到了一堆 SQL,但在循环本身期间却没有。我应该指出,我正在编写一个蒙特卡罗模拟器,所以这些循环正在运行 100000 次(我需要检查用于获取容器的 SQL 是否只执行一次)。
    • 啊,那太好了。由于迭代 .collection 属性,SQLAlchemy 必须全部获取它们。一般来说,对于所有以“使用 SQLAlchemy 做一些缓慢的事情”的形式开始的所有故障排除,我的“第一步”是打开 SQL 输出以确保它正在做我认为它正在做的事情。如果是,那我继续。如果不是,那么是时候调整算法或映射器了。
    • 另外,虽然我非常喜欢 SQLAlchemy,但如果性能是您的软件最重要的约束条件(例如,开发速度和易于维护),那么使用像 SQLAlchemy 这样的抽象库可能不是t 适合工作的工具。
    【解决方案3】:

    尝试使用带有 JOIN 的单个查询而不是 python 循环。

    【讨论】:

    • 谢谢,但是 ORM 的意义不在于这些容器将为我智能填充吗?我不想失去这个好处。我还做了一些有限的测试,实际上运行大查询并逐行处理 ResultProxy 可能会更慢,此时我仍在为“foo.property”访问付费。
    • ORM 只是为了方便以面向对象的方式使用 rdbms。它不能将关系从关系数据库中取出。
    猜你喜欢
    • 1970-01-01
    • 2011-11-18
    • 1970-01-01
    • 2011-09-20
    • 2021-12-30
    • 1970-01-01
    • 1970-01-01
    • 2012-10-01
    • 2021-08-27
    相关资源
    最近更新 更多