SQLAlchemy:使用ORM扫描大表?

49 投票
3 回答
35490 浏览
提问于 2025-04-15 12:59

我最近在玩SQLAlchemy,感觉真的很不错。

为了测试,我创建了一个巨大的表,里面存放着我的图片档案,使用SHA1哈希来索引(这样可以去掉重复的图片 :-))。这个过程快得让我印象深刻……

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

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

我原本以为会看到哈希值一闪而过,但结果却一直在扫描硬盘。同时,内存使用量也在飞速上升,几秒钟后就达到了1GB。这似乎是因为SQLAlchemy的身份映射功能,我原以为它只是保持弱引用。

有人能给我解释一下吗?我以为每个图片p在哈希值写出后就会被收集掉啊!?

3 个回答

9

你可以选择在需要的时候再去获取图片,这样可以节省资源。你可以针对每个查询单独设置。

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

或者你也可以在映射器里设置。

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

具体怎么做,可以在文档中找到详细说明,点击这里查看。

无论你选择哪种方式,都会确保只有在你真正访问这个属性的时候,图片才会被加载。

37

这是我通常在这种情况下的做法:

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

这样可以避免数据库API(比如psycopg2和MySQLdb)在处理数据时的一些缓冲问题。不过,如果你的查询中有明确的JOIN操作,还是需要谨慎使用。尽管提前加载的集合会确保完全加载,因为它们是应用在一个有实际LIMIT/OFFSET的子查询上的。

我注意到,Postgresql在返回一个大结果集的最后100行时,所花的时间几乎和返回整个结果集一样(不算实际获取行的时间),因为OFFSET只是简单地扫描整个结果。

63

好的,我刚刚找到了一种自己解决这个问题的方法。把代码改成

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

这样的话,每次只加载5张图片。看起来默认情况下,查询会一次性加载所有的数据行。不过,我对这个方法的警告还不是很明白。引用自SQLAlchemy文档

警告:使用这个方法要小心;如果同一个实例出现在多个数据行的批次中,最终用户对属性的修改会被覆盖。特别是,通常不可能在急切加载的集合(也就是任何lazy=False的情况)中使用这个设置,因为当在后续的结果批次中遇到这些集合时,它们会被清空以进行新的加载。

所以,如果使用yield_per确实是处理大量SQL数据时使用ORM的“正确方法”,那么什么时候使用它才是安全的呢?

撰写回答