Django 多表字段计算优化:处理None值并提升配方成本计算效率
性能瓶颈根因
- 存在典型的N+1查询问题:循环遍历每个带配方的Item时,
get_recette_price方法都会单独发起一次数据库查询拉取所有原料的最新价格,100个配方就会生成上百次独立查询,数据库连接开销占了90%以上的耗时 - 运行时的无效开销:通过遍历模型元字段、把数据库值转成字符串再转整数的方式提取配方字段,没有任何数据库计算下推的优势,白白消耗Python进程资源
- 重复的逻辑实现:同一条「查询单个Item最新价格」的逻辑,在Item本身的注解、原料价格查询两处重复实现,没有做批量复用
- 对None值的适配逻辑鲁棒性差:靠切片列表的方式拆分数量、原料列表,一旦未使用字段的None判断出错,就会直接出现计算错位
第一阶段:零表结构改动的即时优化
1. 把所有计算下推到数据库,彻底消除N+1查询
完全去掉循环内的数据库查询,利用Django的ORM注解,在单条SQL里完成所有配方的成本计算,天然适配稀疏的None值场景:
from django.db.models import OuterRef, Subquery, F, Value, IntegerField, Coalesce # 定义公共的最新价格查询子查询,避免重复写逻辑 def get_latest_price_subquery(ressource_field): """生成针对单个原料字段的最新价格查询子查询""" return Subquery( Prix_x1.objects.filter( item=OuterRef(ressource_field) ).order_by('-saved_at').values('prix')[:1], output_field=IntegerField() ) # 给Recette批量注解配方成本 recette_annotations = {} total_cost_expr = Value(0, output_field=IntegerField()) for i in range(1, 9): qte_field = f'qte_ressource_{i}' ressource_field = f'ressource_{i}' # 每对原料的成本:对应最新价格 * 需求数量,None值自动转0 cost_expr = Coalesce( get_latest_price_subquery(ressource_field) * F(qte_field), Value(0), Value(0), output_field=IntegerField() ) recette_annotations[f'cost_{i}'] = cost_expr total_cost_expr = total_cost_expr + cost_expr recette_annotations['total_craft_cost'] = total_cost_expr # 带成本注解的配方查询 recette_qs = Recette.objects.all().annotate(**recette_annotations)
这种写法的效果:原来需要几百次的查询压缩为1次,数据库层面完成所有乘法、加法计算,None值的字段会被Coalesce自动转成0,完全不需要手动判断哪些字段是空的。
2. 简化Python侧的字段提取逻辑
替换原来遍历模型元字段的get_fields方法,直接按字段名取值,天然适配None场景:
def get_recette_price(self): total = 0 is_recette_ok = True for i in range(1, 9): qte = getattr(self, f'qte_ressource_{i}') ressource = getattr(self, f'ressource_{i}') # 跳过未使用的空字段 if qte is None and ressource is None: continue # 只要有非空的字段缺了数量或者原料,就标记配方无效 if qte is None or ressource is None: is_recette_ok = False break # 这里的价格已经通过预加载/数据库注解提前取到,不需要临时查库 ressource_price = getattr(ressource, 'latest_price', None) if ressource_price is None: is_recette_ok = False break total += qte * ressource_price return total, is_recette_ok
配合prefetch_related批量预加载所有关联原料的最新价格,完全避免循环内的数据库查询:
from django.db.models import Prefetch # 预加载所有原料的最新价格 items = Item.objects.filter( recette__isnull=False ).select_related( 'recette' ).annotate( latest_hdv_price=Subquery( Prix_x1.objects.filter(item=OuterRef('pk')).order_by('-saved_at').values('prix')[:1] ) ).prefetch_related( # 预加载每个Recette关联的所有原料Item,带上最新价格注解 Prefetch( 'recette__ressource_1', queryset=Item.objects.annotate(latest_price=get_latest_price_subquery('pk')) ), # 重复给ressource_2到ressource_8做同样的预加载 )
第二阶段:长期结构优化
1. 调整模型结构,从根本解决扩展性问题
当前固定8对原料/数量字段的设计,未来如果需要支持更多原料,必须修改表结构加字段,维护成本很高。建议把关联关系拆分为中间表:
class RecetteRessource(models.Model): recette = models.ForeignKey(Recette, on_delete=models.CASCADE, related_name='ressource_entries') ressource = models.ForeignKey(Item, on_delete=models.CASCADE) required_qte = models.IntegerField()
拆分后的优势:
- 天然支持任意数量的原料,不需要再维护8对固定字段,None值的判断逻辑完全消失
- 可以直接通过聚合查询一次性算出所有配方的总成本:按配方分组,关联原料的最新价格,求和
required_qte * latest_price即可,不需要逐字段做乘法 - 后续的查询逻辑会非常简洁,没有硬编码的字段数遍历
2. 加必要的数据库索引
给Prix_x1表的(item, saved_at)加联合索引,因为每次查询最新价格都是按item过滤、按saved_at倒序取第一条,加索引后价格查询的速度会提升一个数量级。
内容的提问来源于stack exchange,提问作者Thibault Bacqueyrisses
相关产品推荐
相关产品推荐

