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

Эффективная (постоянная) и оптимизированная по скорости оптимизация итерации по большой таблице в Django

У меня очень большая таблица. Он в настоящее время находится в базе данных MySQL. Я использую django.

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

Я хотел бы сохранить итерацию как можно быстрее при постоянном использовании памяти.

Как уже ясно в Ограничение использования памяти в * Большом * Django QuerySet и Почему выполняется итерация через большой Django QuerySet, потребляющий огромное количество памяти?, простая итерация по всем объектам в django будет убивать машину, поскольку она будет извлекать ВСЕ объекты из базы данных.

На пути к решению

Прежде всего, чтобы уменьшить потребление памяти, вы должны быть уверены, что DEBUG является False (или обезьяна паттирует курсор: отключает ведение журнала SQL при сохранении настроек .DEBUG?), чтобы убедиться, что django не хранит файлы в connections для отладки.

Но даже при этом

for model in Model.objects.all()

- это не гонка.

Даже при немного улучшенной форме:

for model in Model.objects.all().iterator()

Используя iterator(), вы сохраните некоторую память, не сохраняя результат кэша внутренне (хотя и не обязательно на PostgreSQL!); но по-прежнему будут извлекать все объекты из базы данных.

Наивное решение

Решение в первом вопросе состоит в том, чтобы обрезать результаты на основе счетчика с помощью chunk_size. Существует несколько способов написать его, но в основном все они сводятся к запросу OFFSET + LIMIT в SQL.

что-то вроде:

qs = Model.objects.all()
counter = 0
count = qs.count()
while counter < count:     
    for model in qs[counter:counter+count].iterator()
        yield model
    counter += chunk_size

Хотя это эффективная память (постоянное использование памяти пропорционально chunk_size), она очень плохая с точки зрения скорости: по мере того, как OFFSET растет, как MySQL, так и PostgreSQL (и, вероятно, большинство БД) начнут задыхаться и замедляться.

Лучшее решение

Лучшее решение доступно в этот пост от Thierry Schellenbach. Он фильтрует на ПК, который быстрее, чем компенсирует (насколько быстро, возможно, зависит от БД)

pk = 0
last_pk = qs.order_by('-pk')[0].pk
queryset = qs.order_by('pk')
while pk < last_pk:
    for row in qs.filter(pk__gt=pk)[:chunksize]:
        pk = row.pk
        yield row
    gc.collect()

Это начинает становиться удовлетворительным. Теперь Memory = O (C) и Speed ​​~ = O (N)

Проблемы с "лучшим" решением

Лучшее решение работает только тогда, когда PK доступен в QuerySet. К несчастью, это не всегда так, в частности, когда QuerySet содержит комбинации разных (group_by) и/или значений (ValueQuerySet).

В этой ситуации "лучшее решение" не может быть использовано.

Можем ли мы лучше?

Теперь мне интересно, можем ли мы пойти быстрее и избежать проблемы с QuerySets без ПК. Возможно, используя что-то, что я нашел в других ответах, но только в чистом SQL: используя курсоры.

Так как я плохо разбираюсь в необработанном SQL, в частности в Django, возникает реальный вопрос:

как мы можем построить лучший итератор Django QuerySet для больших таблиц

Мое взятие из того, что я прочитал, заключается в том, что мы должны использовать серверные курсоры (видимо (см. ссылки), используя стандартный Django Cursor, не достигли бы такого же результата, потому что по умолчанию оба соединения python-MySQL и psycopg кэшируют результаты).

Будет ли это действительно более быстрое (и/или более эффективное) решение?

Можно ли это сделать, используя raw SQL в django? Или мы должны писать конкретный код python в зависимости от соединителя базы данных?

Курсоры на стороне сервера в PostgreSQL и в MySQL

Что, насколько я мог получить на данный момент...

a Django chunked_iterator()

Теперь, лучше всего, этот метод будет работать как queryset.iterator(), а не iterate(queryset), и будет частью ядра django или, по крайней мере, подключаемого приложения.

