Итерация над большими наборами запросов в 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
избавляет нас от одного дополнительного запроса за итерацию.