为什么要遍历占用大量内存的大型Django QuerySet?

dav*_*ers 104 sql django postgresql django-orm

该表包含大约一千万行.

for event in Event.objects.all():
    print event
Run Code Online (Sandbox Code Playgroud)

这导致内存使用量稳定增加到4 GB左右,此时行快速打印.第一行打印之前的漫长延迟令我感到惊讶 - 我预计它几乎可以立即打印出来.

我也尝试过Event.objects.iterator()以同样的方式行事.

我不明白Django加载到内存中的原因或为什么会这样做.我希望Django的通过在数据库级别的结果迭代,which'd意味着结果将在大约(长时间的等待之后,而不是一次全部)以恒定的速率进行打印.

我误解了什么?

(我不知道它是否是相关的,但我使用PostgreSQL.)

ete*_*ode 104

Nate C很接近,但并不完全.

来自文档:

您可以通过以下方式评估QuerySet:

  • 迭代.QuerySet是可迭代的,并且在您第一次迭代它时执行其数据库查询.例如,这将打印数据库中所有条目的标题:

    for e in Entry.objects.all():
        print e.headline
    
    Run Code Online (Sandbox Code Playgroud)

因此,当您第一次进入该循环并获取查询集的迭代形式时,将同时检索您的一千万行.您遇到的等待是Django加载数据库行并为每个行创建对象,然后返回您可以实际迭代的内容.然后你就拥有了记忆中的一切,结果就会溢出来.

从我阅读的文档来看,iterator()除了绕过QuerySet的内部缓存机制之外别无其他.我认为它可能是一个一个接一个的事情,但相反,你的数据库需要1000万次点击.也许不是那么可取.

有效地迭代大型数据集是我们仍然没有完全正确的事情,但是有一些片段可能会对您的目的有用:

  • 感谢@eternicode 的精彩回答。最后,我们使用原始 SQL 来实现所需的数据库级迭代。 (2认同)
  • @eternicode很好的答案,只是打了这个问题.从那以后Django有没有相关的更新? (2认同)
  • 自Django 1.11起的文档说iterator()确实使用服务器端游标。 (2认同)

mpa*_*paf 38

可能不是更快或更有效,但作为现成的解决方案,为什么不使用django core的Paginator和Page对象记录在这里:

https://docs.djangoproject.com/en/dev/topics/pagination/

像这样的东西:

from django.core.paginator import Paginator
from djangoapp.models import model

paginator = Paginator(model.objects.all(), 1000) # chunks of 1000, you can 
                                                 # change this to desired chunk size

for page in range(1, paginator.num_pages + 1):
    for row in paginator.page(page).object_list:
        # here you can do whatever you want with the row
    print "done processing page %s" % page
