我有一个~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内置发电机上缺少的东西?
this question的答案似乎表明内存消耗是不可预期的。
答案 0 :(得分:104)
大多数DBAPI实现在获取行时完全缓冲行 - 通常,在SQLAlchemy ORM甚至获得一个结果之前,整个结果集都在内存中。
但是,然后,Query的工作方式是它在返回给你的对象之前默认完全加载给定的结果集。这里的基本原理认为查询不仅仅是简单的SELECT语句 - 连接到其他表,这些表可能在一个结果集中多次返回相同的对象标识(与急切加载相同),整个行集需要在内存中,所以可以返回正确的结果 - 否则集合等可能只是部分填充。
因此,Query提供了一个更改此行为的选项,即yield_per()调用http://www.sqlalchemy.org/docs/orm/query.html?highlight=yield_per#sqlalchemy.orm.query.Query.yield_per。此调用将导致Query批量生成行,您可以在其中为批量指定大小。正如文档所述,这只适用于你没有做任何急切的集合加载 - 所以基本上如果你真的知道你在做什么。而且,如果底层DBAPI预先缓冲行,那么仍然会有内存开销,因此该方法的扩展性略好于不使用它。
我几乎没有使用yield_per() - 相反,我使用上面使用窗口函数建议的LIMIT方法的更好版本。 LIMIT和OFFSET有一个巨大的问题,即非常大的OFFSET值会导致查询变得越来越慢,因为N的OFFSET会导致它遍历N行 - 就像执行相同的查询五十次而不是一次,每次读取一次行数越来越多。使用窗口函数方法,我预取一组“窗口”值,这些值引用我想要选择的表的块。然后我发出单独的SELECT语句,每个语句一次从其中一个窗口拉出。
窗口函数方法在http://www.sqlalchemy.org/trac/wiki/UsageRecipes/WindowedRangeQuery的维基上,我使用它非常成功。
另请注意,并非所有数据库都支持窗口功能 - 您需要PG,Oracle或SQL Server。恕我直言至少使用Postgresql绝对是值得的 - 如果你使用关系数据库,你也可以使用最好的。
答案 1 :(得分:12)
我一直在研究SQLAlchemy的高效遍历/分页,并希望更新这个答案。
我认为您可以使用切片调用来正确限制查询的范围,并且可以有效地重用它。
示例:
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
答案 2 :(得分:6)
我不是数据库专家,但是当使用SQLAlchemy作为一个简单的Python抽象层(即,不使用ORM Query对象)时,我想出了一个令人满意的解决方案来查询300M行表而不会爆炸内存使用...
这是一个虚拟的例子:
result[0].replace('\\\\', "\\")
然后,我使用SQLAlchemy 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)
方法在fetchmany()
循环中迭代结果:
while
这种方法允许我进行各种数据聚合,而不会产生任何危险的内存开销。
empty = False
while not empty:
batch = proxy.fetchmany(100000) # 100,000 rows at a time
if not batch:
empty = True
for row in batch:
# Do your stuff here...
proxy.close()
NOTE
适用于Postgres和stream_results
适配器,但我想它不适用于任何DBAPI,也不适用于任何数据库驱动程序...... < / em>的
这个blog post中有一个有趣的用例启发了我的上述方法。
答案 3 :(得分:5)
根据Joel的回答,我使用以下内容:
WINDOW_SIZE = 1000
def qgen(query):
start = 0
while True:
stop = start + WINDOW_SIZE
things = query.slice(start, stop).all()
if things is None:
break
for thing in things:
yield(thing)
start += WINDOW_SIZE
答案 4 :(得分:2)
AFAIK,第一个变体仍然从表中获取所有元组(带有一个SQL查询),但在迭代时为每个实体构建ORM表示。因此,它比在迭代之前构建所有实体的列表更有效,但您仍然需要将所有(原始)数据提取到内存中。
因此,在巨大的桌子上使用LIMIT对我来说听起来不错。
答案 5 :(得分:2)
使用LIMIT / OFFSET是不好的,因为您需要先找到所有{OFFSET}列,因此较大的是OFFSET - 您获得的请求时间越长。 对我使用窗口查询也会在包含大量数据的大型表上给出错误的结果(您等待的结果时间过长,在我的情况下,对于分块的Web响应而言,这并不好。)
此处给出的最佳方法https://stackoverflow.com/a/27169302/450103。在我的情况下,我解决了问题,只需在datetime字段上使用index并使用datetime&gt; = previous_datetime获取下一个查询。愚蠢,因为我以前在不同情况下使用过该索引,但认为对于获取所有数据窗口查询会更好。就我而言,我错了。