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


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

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

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

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

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

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

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

9 81

9 ответов:

Нейт С был близок, но не совсем.

С документы:

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

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

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

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

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

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

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

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

поведение Django по умолчанию заключается в том, чтобы кэшировать весь результат запроса при его оценке. Вы можете использовать метод итератора QuerySet, чтобы избежать этого кэширования:

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

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

метод iterator () вычисляет набор запросов, а затем считывает результаты непосредственно без кэширования на уровне набора запросов. Этот метод приводит к в более лучшем представлении и значительно уменьшение объема памяти при итерации по большому количеству объектов, доступ к которым требуется только один раз. Обратите внимание, что кэширование по-прежнему выполняется на уровне базы данных.

использование 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

это из документов: 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 внутри.

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

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

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

потому что таким образом объекты для всего queryset загружаются в память все сразу. Вам нужно, чтобы кусок свой объект QuerySet на мелкие удобоваримые кусочки. Шаблон для этого называется spoonfeeding. Вот краткая реализация.

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 на нескольких объектах параллельно.

вот решение, включая лен и посчитайте:

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

Я обычно использую raw MySQL raw query вместо 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 выполняет vs извлечение всего набора результатов JDBC сразу