为什么要遍历大型Django QuerySet消耗大量内存?


110

该表包含大约一千万行。

for event in Event.objects.all():
    print event

这会导致内存使用量稳定增加到4 GB左右,这时行将快速打印。第一行打印之前漫长的延迟让我感到惊讶–我希望它几乎可以立即打印。

我也尝试过Event.objects.iterator()以相同的方式表现。

我不明白Django正在将什么加载到内存中或为什么这样做。我希望Django在数据库级别遍历结果,这意味着结果将以大致恒定的速率打印(而不是经过漫长的等待一次全部打印)。

我误会了什么?

(我不知道它是否相关,但是我正在使用PostgreSQL。)


6
在较小的机器上,这甚至可能导致django外壳或服务器立即被“杀死”
Stefano

Answers:


112

内特C距离很近,但不太远。

文档

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

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

    for e in Entry.objects.all():
        print e.headline

因此,当您第一次进入该循环并获得查询集的迭代形式时,一次就可以检索一千万行。您遇到的等待是Django加载数据库行并为每个数据库行创建对象,然后返回实际上可以迭代的内容。然后,您将所有内容存储在内存中,结果溢出了。

根据我对文档的阅读, iterator()仅能绕过QuerySet的内部缓存机制。我认为这样做一件一件一件的事情可能是有意义的,但是相反地,这需要在数据库上进行一千万次单独点击。也许不是所有想要的。

我们仍然没有完全正确地遍历大型数据集,但是有一些摘要可能对您有用:


1
感谢您的出色回答,@ eternicode。最后,我们使用原始SQL进行所需的数据库级迭代。
davidchambers,

2
@eternicode不错的答案,就是这个问题。从那以后,Django中是否有任何相关的更新?
Zólyomi伊什特万

2
自Django 1.11起的文档说iterator()确实使用服务器端游标。
杰夫·约翰逊

42

可能不是更快或更高效,但是作为现成的解决方案,为什么不使用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

3
自发布以来,现在可能会进行小幅改进。 Paginator现在具有page_range避免样板的属性。如果要寻找最小的内存开销,则可以使用object_list.iterator()它将不会填充queryset缓存prefetch_related_objects然后需要预取
Ken Colton

28

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

for event in Event.objects.all().iterator():
    print event

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

8

这来自docs:http : //docs.djangoproject.com/en/dev/ref/models/querysets/

除非您执行某些操作来评估查询集,否则实际上不会发生数据库活动。

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

但是,如果您执行以下操作:

Event.objects.all()[300:900]

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

然后它将在内部向sql添加偏移量和限制。


7

对于大量记录,数据库游标性能甚至更好。在Django中您确实需要原始SQL,Django光标与SQL cursur有所不同。

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


4
弗兰克(Frank),这绝对是一个好点,但是很高兴看到一些代码详细信息来寻求解决方案;-)(这个问题现在已经很老了...)
Stefano

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")

values_list可用于获取数据库中的所有ID,然后分别获取每个对象。一段时间后,将在内存中创建大对象,并且不会存在垃圾,直到退出循环为止。上面的代码每使用完第100个项目,就会进行手动垃圾回收。


StreamingHttpResponse可以解决吗?stackoverflow.com/questions/15359768/...
ratata

2
但是,这将导致数据库中的命中次数与循环数相等,我深信。
raratiru '16

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

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

def set_population_density(town):
    town.population_density = calculate_population_density(...)
    town.save()

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

spoonfeed(Town.objects.all(), set_population_density)

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


1
看起来这将被内置到1.12中,迭代(chunk_size = 1000)
凯文·帕克

3

这是一个包括len和count的解决方案:

class GeneratorWithLen(object):
    """
    Generator that includes len and count for given queryset
    """
    def __init__(self, generator, length):
        self.generator = generator
        self.length = length

    def __len__(self):
        return self.length

    def __iter__(self):
        return self.generator

    def __getitem__(self, item):
        return self.generator.__getitem__(item)

    def next(self):
        return next(self.generator)

    def count(self):
        return self.__len__()

def batch(queryset, batch_size=1024):
    """
    returns a generator that does not cache results on the QuerySet
    Aimed to use with expected HUGE/ENORMOUS data sets, no caching, no memory used more than batch_size

    :param batch_size: Size for the maximum chunk of data in memory
    :return: generator
    """
    total = queryset.count()

    def batch_qs(_qs, _batch_size=batch_size):
        """
        Returns a (start, end, total, queryset) tuple for each batch in the given
        queryset.
        """
        for start in range(0, total, _batch_size):
            end = min(start + _batch_size, total)
            yield (start, end, total, _qs[start:end])

    def generate_items():
        queryset.order_by()  # Clearing... ordering by id if PK autoincremental
        for start, end, total, qs in batch_qs(queryset):
            for item in qs:
                yield item

    return GeneratorWithLen(generate_items(), total)

用法:

events = batch(Event.objects.all())
len(events) == events.count()
for event in events:
    # Do something with the Event

0

对于这种任务,我通常使用原始MySQL原始查询而不是Django ORM。

MySQL支持流模式,因此我们可以安全快速地遍历所有记录,而不会发生内存不足错误。

import MySQLdb
db_config = {}  # config your db here
connection = MySQLdb.connect(
        host=db_config['HOST'], user=db_config['USER'],
        port=int(db_config['PORT']), passwd=db_config['PASSWORD'], db=db_config['NAME'])
cursor = MySQLdb.cursors.SSCursor(connection)  # SSCursor for streaming mode
cursor.execute("SELECT * FROM event")
while True:
    record = cursor.fetchone()
    if record is None:
        break
    # Do something with record here

cursor.close()
connection.close()

参考:

  1. 从MySQL检索百万行
  2. MySQL结果集流如何执行与一次获取整个JDBC ResultSet的关系

您仍然可以使用Django ORM生成查询。只需queryset.query在您的执行中使用result即可。
Pol的
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.