126

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

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

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

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

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

我误解了什么?

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

4

9 回答 9

123

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

文档

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

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

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

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

根据我对文档的阅读,iterator()无非是绕过 QuerySet 的内部缓存机制。我认为做一件一件的事情可能是有意义的,但这反过来需要你的数据库上的一千万次点击。也许并不是那么理想。

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

于 2010-11-19T05:44:27.873 回答
53

可能不是更快或最有效的,但作为现成的解决方案,为什么不使用此处记录的 django 核心的 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
于 2013-11-29T12:19:53.237 回答
32

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

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

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

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

使用 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
于 2015-07-20T20:18:56.157 回答
8

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

于 2010-11-19T05:17:49.017 回答
8

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

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

于 2010-11-19T07:57:14.797 回答
8

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,然后分别获取每个对象。随着时间的推移,大对象将在内存中创建,并且在退出 for 循环之前不会被垃圾收集。上面的代码在每消费 1​​00 个项目后进行手动垃圾收集。

于 2014-06-30T14:10:48.860 回答
6

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

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):
            yield 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在多个对象上并行执行。

于 2015-04-04T20:22:03.643 回答
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
于 2015-10-29T08:07:28.477 回答
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
于 2017-06-30T04:32:44.520 回答