【问题标题】:Why is iterating through a large Django QuerySet consuming massive amounts of memory?为什么遍历大型 Django QuerySet 会消耗大量内存?
【发布时间】:2011-05-12 10:51:45
【问题描述】:

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

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

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

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

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

我误解了什么?

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

【问题讨论】:

  • 在较小的机器上,这甚至会直接导致 django shell 或服务器被“杀死”

标签: sql django postgresql django-orm


【解决方案1】:

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

来自the docs

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

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

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

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

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

有效地迭代大型数据集是我们还没有完全正确的事情,但是那里有一些你可能会发现对你的目的有用的 sn-ps:

【讨论】:

  • 感谢@eternicode 的精彩回答。最后,我们使用原始 SQL 来进行所需的数据库级迭代。
  • @eternicode 不错的答案,请点击这个问题。从那以后,Django 中是否有任何相关更新?
  • 自 Django 1.11 以来的文档说 iterator() 确实使用服务器端游标。
【解决方案2】:

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

【讨论】:

【解决方案3】:

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

【讨论】:

    【解决方案4】:

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

    【讨论】:

      【解决方案5】:

      对于大量记录,database cursor 的性能更好。你确实需要 Django 中的原始 SQL,Django-cursor 与 SQL cursur 不同。

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

      【讨论】:

      • 弗兰克,这绝对是一个好点,但很高兴看到一些代码细节来推动解决方案;-)(这个问题现在已经很老了......)
      【解决方案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,然后分别获取每个对象。随着时间的推移,大型对象将在内存中创建,并且在退出 for 循环之前不会被垃圾收集。上面的代码在每消费 1​​00 个项目后进行手动垃圾收集。

      【讨论】:

      【解决方案7】:

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

      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

      【讨论】:

      • 看起来这将通过 iterate(chunk_size=1000) 内置到 1.12 中
      【解决方案8】:

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

      【讨论】:

        【解决方案9】:

        对于这类任务,我通常使用原始 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

        【讨论】:

        • 您仍然可以使用 Django ORM 生成查询。只需在您的执行中使用生成的queryset.query
        猜你喜欢
        • 2011-05-10
        • 1970-01-01
        • 2011-08-29
        • 1970-01-01
        • 1970-01-01
        • 2019-08-05
        • 2017-06-17
        • 2022-01-16
        • 2014-01-04
        相关资源
        最近更新 更多