Run Code Online (Sandbox Code Playgroud)

  • 自邮政以来,现在可以进 `Paginator`现在有一个[`page_range`](https://docs.djangoproject.com/en/dev/topics/pagination/#django.core.paginator.Paginator.page_range)属性,以避免样板.如果寻找最小的内存开销,你可以使用[`object_list.iterator()`,它不会填充查询集缓存](https://docs.djangoproject.com/en/dev/ref/models/querysets/#django .db.models.query.QuerySet.iterator).然后需要[`prefetch_related_objects`](https://docs.djangoproject.com/en/1.10/ref/models/querysets/#prefetch-related-objects)进行预取 (3认同)

Luk*_*ore 26

Django的默认行为是在评估查询时缓存QuerySet的整个结果.您可以使用QuerySet的迭代器方法来避免此缓存:

for event in Event.objects.all().iterator():
    print event
Run Code Online (Sandbox Code Playgroud)

https://docs.djangoproject.com/en/dev/ref/models/querysets/#iterator

iterator()方法计算查询集,然后直接读取结果,而不在QuerySet级别进行缓存.当迭代您只需要访问一次的大量对象时,此方法可以获得更好的性能并显着减少内存.请注意,缓存仍在数据库级别完成.

使用iterator()减少了我的内存使用量,但它仍然高于我的预期.使用mpaf建议的分页器方法使用的内存要少得多,但对于我的测试用例来说要慢2-3倍.

from django.core.paginator import Paginator

def chunked_iterator(queryset, chunk_size=10000):
    paginator = Paginator(queryset, chunk_size)
    for page in range(1, paginator.num_pages + 1):
        for obj in paginator.page(page).object_list:
            yield obj

for event in chunked_iterator(Event.objects.all()):
    print event
Run Code Online (Sandbox Code Playgroud)


nat*_*e c 8

这来自以下文档:http: //docs.djangoproject.com/en/dev/ref/models/querysets/

在您执行评估查询集的操作之前,实际上不会发生数据库活动.

因此,当print event运行时,查询将触发(根据您的命令进行全表扫描)并加载结果.你要求所有的对象,没有办法得到第一个对象而没有获得所有对象.

但如果你做的事情如下:

Event.objects.all()[300:900]
Run Code Online (Sandbox Code Playgroud)

http://docs.djangoproject.com/en/dev/topics/db/queries/#limiting-querysets

然后它会在内部添加sql的偏移量和限制.


thc*_*ark 8

这里有很多过时的结果。不确定何时添加,但 Django 的QuerySet.iterator()方法使用具有块大小的服务器端游标来从数据库传输结果。因此,如果您使用的是 postgres,现在应该可以为您处理开箱即用的问题。


Fra*_*ens 7

对于大量记录,数据库游标执行得更好.你确实需要在Django中使用原始SQL,Django-cursor与SQL cursur不同.

Nate C建议的LIMIT - OFFSET方法可能足以满足您的需求.对于大量数据,它比光标慢,因为它必须一遍又一遍地运行相同的查询,并且必须跳过越来越多的结果.

  • 弗兰克,这绝对是一个好点,但很高兴看到一些代码细节来推动解决方案;-)(这个问题现在很老了......) (4认同)

Kra*_*mar 7

Django没有很好的解决方案从数据库中获取大型项目.

import gc
# Get the events in reverse order
eids = Event.objects.order_by("-id").values_list("id", flat=True)

for index, eid in enumerate(eids):
    event = Event.object.get(id=eid)
    # do necessary work with event
    if index % 100 == 0:
       gc.collect()
       print("completed 100 items")
Run Code Online (Sandbox Code Playgroud)

values_list可用于获取数据库中的所有ID,然后单独获取每个对象.有一段时间,大型对象将在内存中创建,并且不会被垃圾收集直到退出循环.上面的代码在每消耗100个项目后进行手动垃圾收集.

  • 但是,这将导致数据库中的命中次数与循环数相等,我深信。 (2认同)

lin*_*eak 5

因为那样,整个查询集的对象会立即全部加载到内存中。您需要将查询集分块为更小的可消化位。执行此操作的模式称为“喂汤”。这是一个简短的实现。

def spoonfeed(qs, func, chunk=1000, start=0):
    ''' Chunk up a large queryset and run func on each item.

    Works with automatic primary key fields.

    chunk -- how many objects to take on at once
    start -- PK to start from

    >>> spoonfeed(Spam.objects.all(), nom_nom)
    '''
    while start < qs.order_by('pk').last().pk:
        for o in qs.filter(pk__gt=start, pk__lte=start+chunk):
            yeild func(o)
        start += chunk
Run Code Online (Sandbox Code Playgroud)

要使用此功能,您需要编写一个对对象执行操作的函数:

def set_population_density(town):
    town.population_density = calculate_population_density(...)
    town.save()
Run Code Online (Sandbox Code Playgroud)

然后在您的查询集上运行该函数:

spoonfeed(Town.objects.all(), set_population_density)
Run Code Online (Sandbox Code Playgroud)

可以通过func在多个对象上并行执行的多处理来进一步改善这一点。