内存有效的内置SqlAlchemy迭代器/生成器?


90

我有一个〜10M记录的MySQL表,可以使用SqlAlchemy进行交互。我发现对这个表的大子集的查询将消耗过多的内存,即使我以为我使用的是内置生成器,它可以智能地获取数据集的一口大小的块:

for thing in session.query(Things):
    analyze(thing)

为了避免这种情况,我发现我必须构建自己的迭代器,该迭代器会分块地进行处理:

lastThingID = None
while True:
    things = query.filter(Thing.id < lastThingID).limit(querySize).all()
    if not rows or len(rows) == 0: 
        break
    for thing in things:
        lastThingID = row.id
        analyze(thing)

这是正常现象还是关于SA内置发电机我缺少什么?

这个问题的答案似乎表明内存消耗是不希望的。


我有非常相似的东西,除了它产生“事物”。比所有其他解决方案更好地工作
iElectric

2
是Thing.id> lastThingID吗?什么是“行”?
2013年

Answers:


118

大多数DBAPI实现在获取行时都会完全缓冲行-因此通常,在SQLAlchemy ORM甚至没有保留一个结果之前,整个结果集就在内存中。

但是,有效的方法Query是在返回对象之前默认情况下完全加载给定的结果集。这里的基本原理是查询不只是简单的SELECT语句。例如,在连接到可能在一个结果集中多次返回相同对象标识的其他表中(与急切加载相同),整个行集都需要存储在内存中,以便可以返回正确的结果,否则返回集合。可能仅被部分填充。

因此Query提供了一个通过更改此行为的选项yield_per()。此调用将导致Query批量生成行,并在其中指定批量大小。正如文档所述,这仅在您不进行任何急切加载集合的情况下才是合适的,因此基本上是您真的知道自己在做什么。同样,如果底层的DBAPI预缓冲行,则仍然会有内存开销,因此该方法的伸缩性仅比不使用它更好。

我很少用过yield_per(); 取而代之的是,我使用上面建议的使用窗口函数的LIMIT方法的更好版本。LIMIT和OFFSET存在一个巨大的问题,即非常大的OFFSET值会导致查询变得越来越慢,因为N的OFFSET会使它分页浏览N行-就像执行相同的查询而不是一次,每次查询50次行数越来越大。使用窗口函数方法,我预取了一组“窗口”值,这些值引用了我要选择的表的块。然后,我发出单独的SELECT语句,每个语句一次从这些窗口之一拉出。

窗口函数方法在Wiki上,我使用它非常成功。

另请注意:并非所有数据库都支持窗口功能。您需要Postgresql,Oracle或SQL Server。恕我直言,至少使用Postgresql绝对值得-如果您使用的是关系数据库,则最好使用最佳数据库。


您提到查询实例化了所有用于比较身份的内容。是否可以通过对主键进行排序并仅比较连续结果来避免这种情况?
东武2012年

问题是,如果您生成一个标识为X的实例,则应用程序将获得它的所有权,然后根据该实体进行决策,甚至可能对其进行突变。稍后,也许(实际上通常)甚至在下一行中,结果也会返回相同的标识,也许会在其集合中添加更多内容。因此,应用程序收到的对象处于不完整状态。排序无济于事,因为最大的问题是急切加载的工作方式-“联合”加载和“子查询”加载都有不同的问题。
zzzeek 2012年

我了解“下一行更新集合”的情况,在这种情况下,您只需要向前看一个数据库行就可以知道集合何时完成。急切加载的实现必须与排序配合,以便始终在相邻行上进行集合更新。
东武

当您确信要发出的查询与传递部分结果集兼容时,yield_per()选项始终存在。我花了几天的时间参加马拉松比赛,试图在所有情况下都启用这种行为,但总是模糊不清,也就是说,直到您的程序使用其中之一时,边缘才失败。特别地,不能假设依赖排序。与往常一样,我欢迎您提供实际的代码。
zzzeek 2012年

1
由于我使用的是postgres,因此似乎可以使用Repeatable Read只读事务并在该事务中运行所有窗口查询。
2014年

