You need to enable JavaScript to run this app. Django 多表字段计算优化:处理None值并提升配方成本计算效率
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.31 23:15:04