Обновление. Благодаря "Т" в комментариях для поиска django ticket, которые содержат некоторую дополнительную информацию. Различия в поведении коннектора делают его таким, чтобы, вероятно, лучшим решением было бы создать конкретный метод chunked, а не прозрачно расширяться iterator (звучит как хороший подход ко мне). Реализация stub существует, но не было никакой работы в год, и не похоже, что автор готов перейти на что еще.

Дополнительные ссылки:

редактирует:

Django 1.6 добавляет постоянные соединения с базой данных

Постоянные соединения базы данных Django

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

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

4b9b3361

Ответ 1

Существенный ответ: использовать исходный SQL-код с серверными курсорами.

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

Для Django 1.5.2 и MySQLdb 1.2.4 будет работать следующий код. Кроме того, он хорошо прокомментировал.

Предостережение:. Это не основано на общедоступных API-интерфейсах, поэтому в будущих версиях Django он может быть поврежден.

# This script should be tested under a Django shell, e.g., ./manage.py shell

from types import MethodType

import MySQLdb.cursors
import MySQLdb.connections
from django.db import connection
from django.db.backends.util import CursorDebugWrapper


def close_sscursor(self):
    """An instance method which replace close() method of the old cursor.

    Closing the server-side cursor with the original close() method will be
    quite slow and memory-intensive if the large result set was not exhausted,
    because fetchall() will be called internally to get the remaining records.
    Notice that the close() method is also called when the cursor is garbage 
    collected.

    This method is more efficient on closing the cursor, but if the result set
    is not fully iterated, the next cursor created from the same connection
    won't work properly. You can avoid this by either (1) close the connection 
    before creating a new cursor, (2) iterate the result set before closing 
    the server-side cursor.
    """
    if isinstance(self, CursorDebugWrapper):
        self.cursor.cursor.connection = None
    else:
        # This is for CursorWrapper object
        self.cursor.connection = None


def get_sscursor(connection, cursorclass=MySQLdb.cursors.SSCursor):
    """Get a server-side MySQL cursor."""
    if connection.settings_dict['ENGINE'] != 'django.db.backends.mysql':
        raise NotImplementedError('Only MySQL engine is supported')
    cursor = connection.cursor()
    if isinstance(cursor, CursorDebugWrapper):
        # Get the real MySQLdb.connections.Connection object
        conn = cursor.cursor.cursor.connection
        # Replace the internal client-side cursor with a sever-side cursor
        cursor.cursor.cursor = conn.cursor(cursorclass=cursorclass)
    else:
        # This is for CursorWrapper object
        conn = cursor.cursor.connection
        cursor.cursor = conn.cursor(cursorclass=cursorclass)
    # Replace the old close() method
    cursor.close = MethodType(close_sscursor, cursor)
    return cursor


# Get the server-side cursor
cursor = get_sscursor(connection)

# Run a query with a large result set. Notice that the memory consumption is low.
cursor.execute('SELECT * FROM million_record_table')

# Fetch a single row, fetchmany() rows or iterate it via "for row in cursor:"
cursor.fetchone()

# You can interrupt the iteration at any time. This calls the new close() method,
# so no warning is shown.
cursor.close()

# Connection must be close to let new cursors work properly. see comments of
# close_sscursor().
connection.close()

Ответ 2

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

def table_iterator(model, page_size=10000):
    try: max = model.objects.all().order_by("-pk")[0].pk
    except IndexError: return 
    pages = int(max / page_size) + 1
    for page_num in range(pages):
        lower = page_num * page_size
        page = model.objects.filter(pk__gte=lower, pk__lt=lower+page_size)
        for obj in page:
            yield obj

Использование выглядит так:

for obj in table_iterator(Model):
    # do stuff

Ответ 3

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

large_qs = MyModel.objects.all().values_list("id", flat=True)
for model_id in large_qs:
    model_object = MyModel.objects.get(id=model_id)
    # do whatever you need to do with the model here

В память загружаются только идентификаторы, а объекты извлекаются и отбрасываются по мере необходимости. Обратите внимание на увеличенную загрузку базы данных и более медленное время выполнения, как компромиссы для сокращения использования памяти.

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