限制*大* Django QuerySet中的内存使用

35 投票
8 回答
19369 浏览
提问于 2025-04-16 10:58

我有一个任务,需要定期在我的数据库中的“绝大多数”对象上运行(比如每天一次,每周一次,随便)。基本上,这意味着我有一个查询,它在自己的线程中运行,像这样。

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

(注意,这实际上是一个filter()而不是all(),但无论如何,我最终还是选择了一个非常大的对象集合。)

我遇到的问题是,运行一段时间后,我的托管服务提供商会杀掉这个线程,因为我使用的内存太多。我猜测,所有这些内存使用都是因为虽然我查询返回的QuerySet对象最开始占用的内存很小,但随着我遍历这些对象,QuerySet对象会缓存每一个model_instance,导致内存不断增加。

我的问题是,“有什么好的方法可以以节省内存的方式遍历几乎所有的SomeModel对象?”或者也许我的问题是“如何从django的查询集中‘取消缓存’模型实例?”

编辑:我实际上是使用查询集的结果来构建一系列新对象。因此,我根本没有更新被查询的对象。

8 个回答

14

你不能使用 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

你可以根据自己的需求调整 pagesize,如果需要的话,还可以把 [offset : offset + pagesize] 改成 [offset * pagesize : (offset + 1) * pagesize],这样可能更适合你。当然,别忘了把 Model 替换成你实际使用的模型名称。

25

那使用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
17

我最后做的事情是创建了一个可以“包装”查询集(QuerySet)的东西。它的工作原理是先对查询集进行深拷贝,然后使用切片语法,比如说 some_queryset[15:45],但是在这个切片完全遍历完之后,它会再对原始查询集进行一次深拷贝。这意味着在内存中只存储了“这个”特定切片返回的对象。

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 解释器节省内存。它的基本原理是通过进行 更多 的数据库查询来实现的。通常,人们的目标是尽量减少数据库查询次数,而不太考虑内存使用。不过希望有人会觉得这个方法有用。

撰写回答