优化djangos count()方法

时间:2016-07-01 19:17:45

标签: python sql django django-rest-framework

所以我有这个查询集:

from django.contrib.gis.db.models.query import GeoQuerySet
from django.db import models as base_models

class RestaurantsQuerySet(GeoQuerySet):
    def get_list(self, lng, lat):
        reference_point = Point(lng, lat, srid=SRID)

        return self.annotate(rating=models.Avg('comments__rating'))\
                   .annotate(distance=Distance('location', reference_point))

    def count(self):
        return self.values('id').aggregate(count=base_models.Count('id'))['count']

我认为查询看起来像:

SELECT COUNT("__col1") 
FROM (
    SELECT "restaurants_restaurant"."id" AS "__col1" 
    FROM "restaurants_restaurant" 
    GROUP BY "restaurants_restaurant"."id") subquery

而django orm创造了这个小怪物:

    SELECT COUNT("__col1") 
    FROM (
        SELECT "restaurants_restaurant"."id" AS Col1,   "restaurants_restaurant"."id" AS "__col1" 
        FROM "restaurants_restaurant" 
        LEFT OUTER JOIN "comments_comment" ON ("restaurants_restaurant"."id" = "comments_comment"."restaurant_id") 
        GROUP BY "restaurants_restaurant"."id", ST_Distance_Sphere("restaurants_restaurant"."location", 
                 ST_GeomFromEWKB('\x0101000020e61000003eb555a41d2d4b405a338d81d0a73240'::bytea
))) subquery

要调用的第一个方法是get_list。看起来好像django会记得"该调用以及qs已使用ratingdistance进行注释,并将其放入count查询中。所以我想问题是 - 我如何"重置"这个查询集在注释之前的状态?

修改

好的,似乎我的问题还不完整。我还有RestaurantsList视图定义如下:

class RestaurantList(generics.ListAPIView):
    def get_queryset(self):
        return Restaurant.objects.get_list(self._lng, self._lat)

我看了一下django-rest-framework的内脏,我可以看到:

class ListModelMixin(object):
    """
    List a queryset.
    """
    def list(self, request, *args, **kwargs):
        queryset = self.filter_queryset(self.get_queryset())

        page = self.paginate_queryset(queryset)
        if page is not None:
            serializer = self.get_serializer(page, many=True)
            return self.get_paginated_response(serializer.data)

        serializer = self.get_serializer(queryset, many=True)
        return Response(serializer.data)

所以看起来它始终使用从get_queryset方法返回的查询集,因为它使用distancerating进行注释,最终会包含在计数查询。仍然没有解决方案...

1 个答案:

答案 0 :(得分:0)

在浏览了django-rest-framework代码后,我提出了这个想法:

  1. 覆盖给定视图的默认Paginator类:

    class RestaurantList(generics.ListAPIView):
        pagination_class = custom.LimitOffsetPagination
    
  2. 创建自定义分页类:

    class LimitOffsetPagination(pagination.LimitOffsetPagination):
        def __init__(self):
            self._countable_queryset = None
            self._was_counted = False
    
        def was_initialized(self):
            return self._countable_queryset is not None
    
        def set_raw_queryset_for_count(self, queryset: QuerySet):
            self._countable_queryset = queryset
    
        def paginate_queryset(self, queryset, request, view=None):
            self.limit = self.get_limit(request)
            if self.limit is None:
                return None
            self.offset = self.get_offset(request)
            self.count = self._countable_queryset.count()
            self.request = request
            if self.count > self.limit and self.template is not None:
                self.display_page_controls = True
            return list(queryset[self.offset:self.offset + self.limit])
    
  3. 覆盖视图paginator属性:

    @property
    def paginator(self):
        paginator = super().paginator
        if not paginator.was_initialized():
            paginator.set_raw_queryset_for_count(Restaurant.objects.all())
        return paginator
    
  4. 现在计数查询看起来会更友好

    SELECT COUNT(*) AS "__count" FROM "restaurants_restaurant"