Ограничение использования памяти в * Large * Django QuerySet
У меня есть задача, которую нужно запускать на "большинстве" объектов в моей базе данных один раз в каждый промежуток времени (один раз в день, раз в неделю, что угодно). В основном это означает, что у меня есть запрос, который выглядит так, как это работает в нем собственный поток.
for model_instance in SomeModel.objects.all():
do_something(model_instance)
(Обратите внимание, что на самом деле это фильтр() не все(), но, тем не менее, я все еще вхожу в выбор очень большого набора объектов.)
Проблема, с которой я сталкиваюсь, заключается в том, что после запуска некоторое время поток был убит моим хостинг-провайдером, потому что я использую слишком много памяти. Я предполагаю, что все это использование памяти происходит, потому что хотя объект QuerySet
, возвращенный моим запросом, изначально имеет очень малый объем памяти, он заканчивается, когда объект QuerySet
кэширует каждый model_instance
, когда я повторяю их.
Мой вопрос: "Каков наилучший способ перебора почти каждой SomeModel
в моей базе данных в эффективном режиме памяти?" или, возможно, мой вопрос: "Как я могу" исключить "экземпляры модели из набора запросов django?"
EDIT: Я фактически использую результаты запроса для создания серии новых объектов. Таким образом, я вообще не обновляю объекты с запросом.
Ответы
Ответ 1
Итак, что я на самом деле закончил, это создание чего-то, что вы можете "обернуть" QuerySet. Он работает, делая глубокую копию QuerySet, используя синтаксис среза - например, some_queryset[15:45]
-, но затем он делает еще одна глубокая копия исходного QuerySet, когда срез был полностью повторен. Это означает, что в памяти сохраняется только набор объектов, возвращаемых в 'this' конкретном срезе.
class MemorySavingQuerysetIterator(object):
def __init__(self,queryset,max_obj_num=1000):
self._base_queryset = queryset
self._generator = self._setup()
self.max_obj_num = max_obj_num
def _setup(self):
for i in xrange(0,self._base_queryset.count(),self.max_obj_num):
# By making a copy of of the queryset and using that to actually access
# the objects we ensure that there are only `max_obj_num` objects in
# memory at any given time
smaller_queryset = copy.deepcopy(self._base_queryset)[i:i+self.max_obj_num]
logger.debug('Grabbing next %s objects from DB' % self.max_obj_num)
for obj in smaller_queryset.iterator():
yield obj
def __iter__(self):
return self
def next(self):
return self._generator.next()
Итак, вместо...
for obj in SomeObject.objects.filter(foo='bar'): <-- Something that returns *a lot* of Objects
do_something(obj);
Вы бы сделали...
for obj in MemorySavingQuerysetIterator(in SomeObject.objects.filter(foo='bar')):
do_something(obj);
Обратите внимание, что целью этого является сохранение памяти в интерпретаторе Python. По сути, это делает запрос больше. Обычно люди пытаются сделать совершенно противоположное этому - т.е. Максимально минимизировать запросы к базе данных без учета использования памяти. Надеюсь, кто-то найдет это полезным, хотя.
Ответ 2
Вы не можете просто использовать Model.objects.all(). iterator(), потому что он будет извлекать сразу все элементы таблицы. Вы также не можете просто пойти с методом Model.objects.all() [offset: offset + pagesize], потому что он поймает ваши результаты. Любой из них превысит ваш предел памяти.
Я попытался объединить оба решения, и это сработало:
offset = 0
pagesize = 1000
count = Model.objects.all().count()
while offset < count:
for m in Model.objects.all()[offset : offset + pagesize].iterator:
do_something with m
offset += pagesize
Измените размер страницы в соответствии с вашими требованиями и, возможно, измените значение [offset: offset + pagesize] на [offset * pagesize: (offset + 1) * pageize] idiom, если он вам подходит. Кроме того, конечно, замените модель на свое фактическое название модели.
Ответ 3
Многие решения реализуют sql OFFSET
и LIMIT
путем нарезки набора запросов. Как отмечает Стефано, с большими наборами данных это становится очень неэффективным. Правильный способ обращения с этим - использовать серверные курсоры для отслеживания OFFSET.
Встроенная поддержка курсора на стороне сервера в работе для django. Пока это не будет готово, вот простая реализация, если вы используете postgres с бэкэнд psycopg2:
def server_cursor_query(Table):
table_name = Table._meta.db_table
# There must be an existing connection before creating a server-side cursor
if connection.connection is None:
dummy_cursor = connection.cursor() # not a server-side cursor
# Optionally keep track of the columns so that we can return a QuerySet. However,
# if your table has foreign keys, you may need to rename them appropriately
columns = [x.name for x in Table._meta.local_fields]
cursor = connection.connection.cursor(name='gigantic_cursor')) # a server-side
# cursor
with transaction.atomic():
cursor.execute('SELECT {} FROM {} WHERE id={}'.format(
', '.join(columns), table_name, id))
while True:
rows = cursor.fetchmany(1000)
if not rows:
break
for row in rows:
fields = dict(zip(columns, row))
yield Table(**fields)
См. это сообщение в блоге для отличного объяснения проблем с памятью из больших запросов в django.
Ответ 4
Как насчет использования объектов Dagango Paginator и страниц, описанных здесь:
https://docs.djangoproject.com/en/dev/topics/pagination/
Что-то вроде этого:
from django.core.paginator import Paginator
from djangoapp.models import SomeModel
paginator = Paginator(SomeModel.objects.all(), 1000) # chunks of 1000
for page_idx in range(1, paginator.num_pages):
for row in paginator.page(page_idx).object_list:
# here you can do what you want with the row
print "done processing page %s" % page_idx
Ответ 5
Я продолжаю исследования, и это похоже на то, что я хочу сделать эквивалент SQL OFFSET и LIMIT, который согласно Django Doc для ограничения запросов Querysets означает, что я хочу использовать синтаксис среза, например SomeModel.objects.all()[15:25]
Итак, теперь я думаю, что может быть что-то вроде этого, что я ищу:
# Figure out the number of objects I can safely hold in memory
# I'll just say 100 for right now
number_of_objects = 100
count = SomeModel.objects.all().count():
for i in xrange(0,count,number_of_objects):
smaller_queryset = SomeModel.objects.all()[i:i+number_of_objects]
for model_instance in smaller_queryset:
do_something(model_instance)
По моим расчетам это сделало бы так, что smaller_queryset
никогда не станет слишком большим.
Ответ 6
Для этого есть фрагмент django:
http://djangosnippets.org/snippets/1949/
Он выполняет итерацию над запросом, уступая строкам меньших "кусков" исходного набора запросов. В результате получается значительно меньше памяти, позволяя вам настраиваться на скорость. Я использую его в одном из моих проектов.