【问题标题】:SQLAlchemy: Scan huge tables using ORM?SQLAlchemy:使用 ORM 扫描大表?
【发布时间】:2010-11-11 20:48:43
【问题描述】:

我目前正在玩一些 SQLAlchemy,这真的很整洁。

为了测试,我创建了一个包含我的图片存档的巨大表,由 SHA1 哈希索引(以删除重复 :-))。这是令人印象深刻的快...

为了好玩,我对生成的 SQLite 数据库执行了相当于 select * 的操作:

session = Session()
for p in session.query(Picture):
    print(p)

我希望看到哈希滚动,但它只是继续扫描磁盘。与此同时,内存使用量猛增,几秒钟后就达到了 1GB。这似乎来自 SQLAlchemy 的身份映射功能,我认为它只是保留弱引用。

谁能给我解释一下?我以为每张图片p都会在写出hash后被收集!?

【问题讨论】:

  • 我自己也注意到了同样的问题。如果我做len(Session.query(Model).limit(100).all()),我会得到1。如果我删除limit,内存使用量会猛增。

标签: python performance orm sqlalchemy


【解决方案1】:

好的,我自己找到了一种方法。将代码更改为

session = Session()
for p in session.query(Picture).yield_per(5):
    print(p)

一次只加载 5 张图片。默认情况下,查询似乎一次加载所有行。但是,我还不明白该方法的免责声明。引用SQLAlchemy docs

警告:谨慎使用此方法;如果同一个实例出现在多于一批的行中,最终用户对属性的更改将被覆盖。 特别是,通常不可能将此设置与急切加载的集合(即任何lazy=False)一起使用,因为在后续结果批处理中遇到这些集合时将被清除以进行新的加载。

那么,如果使用yield_per 实际上是正确的方法(tm),在使用 ORM 时扫描大量 SQL 数据,那么什么时候使用它是安全的?

【讨论】:

  • yield_per 在每个结果实例都有一个结果 SQL 行时是安全的。当您 eagerload 或加入一对多关系时,每个实例都会获得额外的行。如果您需要有关情况的更多详细信息,可以创建一个关于 yield_per 的单独问题。
  • 注意事项:如果您在执行此操作时尝试提交,则会引发异常。见stackoverflow.com/questions/12233115/…
  • 在与 MySQL 中的内存泄漏进行了长期斗争之后,我在 yield_per 文档中看到了这一点:Also note that while yield_per() will set the stream_results execution option to True, currently this is only understood by psycopg2 dialect which will stream results using server side cursors instead of pre-buffer all rows for this query. Other DBAPIs pre-buffer all rows before making them available. 解决方法:stackoverflow.com/a/3699677/281469
【解决方案2】:

您可以将图片推迟到仅在访问时检索。您可以逐个查询地执行此操作。 喜欢

session = Session()
for p in session.query(Picture).options(sqlalchemy.orm.defer("picture")):
    print(p)

或者你可以在映射器中完成

mapper(Picture, pictures, properties={
   'picture': deferred(pictures.c.picture)
})

如何做到这一点在文档here

无论哪种方式都可以确保仅在您访问该属性时才加载图片。

【讨论】:

  • 谢谢大卫,这很有趣。实际上,我只是将代码更改为使用 from sqlalchemy.orm import deferred ... class Picture(object): ... imagedata = deferred(Column(Binary)) 这是一个很大的改进。但是,由于为查询创建了几个 1000 个图片对象,因此在输出第一个结果之前仍然需要几秒钟。我希望在 SQLAlchemy 遍历 SQLite 结果行时一次创建一个对象。
  • 我认为大部分时间将花在从数据库中检索数据而不是制作对象上。所以查询本身需要有限制。所以。 for p in session.query(Picture)[0:5]: print p.
【解决方案3】:

对于这种情况,我通常会这样做:

def page_query(q):
    offset = 0
    while True:
        r = False
        for elem in q.limit(1000).offset(offset):
           r = True
           yield elem
        offset += 1000
        if not r:
            break

for item in page_query(Session.query(Picture)):
    print item

这避免了 DBAPI 也执行的各种缓冲(例如 psycopg2 和 MySQLdb)。如果您的查询具有显式 JOIN,则仍然需要适当地使用它,尽管急切加载的集合可以保证完全加载,因为它们应用于提供了实际 LIMIT/OFFSET 的子查询。

我注意到 Postgresql 返回大型结果集的最后 100 行所需的时间几乎与返回整个结果所需的时间一样长(减去实际的行获取开销),因为 OF​​FSET 只是对整个结果进行简单扫描东西。

【讨论】:

  • 我认为您可以摆脱循环中的一项分配,例如:while True: elem = None;对于 elem .... 偏移量 += 1000;如果 elem==None: 中断;偏移量 += 1000
  • 不,戴夫,你需要在循环结束时设置elem = None,否则它永远不会终止。
  • 请注意,对查询进行切片将产生相同的效果:q[offset:offset+1000]
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-02-21
  • 2013-06-05
  • 2021-05-25
  • 2019-05-16
相关资源
最近更新 更多