限制 *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 OFFSETLIMIT。正如 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

Django QuerySet 包含重复的条目

上传文件限制导致413-Request Entity Too Large

Django - ModelChoiceField 查询集如何工作?

NoReverseMatch at ...... in django

“Request Entity Too Large” 上传图片出现大小限制