在* Large * Django QuerySet中限制内存使用

时间:2011-01-31 22:31:38

标签: python django memory-management django-queryset

我有一个任务需要每隔一段时间(每天一次,每周一次,无论如何)在我的数据库中的“大多数”对象上运行一次。基本上这意味着我有一些看起来像这样的查询在它自己的线程中运行。

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

(请注意,它实际上是一个filter()而不是所有()但是我仍然最终选择非常大的对象集。)

我遇到的问题是,运行一段时间之后线程被我的托管服务提供商杀死,因为我使用了太多内存。我假设所有这些内存使用正在发生,因为即使我的查询返回的QuerySet对象最初具有非常小的内存占用,它最终会增长为QuerySet对象在我遍历它们时缓存每个model_instance

我的问题是,“以内存有效的方式迭代数据库中几乎每个SomeModel的最佳方法是什么?”或者我的问题是“如何从django查询集中取消缓存'模型实例?”

编辑:我实际上是使用查询集的结果来构建一系列新对象。因此,我最终不会更新查询的对象。

8 个答案:

答案 0 :(得分:17)

所以我实际上最终做的是构建一些可以“封装”QuerySet的东西。它通过使用切片语法(例如some_queryset[15:45] - 对QuerySet进行深度复制来实现 - 但是然后当切片完全迭代完毕后,它会对原始QuerySet进行另一次深度复制。这意味着只有'this'特定切片中返回的对象集存储在内存中。

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解释器中的 save memory 。它主要是通过更多数据库查询来实现的。通常人们试图与此完全相反 - 即,尽可能地减少数据库查询而不考虑内存使用情况。希望有人会觉得这很有用。

答案 1 :(得分:11)

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

更改页面大小以满足您的要求,并且如果它更适合您,可以将[offset:offset + pagesize]更改为[offset * pagesize:(offset + 1)* pagesize]惯用法。当然,也可以用实际的型号名称替换Model。

答案 2 :(得分:11)

如何使用此处记录的django core的Paginator和Page对象:

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

答案 3 :(得分:8)

许多解决方案通过切片查询集来实现sql OFFSETLIMIT。正如斯特凡诺指出的那样,对于更大的数据集,这变得非常低效。处理此问题的正确方法是使用服务器端光标来跟踪OFFSET。

本机服务器端游标支持为in the works for django。在它准备就绪之前,如果您使用带有psycopg2后端的postgres,这是一个简单的实现:

def server_cursor_query(Table):
    table_name = Table._meta.db_table

    # There must be an existing connection before creating a server-side cursor
    if connection.connection is None:
        dummy_cursor = connection.cursor()  # not a server-side cursor

    # Optionally keep track of the columns so that we can return a QuerySet. However,
    # if your table has foreign keys, you may need to rename them appropriately
    columns = [x.name for x in Table._meta.local_fields]

    cursor = connection.connection.cursor(name='gigantic_cursor')) # a server-side
                                                                   # cursor

    with transaction.atomic():
        cursor.execute('SELECT {} FROM {} WHERE id={}'.format(
            ', '.join(columns), table_name, id))

        while True:
            rows = cursor.fetchmany(1000)

                if not rows:
                    break

                for row in rows:
                    fields = dict(zip(columns, row))
                    yield Table(**fields)

有关django中大型查询的内存问题的详细解释,请参阅this blog post

答案 4 :(得分:3)

我正在继续研究,看起来我想要做相当于SQL OFFSET和LIMIT,根据Django Doc's on Limiting Querysets意味着我想使用切片语法,例如{{1} }

所以现在我想也许这就是我正在寻找的东西:

SomeModel.objects.all()[15:25]

通过我的计算,这将使# Figure out the number of objects I can safely hold in memory # I'll just say 100 for right now number_of_objects = 100 count = SomeModel.objects.all().count(): for i in xrange(0,count,number_of_objects): smaller_queryset = SomeModel.objects.all()[i:i+number_of_objects] for model_instance in smaller_queryset: do_something(model_instance) 永远不会变得太大。

答案 5 :(得分:3)

有一个django代码段:

http://djangosnippets.org/snippets/1949/

它通过生成原始查询集的较小“块”行来迭代查询集。最终使用的内存显着减少,同时允许您调整速度。我在我的一个项目中使用它。

答案 6 :(得分:0)

以下是一个查询集迭代器,该查询集迭代器将查询集拆分为多个块,并且不比基本迭代器慢很多(这是数据库查询的线性数量,而不是1,但是每1,000行仅查询一次) 。此功能按主键进行分页,这对于高效实现是必需的,因为在大多数SQL数据库中,offset是线性时间操作。

def queryset_iterator(queryset, page_size=1000):
    if not queryset:
        return
    max_pk = queryset.order_by("-pk")[0].pk
    # Scale the page size up by the average density of primary keys in the queryset
    adjusted_page_size = int(page_size * max_pk / queryset.count())
    
    pages = int(max_pk / adjusted_page_size) + 1
    for page_num in range(pages):
        lower = page_num * adjusted_page_size
        page = queryset.filter(pk__gte=lower, pk__lt=lower+page_size)
        for obj in page:
            yield obj

使用方式如下:

for obj in queryset_iterator(Model.objects.all()):
    # do stuff

此代码具有三个假设:

  1. 您的主键是整数(不适用于UUID主键)。
  2. 查询集的主键至少在某种程度上均匀分布。如果不是这样,adjusted_page_size可能会变得太大,并且在迭代过程中可能会得到一个或几个大页面。

为了了解开销,我在具有40,000个条目的Postgres表上对此进行了测试。与原始迭代相比,queryset_iterator将迭代时间增加了大约80%(2.2秒与1.2秒)。尽管页面大小开始增加到200以下,但开销对于200到10,000之间的页面并没有太大变化。

答案 7 :(得分:0)

以下方法不使用昂贵的数据库偏移查询,并避免计算页码,从而提高效率。 文档字符串中指定的限制。

def queryset_pk_iterator(queryset, batch_size=1000):
    """
    Iterator that splits the queryset into batches to reduce memory consumption.
    Useful in cases where builtin .iterator() method of the queryset skips the "prefetch_related" optimization.

    :param queryset: Queryset to iterate over. The supplied queryset must not specify order and limit/offset.
        Queryset objects must have a monotonically increasing and ordering primary key.
    :param batch_size: Size of the batches into which to split the queryset.
    :return: iterator object
    """
    pk = None
    while True:
        batch_queryset = queryset.order_by('pk')
        if pk is not None:
            batch_queryset = batch_queryset.filter(pk__gt=pk)
        batch_queryset = batch_queryset[:batch_size]
        obj = None
        for obj in batch_queryset:
            yield obj
        if obj is None:
            return
        pk = obj.pk