Итерация над большими наборами запросов в Django

Как эффективно итерировать большой набор запросов (записи исчисляются миллионами) с помощью Django?

Я пытаюсь удалить несколько миллионов записей, которые я не могу сделать с помощью простого массового SQL-запроса DELETE, потому что транзакция будет потреблять слишком много памяти сервера. Поэтому я пытаюсь написать сценарий на Python для группировки ~10000 отдельных операторов DELETE в транзакции.

Мой сценарий выглядит следующим образом:

from django.db import transaction
from django.conf import settings

settings.DEBUG = False

qs = MyModel.objects.filter(some_criteria=123)
total = qs.count()
i = 0
transaction.set_autocommit(False)
for record in qs.iterator():
    i += 1
    if i == 1 or not i % 100 or i == total:
        print('%i of %i %.02f%%: %s' % (i + 1, total, (i + 1) / float(total) * 100, record))
    record.delete()
    if not i % 1000:
        transaction.commit()
transaction.commit()

Это работает нормально для первых 2000 записей, но затем выдает ошибку:

Traceback (most recent call last):
  File "/project/.env/lib/python3.7/site-packages/django/db/models/sql/compiler.py", line 1512, in cursor_iter
    for rows in iter((lambda: cursor.fetchmany(itersize)), sentinel):
  File "/project/.env/lib/python3.7/site-packages/django/db/models/sql/compiler.py", line 1512, in <lambda>
    for rows in iter((lambda: cursor.fetchmany(itersize)), sentinel):
  File "/project/.env/lib/python3.7/site-packages/django/db/utils.py", line 96, in inner
    return func(*args, **kwargs)
  File "/project/.env/lib/python3.7/site-packages/django/db/utils.py", line 89, in __exit__
    raise dj_exc_value.with_traceback(traceback) from exc_value
  File "/project/.env/lib/python3.7/site-packages/django/db/utils.py", line 96, in inner
    return func(*args, **kwargs)
django.db.utils.ProgrammingError: named cursor isn't valid anymore

Как исправить эту ошибку? Я использую PostgreSQL в качестве бэкенда базы данных.

Google находит, что очень немногие сталкивались с этой ошибкой, и не упоминает никаких решений специально для Django. Я читал, что добавление hold=True к вызову курсора может исправить ситуацию, но неясно, как задать это через ORM Django.

Я пытался добавить try/except, чтобы перехватить и продолжить запрос, но это не сработало. Поскольку трассировка даже не включает мой код, я не уверен, какая именно строка вызывает исключение.

Эта ошибка возникает после 2000 записей, потому что это значение по умолчанию chunk_size для итератора. После их удаления итератор не знает, с какого момента продолжать работу.

Я бы использовал пагинацию для этой задачи.

from django.core.paginator import Paginator
from django.db.models import Subquery

qs = MyModel.objects.filter(some_criteria=123)
paginator = Paginator(qs, 10000)

for page_num in reversed(paginator.page_range):
    MyModel.objects.filter(
        pk__in=Subquery(paginator.page(page_num).object_list.values('pk'))
    ).delete()

Здесь мы рассмотрим страницы в обратном порядке, чтобы избежать проблем, подобных тем, которые возникают при использовании iterator.

Мы не можем вызвать delete непосредственно на object_list, потому что это не разрешено с нарезанными наборами запросов, поэтому мы получаем pk объектов на странице и фильтруем их перед удалением. Subquery избавляет нас от одного дополнительного запроса за итерацию.

Вернуться на верх