SqlAlchemy 面向只读对象模型的优化

14

我有一个复杂的对象网络,这些对象是使用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 扩展会更好。有什么想法吗?
3个回答

10

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

如果你想要一种创建廉价纯Python克隆的方法,请与原始对象共享字典对象:

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

1
很不幸,使用模式是在许多小对象之间进行许多计算,因此本地缓存并不是很有用。克隆的想法听起来确实是正确的方法,感谢您的快速提示。您的最后评论正是我想要的:请要求映射器创建一个“未插桩”的类,因为我知道它是只读的。 - CarlS
非常感谢!我迫不及待想试试这个。 - CarlS
1
我已经开始尝试实现建议的映射器hack,并且时间差异是令人鼓舞的。对于一个简单的循环:for i in xrange(500000): foo = readonlyobj.attr_bar使用正常的工具:2.663秒 使用只读映射器hack:0.078秒在我看来,这是非常显著的结果,所以再次感谢。我仍在努力真正理解它的工作原理,并且这被证明是学习SQLAlchemy更深入的好方法。 - CarlS

0

你应该能够在相关的关系上禁用惰性加载,这样SQLAlchemy将在单个查询中获取它们所有。


重点不在查询速度上,而在于执行数千次“已检测”对象属性访问的简单开销,即“foo.property”。 - CarlS
这种使用模式在进行懒加载时,往往会为每个循环的每次迭代生成一个单独的选择语句。(通常在测试运行期间打开SQL输出时可见。)这就是为什么我的第一反应是这样的。 - Travis Bradshaw
好的,我会仔细检查:上次调试时,我记得在前期看到了一堆 SQL,但是在循环过程中没有看到。需要指出的是,我正在编写蒙特卡罗模拟器,因此这些循环要运行数十万次(我需要确认获取容器的 SQL 是否只执行一次)。 - CarlS
1
啊,那太好了。SQLAlchemy 一定是通过迭代你的 .collection 属性来获取它们所有的结果。通常,对于所有以“使用 SQLAlchemy 进行某些缓慢操作”形式开始的故障排除,我的“第一步”是打开 SQL 输出,以确保它正在执行我认为它应该执行的操作。如果是这样,那么我就继续进行。如果不是,那么就是调整算法或映射器的时间了。 - Travis Bradshaw
好的,不要那么快 :) 正如我上次所说,这不是一个问题,但是你的评论会让我再次确认。我相信SQLAlchemy ORM已经给了我很多优势,使得复杂的对象图形能够运行起来,我唯一的抱怨就是仪表化。我经常使用日志记录,通常在分析步骤之前。我希望通过像上面描述的将映射器黑客到非仪表化的纯Python对象,从中挤出一些速度。如果失败了,那么可能就要用C扩展,直接使用sqlite api进入一堆c结构体了。 - CarlS
显示剩余2条评论

-1
尝试使用JOINs的单个查询而不是Python循环。

谢谢,但 ORM 的意义不就在于智能填充那些容器吗?我不想失去这个好处。我也进行了一些有限的测试,事实上运行一个大查询并逐行处理 ResultProxy 可能会更慢,此时我仍需要支付“foo.property”访问费用。 - CarlS
2
ORM只是一种方便的工具,使得以面向对象的方式操作关系型数据库更加容易。它并不是为了将关系型数据库中的关系去除。 - ebo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接