代码之家  ›  专栏  ›  技术社区  ›  davidchambers

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

  •  90
  • davidchambers  · 技术社区  · 14 年前

    所讨论的表包含大约一千万行。

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

    这导致内存使用量稳步增加到4gb左右,此时行将快速打印。第一行打印之前的长时间延迟让我吃惊,我希望它几乎可以立即打印出来。

    我也试过了 Event.objects.iterator() 也有同样的表现。

    我不明白Django在内存中加载了什么,也不明白它为什么要这样做。我希望Django在数据库级别迭代结果,这意味着结果将以大致恒定的速率打印(而不是在长时间等待之后立即打印)。

    我误解了什么?

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

    9 回复  |  直到 14 年前
        1
  •  105
  •   eternicode    14 年前

    内特C很接近,但不是很接近。

    the docs :

    可以通过以下方式计算QuerySet:

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

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

    因此,当您第一次进入该循环并获得queryset的迭代形式时,将同时检索您的1000万行。您所经历的等待是Django加载数据库行并为每个行创建对象,然后返回一些您可以实际迭代的内容。然后你就把所有的事情都记在记忆里了,结果就出来了。

    从我对文件的阅读来看, iterator() 只不过是绕过QuerySet的内部缓存机制。我认为一件一件地做一件事是有意义的,但反过来,这将需要你的数据库上的一千万个人点击。也许不是那么令人满意。

    高效地迭代大型数据集是我们还没有完全解决的问题,但是有一些片段可能对您的目的很有用:

        2
  •  38
  •   Jason Morrison mpaf    10 年前

    可能不是最快或最有效的,但作为现成的解决方案,为什么不使用这里记录的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
  •  26
  •   Luke Moore    9 年前

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

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

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

    迭代器()方法计算queryset,然后直接读取结果,而不在queryset级别进行缓存。这种方法在遍历大量只需访问一次的对象时,可以获得更好的性能和显著的内存减少。请注意,缓存仍在数据库级别进行。

    使用iterator()可以减少内存使用,但仍然比我预期的要高。使用mpaf建议的paginator方法占用的内存要少得多,但是对于我的测试用例来说要慢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
  •  8
  •   nate c    14 年前

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

    在对queryset进行求值之前,实际上不会发生数据库活动。

    所以当 print event 运行查询激发(这是根据您的命令执行的完整表扫描)并加载结果。你要求所有的东西,如果没有得到所有的东西就没有办法得到第一个。

    但如果你这样做:

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

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

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

        5
  •  7
  •   Frank Heikens    14 年前

    对于大量的记录 database cursor 表现更好。在Django中确实需要原始SQL,Django游标不同于SQL游标。

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

        6
  •  7
  •   Kracekumar    11 年前

    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,然后分别获取每个对象。在一段时间内,大对象将被创建在内存中,不会被垃圾收集TIL退出循环。上面的代码在每100个项目被消耗后进行手动垃圾收集。

        7
  •  5
  •   jackotonye Dmit3Y    7 年前

    因为这样一来,整个queryset的对象会同时加载到内存中。你需要把你的查询集分成可消化的小块。这样做的模式称为勺送。下面是一个简短的实现。

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

    然后在queryset上运行该函数:

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

    这可以通过执行多处理进一步改进 func 在多个平行对象上。

        8
  •  3
  •   danius    9 年前

    这里有一个包括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
  •  0
  •   Tho    7 年前

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