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

时间:2010-11-19 04:49:41

标签: sql django postgresql django-orm

有问题的表包含大约一千万行。

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

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

我也试过Event.objects.iterator(),行为方式相同。

我不明白Django加载到内存中的原因或为什么会这样做。我期望Django在数据库级别迭代结果,这意味着结果将以大致恒定的速率打印(而不是在漫长的等待之后立即打印)。

我误解了什么?

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

9 个答案:

答案 0 :(得分:94)

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

来自the docs

  

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

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

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

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

从我对文档的阅读中,iterator()只会绕过QuerySet的内部缓存机制。我认为它可能是一个一个接一个的事情,但相反,你的数据库需要1000万次点击。也许不是那么可取。

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

答案 1 :(得分:34)

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

答案 2 :(得分:21)

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

答案 3 :(得分:7)

这是来自文档:     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添加偏移和限制。

答案 4 :(得分:6)

对于大量记录,database cursor表现更好。你需要在Django中使用原始SQL,Django-cursor与SQL cursur不同。

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

答案 5 :(得分:6)

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个项目后进行手动垃圾收集。

答案 6 :(得分:4)

因为这样整个查询集的对象一次性加载到内存中。您需要将查询集分成较小的可消化位。这样做的模式称为勺子喂食。这是一个简短的实现。

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

答案 7 :(得分:2)

这里包含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

答案 8 :(得分: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. Retrieving million of rows from MySQL
  2. How does MySQL result set streaming perform vs fetching the whole JDBC ResultSet at once