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

