You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Django ListView中替代regroup以降低内存占用的方案

问题描述

我尝试在模板中使用如下代码对ListView的queryset进行分组,按关联字段的值归类:

<div class="grid grid-cols-4">
    {% regroup object_list by related_field.sort_value as grouped_list %}
    {% for group in grouped_list %}
        <div class="border-2 border-black">  
        <span>... {{group.grouper}} ...</span>
        {% for item in group.list %}
            <p>{{item.related_field.name}}</p>
            <p>{{item.description}}</p>
        {% endfor %}
        </div>
    {% endfor %}
</div>

该模型包含约12个字段,其中2个为关联字段(第一个关联模型有数十个字段,第二个字段较少),ListView的queryset结果包含数百条数据。

视图代码:

class BigListView(ListView):
    model = MyModel
    template_name = 'my_model/big_list_view.html'
    def get_queryset(self):
        return MyModel.objects.order_by('related_model_b.sort_value')

模型代码:

class MyModel(models.Model):

    code = models.CharField(max_length=30, unique=True)
    price = models.DecimalField(max_digits=8, decimal_places=2)
    sale_price = models.DecimalField(max_digits=8, decimal_places=2)
    sale_price_quantity_3_5 = models.DecimalField(max_digits=8, decimal_places=2, blank=True, null=True)
    sale_price_quantity_6 = models.DecimalField(max_digits=8, decimal_places=2, blank=True, null=True)
    description = models.CharField(max_length=40)
    location = models.CharField(max_length=20)
    quantity_on_hand = models.IntegerField()
    size = models.CharField(max_length=20)
    tag_description = models.CharField(max_length=100)

    color = ColorField(help_text='Theme Color')

    image = models.ImageField(upload_to=assign_product_sku, blank=True, null=True)

    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    related_model_a = models.ForeignKey(RelatedModelA, related_name='mymodels', on_delete=models.CASCADE)

    related_model_b = models.ForeignKey('RelatedModelB', related_name='mymodels', on_delete=models.CASCADE, blank=True, null=True)

class RelatedModelA(index.Indexed, models.Model):

    sku = models.CharField(max_length=20, unique=True)
    name = models.CharField(max_length=100)
    alt_name = models.CharField(max_length=100, blank=True)
    description = models.TextField(blank=True)
    age = models.CharField(max_length=3, blank=True)
    size = models.CharField(max_length=100, blank=True)
    weight = models.CharField(max_length=100, blank=True)
    shape = models.CharField(max_length=100, blank=True)
    style = models.CharField(max_length=100, blank=True)
    rate = models.CharField(max_length=100, blank=True)
    attribute_a = models.CharField(max_length=100, blank=True)
    attribute_b = models.CharField(max_length=100, blank=True)
    attribute_c = models.CharField(max_length=100, blank=True)
    attribute_d = models.CharField(max_length=100, blank=True)
    attribute_e = models.CharField(max_length=100, blank=True)
    attribute_f = models.CharField(max_length=100, blank=True)
    attribute_g = models.CharField(max_length=100, blank=True)
    attribute_h = models.CharField(max_length=100, blank=True)
    attribute_i = models.CharField(max_length=100, blank=True)
    attribute_j = models.CharField(max_length=100, blank=True)
    attribute_k = models.CharField(max_length=100, blank=True)
    attribute_l = models.CharField(max_length=100, blank=True)
    attribute_m = models.CharField(max_length=100, blank=True)
    attribute_n = models.CharField(max_length=100, blank=True)
    attribute_o = models.CharField(max_length=100, blank=True)
    attribute_p = models.CharField(max_length=100, blank=True)
    attribute_q = models.CharField(max_length=40, blank=True)
    attribute_r = models.CharField(max_length=100, blank=True)
    attribute_s = models.CharField(max_length=100, blank=True)
    comment = models.CharField(max_length=100, blank=True)

    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    image_url = models.URLField(blank=True)

    slug = models.SlugField(max_length=100, blank=True)
    tags = TaggableManager(blank=True)

class RelatedModelB(models.Model):
    code = models.CharField(max_length=30, unique=True)
    sort_value = models.IntegerField()
    last_retrieved = models.DateTimeField(auto_now=True)
    json_data = models.JSONField(blank=True, null=True)

本地开发环境运行正常,但在Heroku生产环境访问该视图时,因内存占用过高(约2GB)导致应用崩溃。当前应用使用Hobby规格(512MB内存),即使升级到Standard 1x/2x仍不足,更高性能的dyno又过于冗余,仅该视图存在此问题。

请问有什么方法可以降低该视图的内存占用?

解决方案

  • 只查询必要字段:当前queryset会加载MyModel的所有字段(包括大字段image)以及关联模型RelatedModelA的全部数十个字段,这是内存占用高的核心原因。使用only()精准指定模板中用到的字段,配合select_related预取关联模型避免N+1查询:

    def get_queryset(self):
        return MyModel.objects.select_related('related_model_b')\
                              .only('description', 'related_model_a__name', 'related_model_b__sort_value')\
                              .order_by('related_model_b__sort_value')
    
  • 在视图层提前分组:Django模板的regroup标签会将整个queryset加载到内存中处理,换成在视图中用itertools.groupby提前分组,减少模板层面的内存开销:

    from itertools import groupby
    
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        queryset = self.get_queryset()
        # groupby依赖已排序的数据集,这里queryset已按related_model_b__sort_value排序
        grouped_list = []
        for sort_val, items in groupby(queryset, key=lambda x: x.related_model_b.sort_value):
            grouped_list.append({'grouper': sort_val, 'list': list(items)})
        context['grouped_list'] = grouped_list
        return context
    

    修改模板,直接遍历grouped_list即可,去掉regroup逻辑。

  • 启用分页:如果业务允许,对数据进行分页,每次仅加载部分数据到内存。ListView原生支持分页,只需添加paginate_by属性:

    class BigListView(ListView):
        model = MyModel
        template_name = 'my_model/big_list_view.html'
        paginate_by = 20  # 每页显示20条数据
        def get_queryset(self):
            return MyModel.objects.select_related('related_model_b')\
                                  .only('description', 'related_model_a__name', 'related_model_b__sort_value')\
                                  .order_by('related_model_b__sort_value')
    

    模板中添加分页导航代码,即可实现分页浏览。

  • 排除非必要大字段:对于image、json_data这类占用内存较大的字段,使用defer()排除,避免加载到内存:

    def get_queryset(self):
        return MyModel.objects.select_related('related_model_b')\
                              .defer('image', 'sale_price_quantity_3_5', 'sale_price_quantity_6')\
                              .order_by('related_model_b__sort_value')
    

内容的提问来源于stack exchange,提问作者pspahn

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.17 18:15:37