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

Django中一对多关系序列化为数组的SQL优化方案咨询

这个问题我太熟悉了!之前做餐饮类项目的时候也踩过应用层循环查询的坑,数据量一上来就卡得不行。其实Django提供了好几种从SQL层面优化的方案,能彻底解决N+1查询的问题,给你详细说说:

方案1:Django序列化器 + prefetch_related(最通用)

这是Django生态里最常用的优化方式,核心思路是一次性预加载所有关联的菜品数据,避免循环时反复发起数据库查询。

先假设你的基础模型是这样的:

from django.db import models

class Restaurant(models.Model):
    name = models.CharField(max_length=100)
    address = models.CharField(max_length=200)

class Food(models.Model):
    name = models.CharField(max_length=100)
    price = models.DecimalField(max_digits=8, decimal_places=2)
    restaurant = models.ForeignKey(Restaurant, on_delete=models.CASCADE, related_name='foods')

如果用Django REST Framework(DRF)的序列化器,代码可以这么写:

from rest_framework import serializers

class FoodSerializer(serializers.ModelSerializer):
    class Meta:
        model = Food
        fields = ['id', 'name', 'price']

class RestaurantSerializer(serializers.ModelSerializer):
    # 关联菜品序列化器,many=True表示一对多关系
    foods = FoodSerializer(many=True, read_only=True)

    class Meta:
        model = Restaurant
        fields = ['id', 'name', 'address', 'foods']

关键来了!查询的时候一定要用prefetch_related预加载所有关联的菜品:

# 预加载所有餐厅对应的菜品,只会执行2次SQL查询
restaurants = Restaurant.objects.prefetch_related('foods').all()
# 序列化数据
serialized_data = RestaurantSerializer(restaurants, many=True).data
# 转成JSON格式
import json
json_result = json.dumps(serialized_data)

这里Django会先查所有餐厅,再一次性查所有关联的菜品,然后在内存中自动把菜品和对应的餐厅关联起来,完全避免了循环时的N+1查询。就算不用DRF,Django自带的django.core.serializers模块也能实现类似效果,只是灵活性稍差。

方案2:PostgreSQL JSON聚合(性能最优)

如果你的项目用的是PostgreSQL数据库,那可以直接让数据库层面完成菜品数据的聚合和JSON格式化,只需要1次SQL查询,性能比方案1更优。

需要用到Django为PostgreSQL提供的JSONAgg聚合函数:

from django.contrib.postgres.aggregates import JSONAgg
from django.db.models import F

# 直接在数据库层面聚合菜品数据为JSON数组
restaurants_with_foods = Restaurant.objects.annotate(
    foods=JSONAgg(
        # 用JSONObject指定要包含的菜品字段
        models.JSONObject(
            id=F('foods__id'),
            name=F('foods__name'),
            price=F('foods__price')
        )
    )
).values('id', 'name', 'address', 'foods')

# 转成JSON
json_result = json.dumps(list(restaurants_with_foods))

这种方式完全不需要应用层做数据关联,数据库直接返回已经聚合好的结果,数据量大的时候优势特别明显。注意要确保你的Django版本在2.0以上,PostgreSQL版本支持JSON函数(一般9.4+就没问题)。

方案3:手动预加载(适合简单场景)

如果不想用序列化器或者PostgreSQL特性,也可以手动用Prefetch控制预加载的字段,然后自己组装数据:

from django.db.models import Prefetch

# 先定义要预加载的菜品字段
foods_queryset = Food.objects.values('id', 'name', 'price')
# 预加载指定字段的菜品数据
restaurants = Restaurant.objects.prefetch_related(
    Prefetch('foods', queryset=foods_queryset)
).values('id', 'name', 'address')

# 手动组装包含菜品的字典
result = []
for restaurant in restaurants:
    # 从预加载的数据中筛选当前餐厅的菜品
    foods = list(food for food in restaurant['foods'])
    restaurant['foods'] = foods
    result.append(restaurant)

# 转成JSON
json_result = json.dumps(result)

不过这个方案代码比较繁琐,不如前两种优雅,适合快速实现简单需求的场景。

重要提醒

  • 别搞混select_related和prefetch_related:select_related只适用于一对一、多对一的正向关联,而餐厅到菜品是一对多的反向关联,必须用prefetch_related。
  • 如果用PostgreSQL聚合,记得给菜品外键加索引,能进一步提升聚合效率。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.19 10:23:22