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

