Почему итерация через большой набор запросов Django потребляет огромное количество памяти?
таблица, о которой идет речь, содержит примерно десять миллионов строк.
for event in Event.objects.all():
print event
это приводит к постоянному увеличению использования памяти до 4 ГБ или около того, после чего строки быстро печатаются. Длительная задержка перед печатью первого ряда удивила меня – я ожидал, что он будет напечатан почти мгновенно.
Я тоже пробовал Event.objects.iterator()
который вел себя так же.
Я не понимаю, что Django загружает в память или почему он это делает. Я ожидал, что Django повторится результаты на уровне базы данных, что означает, что результаты будут печататься примерно с постоянной скоростью (а не сразу после длительного ожидания).
что я неправильно понял?
(Я не знаю, актуально ли это, но я использую PostgreSQL.)
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: