【问题标题】:Iterating over large querysets in Django迭代 Django 中的大型查询集
【发布时间】:2021-11-15 23:11:05
【问题描述】:

如何使用 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 添加到光标调用我的修复它,但不清楚如何通过Django 的ORM 进行设置。

我尝试添加一个 try/except 来捕获并继续查询,但这没有奏效。由于回溯甚至不包括我的代码,我不确定是哪一行触发了异常。

【问题讨论】:

标签: python django postgresql


【解决方案1】:

此错误发生在 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 类似的问题。

我们不能直接在object_list 上调用delete,因为切片查询集不允许这样做,因此我们获取页面上对象的pks 并在删除之前过滤它们。 Subquery 使我们免于每次迭代的额外查询。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-02-04
    • 2019-02-05
    • 1970-01-01
    相关资源
    最近更新 更多