Почему выполняется итерация через большой Django QuerySet, потребляющий огромное количество памяти?

В рассматриваемой таблице содержится примерно десять миллионов строк.

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

Это приводит к постоянному увеличению использования памяти до 4 ГБ или около того, после чего строки быстро печатаются. Длительная задержка перед печатью первой строки удивила меня - я ожидал, что она будет печататься почти мгновенно.

Я также пробовал Event.objects.iterator(), который вел себя одинаково.

Я не понимаю, что Django загружает в память или почему это делает. Я ожидал, что Django будет перебирать результаты на уровне базы данных, что означает, что результаты будут напечатаны примерно с постоянной скоростью (а не сразу после длительного ожидания).

Что я неправильно понял?

(я не знаю, имеет ли значение это значение, но я использую PostgreSQL.)

Ответ 1

Nate C был близок, но не совсем.

От документы:

Вы можете оценить QuerySet следующими способами:

  • Итерация

    . QuerySet является итерируемым, и он выполняет свой запрос к базе данных при первом повторении. Например, это будет печатать заголовок всех записей в базе данных:

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

Итак, ваши десять миллионов строк извлекаются сразу, когда вы впервые вводите этот цикл и получаете итерирующую форму набора запросов. Ожидание, которое вы испытываете, - это Django, загружающее строки базы данных и создающие объекты для каждого из них, прежде чем возвращать то, что вы действительно можете перебрать. Тогда у вас есть все в памяти, и результаты приходят.

Из моего чтения документов iterator() не более чем обходит внутренние механизмы кэширования QuerySet. Я думаю, что это может иметь смысл для этого сделать одно за другим, но это, наоборот, потребует десяти миллионов индивидуальных хитов в вашей базе данных. Возможно, не все это желательно.

Итерация над большими наборами данных эффективно - это то, что мы до сих пор не получили совершенно прав, но есть некоторые фрагменты, которые могут оказаться полезными для ваших целей:

Ответ 2

Не может быть быстрее или эффективнее, но в качестве готового решения, почему бы не использовать объекты paginator и страницы django, описанные здесь:

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() оценивает набор запросов, а затем непосредственно считывает результаты без кэширования на уровне QuerySet. Этот метод приводит к повышению производительности и значительному сокращению объема памяти при повторении большого количества объектов, которые вам нужно получить только один раз. Обратите внимание, что кэширование все еще выполняется на уровне базы данных.

Использование iterator() уменьшает использование памяти для меня, но оно все же выше, чем я ожидал. Использование подхода paginator, предложенного 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

Для больших объемов записей, курсор базы данных работает еще лучше. Вам нужен сырой SQL в Django, курсор Django - это нечто иное, чем SQL-курсор.

Метод LIMIT - OFFSET, предложенный Nate C, может быть достаточно хорош для вашей ситуации. Для больших объемов данных он медленнее, чем курсор, потому что он должен снова и снова запускать один и тот же запрос и должен перескакивать все больше и больше результатов.

Ответ 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 можно использовать для извлечения всех идентификаторов в базах данных, а затем выборки каждого объекта отдельно. За какое-то время в памяти будут созданы большие объекты и не будет собран мусор, пока цикл цикла не будет завершен. Над кодом выполняется сбор мусора вручную после каждого 100-го предмета.

Ответ 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):
            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 для нескольких объектов.

Ответ 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 raw запрос вместо 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()

Ref:

  1. Извлечение миллиона строк из MySQL
  2. Каким образом потоковая передача набора результатов MySQL выполняется по сравнению с извлечением всего JDBC ResultSet одновременно