Почему итерация по большому Django QuerySet потребляет огромные объемы памяти? - PullRequest
93 голосов
/ 19 ноября 2010

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

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

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

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

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

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

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

Ответы [ 9 ]

94 голосов
/ 19 ноября 2010

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

С Документы :

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

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

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

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

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

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

36 голосов
/ 29 ноября 2013

Может быть не самым быстрым или эффективным, но как готовое решение, почему бы не использовать описанные здесь объекты Paginator и Page ядра ядра 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
22 голосов
/ 20 июля 2015

Поведение 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
7 голосов
/ 19 ноября 2010

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

6 голосов
/ 30 июня 2014

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

6 голосов
/ 19 ноября 2010

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

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

4 голосов
/ 04 апреля 2015

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

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

2 голосов
/ 29 октября 2015

Здесь решение, включающее 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
0 голосов
/ 30 июня 2017

Я обычно использую сырой 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 одновременно
...