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

Django Rest Framework关联COUNT查询优化求助:5000万行表查询超时

这个问题我之前处理过类似的,5000万行的tracks表确实会让JOIN+COUNT的查询直接卡死,给你几个实际可行的优化方案:

  • 用EXISTS子查询替代INNER JOIN + COUNT
    你现在的查询是通过JOIN然后COUNT所有匹配行,但实际上我们只需要判断专辑是否存在至少一个可查看的音轨,用EXISTS子查询效率会高很多——它找到第一个符合条件的记录就停止,不需要遍历所有匹配的tracks行。

    在Django ORM里可以这么写:

    from django.db.models import Exists, OuterRef
    
    # 替换原来的Album.objects.filter(tracks__viewable=1)
    queryset = Album.objects.annotate(
        has_viewable_tracks=Exists(
            Track.objects.filter(album_id=OuterRef('id'), viewable=1)
        )
    ).filter(has_viewable_tracks=True)
    

    对应的SQL会变成类似:

    SELECT `album`.*
    FROM `album`
    WHERE EXISTS (
        SELECT 1 FROM `tracks`
        WHERE `tracks`.`album_id` = `album`.`id` AND `tracks`.`viewable` = 1
    )
    

    这个查询的性能会比原来的COUNT查询提升几个数量级,尤其是tracks表数据量极大的时候。

  • 预计算状态字段(缓存结果)
    如果你的API对实时性要求不是极致苛刻,可以在Album表新增一个布尔字段,比如has_viewable_tracks,然后通过Django信号或者数据库触发器来维护这个字段的值——当track的viewable状态变化、新增或删除时,同步更新对应专辑的这个字段。

    比如用Django信号实现:

    from django.db.models.signals import post_save, post_delete
    from django.dispatch import receiver
    from .models import Track, Album
    
    @receiver(post_save, sender=Track)
    @receiver(post_delete, sender=Track)
    def update_album_viewable_status(sender, instance, **kwargs):
        album = instance.album
        # 用exists()快速判断是否有可查看音轨
        album.has_viewable_tracks = album.tracks.filter(viewable=1).exists()
        album.save(update_fields=['has_viewable_tracks'])
    

    之后查询就变成了单表过滤:

    queryset = Album.objects.filter(has_viewable_tracks=True)
    

    这个方案的性能是最优的,因为完全避免了关联大表的查询,适合数据更新频率不高或者可以接受微小延迟的场景。

  • 优化DRF分页的COUNT查询
    DRF的通用列表视图默认会执行COUNT查询来返回总页数,这正是你现在遇到的慢查询根源之一。如果你的API不需要返回总条数/总页数,可以自定义分页类跳过COUNT查询:

    from rest_framework.pagination import PageNumberPagination
    from rest_framework.response import Response
    
    class NoCountPagination(PageNumberPagination):
        def get_paginated_response(self, data):
            return Response({
                'next': self.get_next_link(),
                'previous': self.get_previous_link(),
                'results': data
            })
    
        def paginate_queryset(self, queryset, request, view=None):
            self.page_size = self.get_page_size(request)
            if not self.page_size:
                return None
    
            self.base_url = request.build_absolute_uri()
            self.page = self.paginator_class(queryset, self.page_size).get_page(
                request.query_params.get(self.page_query_param, 1)
            )
            self.request = request
            return list(self.page)
    

    然后在你的视图里指定这个分页类:

    class AlbumListView(generics.ListAPIView):
        queryset = Album.objects.all()
        serializer_class = AlbumSerializer
        pagination_class = NoCountPagination
    

    如果确实需要总条数,可以考虑用预计算的方式缓存总数,或者用数据库的近似计数(比如PostgreSQL的relpages)来替代精确COUNT。

  • 数据库复合索引优化
    虽然你说关联字段已经加了索引,但可以给tracks表创建(album_id, viewable)的复合索引——这个索引可以直接覆盖我们的查询条件,数据库不需要回表查询viewable的值,能进一步提升查询效率。

    用Django的迁移文件创建这个索引:

    from django.db import migrations, models
    
    class Migration(migrations.Migration):
        dependencies = [
            ('yourapp', 'previous_migration'),
        ]
    
        operations = [
            migrations.AddIndex(
                model_name='track',
                index=models.Index(fields=['album_id', 'viewable'], name='track_album_viewable_idx'),
            ),
        ]
    

总结一下,优先尝试第一个方案(EXISTS子查询),它不需要修改表结构,见效最快;如果性能还不够,再结合预计算字段和索引优化,基本上就能解决大表关联查询的问题了。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.20 10:17:43