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

重写get_queryset时Django QuerySet的求值时机及延迟问题排查

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.27 11:57:53