Ограничение использования памяти в * Large * Django QuerySet - PullRequest
22 голосов
/ 01 февраля 2011

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

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

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

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

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

РЕДАКТИРОВАТЬ: я на самом деле использую результаты набора запросов для создания серии новых объектов,Поэтому я не обновляю запрашиваемые объекты вообще.

Ответы [ 6 ]

17 голосов
/ 04 марта 2011

Так что я фактически закончил делать то, что вы можете «обернуть» в QuerySet. Он работает, делая глубокую копию QuerySet, используя синтаксис среза - например, some_queryset[15:45] - но затем он делаетеще одна глубокая копия исходного QuerySet, когда срез был полностью повторен.Это означает, что в памяти хранится только набор Объектов, возвращенных в данном срезе.

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

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

Как насчет использования описанных здесь объектов Paginator и Page Page ядра django:

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
11 голосов
/ 17 февраля 2012

Вы не можете просто использовать 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

Измените размер страницы в соответствии с вашими требованиями и, при необходимости, измените [смещение: смещение + размер страницы] в идиому [смещение * размер страницы: (смещение + 1) * размер страницы], если она вам больше подходит.Также, конечно, замените Model на ваше реальное название модели.

8 голосов
/ 01 июня 2015

Многие решения реализуют sql OFFSET и LIMIT путем нарезки набора запросов. Как отмечает Стефано, с большими наборами данных это становится очень неэффективным. Правильный способ справиться с этим - использовать серверные курсоры для отслеживания смещения.

Собственная поддержка серверного курсора в работе для 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.

3 голосов
/ 18 апреля 2013

Для этого есть фрагмент django:

http://djangosnippets.org/snippets/1949/

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

3 голосов
/ 01 февраля 2011

Я продолжаю исследования, и похоже, что я хочу сделать эквивалент SQL OFFSET и LIMIT, что в соответствии с Django Doc's по Limiting 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никогда не станет слишком большим.

...