限制 *Large* Django QuerySet 中的内存使用
Posted
技术标签:
【中文标题】限制 *Large* Django QuerySet 中的内存使用【英文标题】:Limiting Memory Use in a *Large* Django QuerySet 【发布时间】:2011-06-18 22:01:46 【问题描述】:我有一个任务需要每隔一段时间(每天一次,每周一次,等等)在我的数据库中的“大多数”对象上运行一次。基本上这意味着我有一些查询看起来像在它自己的线程中运行。
for model_instance in SomeModel.objects.all():
do_something(model_instance)
(请注意,它实际上是一个 filter() 不是 all(),但尽管如此,我仍然最终选择了 非常大 组对象。)
我遇到的问题是,在运行一段时间后,由于我使用了太多内存,我的托管服务提供商杀死了该线程。我假设所有这些内存使用都在发生,因为即使我的查询返回的 QuerySet
对象最初具有非常小的内存占用,它最终会随着 QuerySet
对象缓存每个 @987654325 而增长@ 我遍历它们。
我的问题是,“以内存高效的方式遍历数据库中几乎每个 SomeModel
的最佳方法是什么?”或者我的问题是“如何从 django 查询集中‘取消缓存’模型实例?”
编辑:我实际上是在使用查询集的结果来构建一系列新对象。因此,我根本不会更新查询的对象。
【问题讨论】:
您必须提供一些关于您正在使用查询集做什么的提示。 Django 有规则,许多操作需要将整个 QuerySet 加载到内存中,而其他操作只是一次处理一个行。 docs.djangoproject.com/en/1.2/topics/db/queries/…。请提供一些关于您如何使用 QuerySet 对象的提示。 抱歉,我应该指定我正在使用来自 QuerySet 对象的信息来创建(不同类型的)新对象。所以我从来没有真正更新我正在查询的对象。 【参考方案1】:如何使用此处记录的 django core 的 Paginator 和 Page 对象:
https://docs.djangoproject.com/en/dev/topics/pagination/
类似这样的:
from django.core.paginator import Paginator
from djangoapp.models import SomeModel
paginator = Paginator(SomeModel.objects.all(), 1000) # chunks of 1000
for page_idx in range(1, paginator.num_pages):
for row in paginator.page(page_idx).object_list:
# here you can do what you want with the row
print "done processing page %s" % page_idx
【讨论】:
为什么这不是公认的答案?因为它是一个原生的 django 解决方案,而且似乎是最省力的工作 出于好奇,这不是类似于iterator()
吗? docs.djangoproject.com/en/2.1/ref/models/querysets/#iterator 实际上,Paginator 会先调用 count
(否则为 len)。它不会使效率稍微低一些吗?为什么这个选项比使用迭代器更好?
应该是for page_idx in range(1, paginator.num_pages+1):
否则你会跳过最后一页【参考方案2】:
所以我实际上最终做的是构建一些可以“包装”查询集的东西。它通过使用切片语法制作查询集的深层副本来工作——例如,some_queryset[15:45]
——但随后它使当切片完全迭代时,原始 QuerySet 的另一个深拷贝。这意味着只有在“this”特定切片中返回的对象集存储在内存中。
class MemorySavingQuerysetIterator(object):
def __init__(self,queryset,max_obj_num=1000):
self._base_queryset = queryset
self._generator = self._setup()
self.max_obj_num = max_obj_num
def _setup(self):
for i in xrange(0,self._base_queryset.count(),self.max_obj_num):
# By making a copy of of the queryset and using that to actually access
# the objects we ensure that there are only `max_obj_num` objects in
# memory at any given time
smaller_queryset = copy.deepcopy(self._base_queryset)[i:i+self.max_obj_num]
logger.debug('Grabbing next %s objects from DB' % self.max_obj_num)
for obj in smaller_queryset.iterator():
yield obj
def __iter__(self):
return self
def next(self):
return self._generator.next()
所以不是...
for obj in SomeObject.objects.filter(foo='bar'): <-- Something that returns *a lot* of Objects
do_something(obj);
你会做...
for obj in MemorySavingQuerysetIterator(in SomeObject.objects.filter(foo='bar')):
do_something(obj);
请注意,这样做的目的是节省内存在你的Python解释器中。它本质上是通过more 数据库查询来做到这一点的。通常人们正试图做与此完全相反的事情——即尽可能减少数据库查询而不考虑内存使用情况。希望有人会发现这很有用。
【讨论】:
【参考方案3】:您不能使用Model.objects.all().iterator()
,因为它会一次获取您表上的所有元素。你也不能使用Model.objects.all()[offset:offset+pagesize]
,因为它会缓存结果。任何一个都会超出您的内存限制。
我尝试混合使用这两种解决方案,并且效果很好:
offset = 0
pagesize = 1000
count = Model.objects.all().count()
while offset < count:
for m in Model.objects.all()[offset : offset + pagesize].iterator:
do_something with m
offset += pagesize
更改pagesize
以满足您的要求,如果更适合您,可以选择将[offset : offset + pagesize]
更改为[offset * pagesize : (offset + 1) * pagesize]
成语。当然,也可以将 Model
替换为您的实际型号名称。
【讨论】:
“赶上你的结果”是什么意思? 我假设的缓存 QuerySet.iterator() 将使用偏移量来一一获取行而不缓存结果,具体取决于数据库。文档中特别提到了 Postgres 和 Oracle。 我不确定为什么父答案会谴责使用迭代器,因为(如@Derek 所述).iterator(batch_size=N)
似乎完全符合问题的要求......至少在 Postgres 中服务器端游标?例如,我有一个查询可以返回大约 1000 行(模型实例),其中每行包含大约 20MB 的 JSON 数据。如果没有 .iterator(),峰值内存使用量远远超过 20GB。使用 .iterator(batch_size=N) 完全解决了这个问题,它用内存与数据库往返交换。
没有。 queryset.iterator()
具有参数 chunk_size
并且只会获取每次迭代定义的行数。我在我的一个项目中使用了它,它确实有助于减少大型查询集的内存消耗。见:docs.djangoproject.com/en/3.2/ref/models/querysets/#iterator【参考方案4】:
许多解决方案通过对查询集进行切片来实现 sql OFFSET
和 LIMIT
。正如 stefano 所指出的,对于更大的数据集,这变得非常低效。处理此问题的正确方法是使用服务器端光标来跟踪偏移量。
本机服务器端光标支持是in the works for django。在准备好之前,如果您使用带有 psycopg2 后端的 postgres,这里是一个简单的实现:
def server_cursor_query(Table):
table_name = Table._meta.db_table
# There must be an existing connection before creating a server-side cursor
if connection.connection is None:
dummy_cursor = connection.cursor() # not a server-side cursor
# Optionally keep track of the columns so that we can return a QuerySet. However,
# if your table has foreign keys, you may need to rename them appropriately
columns = [x.name for x in Table._meta.local_fields]
cursor = connection.connection.cursor(name='gigantic_cursor')) # a server-side
# cursor
with transaction.atomic():
cursor.execute('SELECT FROM WHERE id='.format(
', '.join(columns), table_name, id))
while True:
rows = cursor.fetchmany(1000)
if not rows:
break
for row in rows:
fields = dict(zip(columns, row))
yield Table(**fields)
请参阅this blog post,了解有关 django 中大型查询的内存问题的详细说明。
【讨论】:
为工作使用正确的工具而大加 1。确实很高兴看到 Django ORM 的支持。顺便说一句,如果您不介意稍微了解一下内部结构,可以调用sql, params = queryset.query.get_compiler(using=queryset.db).as_sql()
从查询集中获取 SQL 查询。您应该使用 Table.from_db 将其转换为最新 Django 版本的实际实例。
@spectras, .as_sql()
看起来很有用。但是,我需要以与 .as_sql()
SELECT
语句相同的顺序获取字段名称,以便在最后创建 Table 实例。有没有办法在不手动解析.as_sql()
的情况下做到这一点?
嗯,是的,如果你不介意四处寻找,我把它放在这里:gist.github.com/spectras/f22d303088e4b2c498de 如果你确实使用它,我建议设置一些测试来简化 Django 升级。也可以添加对 select_related() 的支持,这将是 3 行,但与另一个 ORM 内部有额外的联系,所以...【参考方案5】:
为此有一个 django sn-p:
http://djangosnippets.org/snippets/1949/
它通过产生原始查询集的较小“块”行来迭代查询集。它最终使用更少的内存,同时允许您调整速度。我在我的一个项目中使用它。
【讨论】:
感谢分享尼克!【参考方案6】:我正在继续研究,看起来我想做相当于 SQL OFFSET 和 LIMIT 的操作,根据Django Doc's on Limiting Querysets,这意味着我想使用切片语法,例如,SomeModel.objects.all()[15:25]
所以现在我在想,也许这就是我正在寻找的东西:
# Figure out the number of objects I can safely hold in memory
# I'll just say 100 for right now
number_of_objects = 100
count = SomeModel.objects.all().count():
for i in xrange(0,count,number_of_objects):
smaller_queryset = SomeModel.objects.all()[i:i+number_of_objects]
for model_instance in smaller_queryset:
do_something(model_instance)
据我估计,这将使smaller_queryset
永远不会变得太大。
【讨论】:
【参考方案7】:以下是一个查询集迭代器,它将查询集拆分为块,并且不会比基本迭代器慢多少(它将是数据库查询的线性数量,而不是 1,但每 1,000 行只有一个查询) .此函数按主键分页,这是高效实现所必需的,因为在大多数 SQL 数据库中,偏移量是线性时间操作。
def queryset_iterator(queryset, page_size=1000):
if not queryset:
return
max_pk = queryset.order_by("-pk")[0].pk
# Scale the page size up by the average density of primary keys in the queryset
adjusted_page_size = int(page_size * max_pk / queryset.count())
pages = int(max_pk / adjusted_page_size) + 1
for page_num in range(pages):
lower = page_num * adjusted_page_size
page = queryset.filter(pk__gte=lower, pk__lt=lower+page_size)
for obj in page:
yield obj
使用看起来像:
for obj in queryset_iterator(Model.objects.all()):
# do stuff
这段代码有三个假设:
-
您的主键是整数(这不适用于 UUID 主键)。
查询集的主键至少在某种程度上是均匀分布的。如果不是这样,
adjusted_page_size
最终可能会太大,您可能会在迭代过程中获得一个或多个大页面。
为了了解开销,我在一个包含 40,000 个条目的 Postgres 表上进行了测试。与原始迭代相比,queryset_iterator 增加了大约 80% 的迭代时间(2.2 秒对 1.2 秒)。对于 200 到 10,000 之间的页面大小,该开销并没有太大变化,尽管它开始上升到 200 以下。
【讨论】:
【参考方案8】:以下方法不使用昂贵的数据库偏移查询,避免计算页码,使其更高效。 文档字符串中指定的限制。
def queryset_pk_iterator(queryset, batch_size=1000):
"""
Iterator that splits the queryset into batches to reduce memory consumption.
Useful in cases where builtin .iterator() method of the queryset skips the "prefetch_related" optimization.
:param queryset: Queryset to iterate over. The supplied queryset must not specify order and limit/offset.
Queryset objects must have a monotonically increasing and ordering primary key.
:param batch_size: Size of the batches into which to split the queryset.
:return: iterator object
"""
pk = None
while True:
batch_queryset = queryset.order_by('pk')
if pk is not None:
batch_queryset = batch_queryset.filter(pk__gt=pk)
batch_queryset = batch_queryset[:batch_size]
obj = None
for obj in batch_queryset:
yield obj
if obj is None:
return
pk = obj.pk
【讨论】:
以上是关于限制 *Large* Django QuerySet 中的内存使用的主要内容,如果未能解决你的问题,请参考以下文章
Django 服务器上的 413 Payload Too Large
上传文件限制导致413-Request Entity Too Large
Django - ModelChoiceField 查询集如何工作?