Подтвердить что ты не робот

Ограничение использования памяти в * Large * Django QuerySet

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

for model_instance in SomeModel.objects.all():
    do_something(model_instance)

(Обратите внимание, что на самом деле это фильтр() не все(), но, тем не менее, я все еще вхожу в выбор очень большого набора объектов.)

Проблема, с которой я сталкиваюсь, заключается в том, что после запуска некоторое время поток был убит моим хостинг-провайдером, потому что я использую слишком много памяти. Я предполагаю, что все это использование памяти происходит, потому что хотя объект QuerySet, возвращенный моим запросом, изначально имеет очень малый объем памяти, он заканчивается, когда объект QuerySet кэширует каждый model_instance, когда я повторяю их.

Мой вопрос: "Каков наилучший способ перебора почти каждой SomeModel в моей базе данных в эффективном режиме памяти?" или, возможно, мой вопрос: "Как я могу" исключить "экземпляры модели из набора запросов django?"

EDIT: Я фактически использую результаты запроса для создания серии новых объектов. Таким образом, я вообще не обновляю объекты с запросом.

4b9b3361

Ответ 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/

Он выполняет итерацию над запросом, уступая строкам меньших "кусков" исходного набора запросов. В результате получается значительно меньше памяти, позволяя вам настраиваться на скорость. Я использую его в одном из моих проектов.