23

我不是数据库专家,但是当使用SQLAlchemy作为简单的Python抽象层(即,不使用ORM查询对象)时,我想出了一个令人满意的解决方案,可以查询300M行表而不会增加内存使用量...

这是一个虚拟的示例:

from sqlalchemy import create_engine, select

conn = create_engine("DB URL...").connect()
q = select([huge_table])

proxy = conn.execution_options(stream_results=True).execute(q)

然后,我使用SQLAlchemyfetchmany()方法在无限while循环中遍历结果:

while 'batch not empty':  # equivalent of 'while True', but clearer
    batch = proxy.fetchmany(100000)  # 100,000 rows at a time

    if not batch:
        break

    for row in batch:
        # Do your stuff here...

proxy.close()

这种方法使我可以进行所有类型的数据聚合,而没有任何危险的内存开销。

NOTE stream_results与Postgres的和作品pyscopg2适配器,但我想这不会有任何DBAPI工作,也没有与任何数据库驱动程序...

这篇博客文章中有一个有趣的用例,启发了我的上述方法。


1
如果正在使用postgres或mysql(使用pymysql),则应为恕我直言。
井上由纪(Yuki Inoue)

1
救了我的命,看到我的查询越来越慢。我已经在pyodbc(从sql server到postgres)上检测了以上内容,并且运行起来就像一个梦。
埃德·贝克

对我来说,这是最好的方法。当我使用ORM时,我需要将SQL编译为我的方言(Postgres),然后直接从连接(而不是从会话)执行,如上所示。我在另一个问题stackoverflow.com/questions/4617291中找到了“如何”编译。速度提高很大。从JOINS更改为SUBQUERIES也使性能大大提高。还建议使用sqlalchemy_mixins,使用smart_query对建立最有效的查询有很大帮助。github.com/absent1706/sqlalchemy-mixins
GustavoGonçalves,

14

我一直在研究SQLAlchemy的高效遍历/分页,并希望更新此答案。

我认为您可以使用slice调用来适当地限制查询的范围,并且可以有效地重用它。

例:

window_size = 10  # or whatever limit you like
window_idx = 0
while True:
    start,stop = window_size*window_idx, window_size*(window_idx+1)
    things = query.slice(start, stop).all()
    if things is None:
        break
    for thing in things:
        analyze(thing)
    if len(things) < window_size:
        break
    window_idx += 1

这似乎非常简单和快速。我不确定.all()是否有必要。我注意到第一次通话后速度有了很大提高。
hamx0r 2015年

@ hamx0r我意识到这是一个旧评论,所以只留给后代。没有.all()的事变量是不支持LEN()查询
大卫·

9

本着乔尔的回答精神,我使用以下内容:

WINDOW_SIZE = 1000
def qgen(query):
    start = 0
    while True:
        stop = start + WINDOW_SIZE
        things = query.slice(start, stop).all()
        if len(things) == 0:
            break
        for thing in things:
            yield thing
        start += WINDOW_SIZE

Things = query.slice(start,stop).all()将在末尾返回[],而while循环将永不中断
Martin Reguly

4

使用LIMIT / OFFSET不好,因为您需要先找到所有{OFFSET}列,所以OFFSET越大-您得到的请求越长。对我来说,使用窗口式查询还会在包含大量数据的大表上产生不好的结果(您等待第一个结果的时间太长,以至于我不适合分块的Web响应)。

此处给出的最佳方法https://stackoverflow.com/a/27169302/450103。就我而言,我仅在datetime字段上使用索引并使用datetime> = previous_datetime获取下一个查询就解决了问题。愚蠢的,因为我之前在不同情况下都使用过该索引,但认为对于获取所有数据的窗口式查询会更好。就我而言,我错了。


3

AFAIK,第一个变体仍然从表中获取所有元组(带有一个SQL查询),但是在迭代时为每个实体建立ORM表示。因此,它比在迭代之前建立所有实体的列表更有效,但是您仍然必须将所有(原始)数据提取到内存中。

因此,在大型表上使用LIMIT对我来说是个好主意。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.