重写get_queryset时Django QuerySet的求值时机及延迟问题排查
问题说明
在重写Django REST Framework视图的get_queryset方法后,对返回的QuerySet求值时机存在疑问:明明get_queryset会立即返回,但请求却要等待很久才完成;手动执行主查询仅需750ms,但接口响应耗时50秒。同时想了解如何强制QuerySet立即求值。
代码场景
前端请求代码:
//Dashboard.js: export default function Dashboard() { const [tableData, setTableData] = useState([]); const getUserSchools = () => { getUser({ email: keycloak.email }).then((response) => { const { data: users } = response; if (users.length) { const appUser = users[0]; axios .get(`/api/school/list/?user_id=${appUser.id}`) .then((data) => { setTableData(data.data.results); }) .catch((err) => { /* eslint-disable-next-line */ console.error(err); setTableData([]); }); } }); };
路由配置:
#school-urls.py urlpatterns = [ url(r"^list/$", SchoolsList.as_view()), ]
视图代码:
#list.py class LargeResultsSetPagination(pagination.PageNumberPagination): page_size = 10 page_size_query_param = 'page_size' max_page_size = 100 class SchoolsList(generics.ListAPIView): pagination_class = LargeResultsSetPagination serializer_class = SchoolGetSerializer def get_queryset(self): queryset = School.objects.all() user_id = self.request.query_params.get('user_id', None) if (user_id is not None): queryset = queryset.filter(user_id=user_id) return queryset.order_by('id')
序列化器代码:
#get.py class SchoolGetSerializer(serializers.ModelSerializer): nearest_community = SchoolCommunitySerializer(required=False) nearest_post_secondary = SchoolPostSecondarySerializer(required=False) regional_district = SchoolRegionalDistrictSerializer(required=False) class Meta: model = School fields = ( "id", "address", "nearest_post_secondary", "nearest_community", "regional_district", )
核心解答
1. QuerySet的求值时机
get_queryset方法只是构造并返回一个未执行的QuerySet对象,这是Django QuerySet的惰性求值特性:只有当需要实际使用数据时(比如迭代、序列化、转换为列表、调用count()/exists()等),才会真正访问数据库执行查询。
在DRF的ListAPIView执行流程中,QuerySet的求值发生在序列化器处理数据阶段:视图会先调用get_queryset拿到QuerySet,之后进行分页处理,最后将分页后的QuerySet传入序列化器,序列化器在遍历实例、序列化关联字段时,才会触发数据库查询。
2. 请求耗时久的原因
你手动执行的只是主School查询,但接口的50秒耗时来自序列化阶段的N+1查询问题:
- 主查询(
School.objects.filter(user_id=xxx))仅需750ms,但序列化器中嵌套的nearest_community、nearest_post_secondary等字段,会对每个School实例单独发起一次关联查询。 - 如果分页返回10条School记录,就会额外发起10次社区查询、10次高校查询(甚至更多),若这些关联查询本身耗时较长,总耗时就会累积到50秒。
3. 优化方案(含强制求值方法)
(1)解决N+1查询:预加载关联数据
这是解决性能问题的核心方案,在get_queryset中使用select_related(针对一对一/外键关联)或prefetch_related(针对多对多/反向外键关联)预加载所有需要的关联数据,将多次查询合并为少数几次:
def get_queryset(self): queryset = School.objects.all() user_id = self.request.query_params.get('user_id', None) if user_id is not None: queryset = queryset.filter(user_id=user_id) # 预加载关联字段,根据关联类型选择对应方法 queryset = queryset.select_related('nearest_community', 'regional_district')\ .prefetch_related('nearest_post_secondary') return queryset.order_by('id')
(2)强制QuerySet立即求值
如果需要让主查询在get_queryset阶段就执行,可以通过以下方式:
- 转换为列表:
queryset = list(queryset.order_by('id')) - 使用
iterator()(适合大数据量,减少内存占用):queryset = queryset.order_by('id').iterator() - 调用
count()/exists()(仅用于判断数量或存在性场景)
注意:强制求值只是提前执行主查询,若嵌套序列化器的关联查询未优化,N+1问题依然存在,所以必须结合预加载方案才能彻底解决性能问题。
(3)临时验证:简化序列化器
正如你补充的,去掉耗时的嵌套字段后响应变快,这可以快速验证关联查询是性能瓶颈。在非必要场景下,可以拆分序列化器,提供不同数据粒度的接口,或使用SerializerMethodField按需加载关联数据。
内容的提问来源于stack exchange,提问作者JarochoEngineer

