编程 Django REST Framework 序列化器的隐藏成本:正确代码不等于高效代码

2026-09-07 16:36:31

Django REST Framework 序列化器的隐藏成本:正确代码不等于高效代码

Django 应用最让人困惑的性能问题之一:PostgreSQL 查过了、查询看起来很快、视图也优化了,一切正常——但接口仍然要几百毫秒甚至几秒。开发者经常忘记检查的一个地方是 DRF 序列化器。序列化器并不总是只把 Python 对象转成 JSON,它可能触发数据库查询、访问关系、执行 Python 代码、计算字段、递归序列化嵌套对象——开销就藏在这里。

藏在序列化器里的 N+1 查询

一个看似合理的序列化器:

class VehicleSerializer(serializers.ModelSerializer):
    customer_name = serializers.SerializerMethodField()

    class Meta:
        model = Vehicle
        fields = ("id", "plate_number", "customer_name")

    def get_customer_name(self, obj):
        return obj.customer.name

看起来完全正常。但 obj.customer 从哪来?如果 customer 关系没被加载,Django 可能再执行一条 SQL。100 辆车就是:1 条查询取车辆 + 100 条查询取客户 = 101 条。这是经典 N+1 问题,危险在于序列化器本身看起来不像数据库操作。

SerializerMethodField 不是免费的

SerializerMethodField 是 DRF 最好用的特性之一,也是不经意造出昂贵 API 最容易的途径之一:

class ServiceSerializer(serializers.ModelSerializer):
    last_service = serializers.SerializerMethodField()

    def get_last_service(self, obj):
        return (Service.objects
                .filter(ownership=obj.ownership)
                .order_by("-created_at")
                .first())

每次调用 get_last_service() 都执行一次数据库查询。序列化 500 条服务?可能 1 条主查询 + 500 条附加查询 = 501 条。代码可读、API 正确、性能却可能很糟。这就是为什么正确代码不等于高效代码。

修复通常不在序列化器内部

优化 Django API 最重要的一课:序列化器不该负责发现数据库本可以准备好的数据。别为每个对象查询,把工作移进 QuerySet:

from django.db.models import OuterRef, Subquery

last_service = Service.objects.filter(
    ownership=OuterRef("ownership"),
).order_by("-created_at")

queryset = Vehicle.objects.annotate(
    last_service_id=Subquery(
        last_service.values("id")[:1]
    )
)

现在数据库在主查询里准备好信息,序列化器变简单(只读 IntegerField)。架构从"数据库 → 序列化器 → 再查一次 → 再查一次"变成"数据库 → 优化 QuerySet → 注解 → 序列化器 → JSON"。

用 DRF 序列化器时永远注意被访问的关系。单值关系(ForeignKey、OneToOneField)用 select_related()——Django 用 SQL JOIN 一次取回车辆和客户;集合(ManyToManyField、反向 ForeignKey)用 prefetch_related()。简单规则:ForeignKey/OneToOne → select_related();ManyToMany/反向 FK → prefetch_related()。这是防止 Django API N+1 最重要的两个工具。

嵌套序列化器更糟

嵌套序列化器方便,但便利会藏昂贵访问。服务序列化器嵌套 ProductSerializer(many=True),API 返回 1000 条服务、每条都要 products——不预取就变成 1 条服务查询 + 1000 条产品查询。嵌套还能更深:Service → Customer → Address、Service → Products → Category。JSON 里看起来无辜的响应,可能代表惊人的数据访问模式。

别建一个巨型序列化器

另一种常见错误:一个序列化器用到底。VehicleSerializer 被 list/detail/create/update/admin/search 全部端点共用,最终变得巨大。list 端点可能只需要 (id, plate_number, color),detail 端点才提供更多信息(含嵌套 customer)。这不是不必要的重复,是 API 设计:list 端点不该因为 detail 端点需要就返回整个对象图。更小的响应通常更好——每个额外关系都会增加数据库工作、序列化时间、响应大小、内存、网络传输和前端处理。API 该返回客户端需要的数据,不是数据库知道的一切。

先测量再优化

别看着序列化器说"这可能造成性能问题",去测它。Django API 值得测的指标:SQL 查询数、SQL 查询时长、序列化器执行时间、总请求时长、响应大小、CPU 使用。工具:Django Debug Toolbar、Django Silk、APM 平台。因为有时候数据库不是瓶颈——数据库 40ms、视图逻辑 30ms、序列化 650ms、JSON 渲染 80ms、总计 800ms,优化 40ms 的查询解决不了 800ms 的响应。给整个请求做剖析。

DRF 性能检查清单

接口变慢时从这开始:数查询(端点执行多少条 SQL);检查 SerializerMethodField(get_<field_name>() 里有没有数据库访问);检查关系访问(obj.customer、obj.owner、obj.category 会不会触发查询);检查嵌套序列化器深度;确认 select_related/prefetch_related 用对了;给列表和详情用不同序列化器;测量再决定改哪里。

来源:The Hidden Cost of Django REST Framework Serializers - DEV Community

复制全文 生成海报 Django DRF 性能 数据库

推荐文章

程序员茄子在线接单