Django Rest Framework 分页极慢计数

Posted

技术标签:

【中文标题】Django Rest Framework 分页极慢计数【英文标题】:Django Rest Framework pagination extremely slow count 【发布时间】:2015-10-22 18:30:07 【问题描述】:

我在 Django Rest 框架中打开了分页,它似乎非常慢。 Count 看起来像是罪魁祸首,由于表中有数百万行,每次返回都需要数百毫秒。

我使用 postgresql 作为数据库。有没有办法不计算行数并仍然使用分页?如果我手动过滤查询集,则在启用此功能之前性能很好。

【问题讨论】:

粘贴代码让问题更清晰。 粘贴代码的方式并不多。我添加的唯一让它变慢的是对 settings.py 的修改:'DEFAULT_PAGINATION_CLASS':'api.pagination.StandardResultsSetPagination' 【参考方案1】:

问题在于,用于计数的查询与用于获取数据的潜在复杂查询相同。那比较浪费。 PageNumberPagination 在内部使用 Django 自己的 Paginator

为了使计数查询更简单,覆盖 DRF 使用的分页器类:

from django.core.paginator import Paginator
from django.utils.functional import cached_property
from rest_framework.pagination import PageNumberPagination

class FasterDjangoPaginator(Paginator):
    @cached_property
    def count(self):
        # only select 'id' for counting, much cheaper
        return self.object_list.values('id').count()


class FasterPageNumberPagination(PageNumberPagination):
    django_paginator_class = FasterDjangoPaginator

【讨论】:

非常好的解决方案,谢谢!将使我的应用程序在任何地方都更快:)【参考方案2】:

覆盖分页类的get_paginated_response 方法,并且不包括计数。你可以参考PageNumberPagination 类的base implementation 看看你应该返回什么。

from rest_framework.pagination import PageNumberPagination
from collections import OrderedDict # requires Python 2.7 or later

class PageNumberPaginationWithoutCount(PageNumberPagination):
    # Set any other options you want here like page_size

    def get_paginated_response(self, data):
        return Response(OrderedDict([
            ('next', self.get_next_link()),
            ('previous', self.get_previous_link()),
            ('results', data)
        ]))

然后在您的settings.py 中,将DEFAULT_PAGINATION_CLASS 设置为您的新分页类。

DEFAULT_PAGINATION_CLASS = 'path.to.PageNumberPaginationWithoutCount'

这种方法在example in the pagination docs中使用。

编辑: 从下面的 cmets 听起来这可能不足以防止慢速 sql 查询,因此您可能还需要覆盖 paginate_queryset

【讨论】:

"覆盖你的分页类的 get_paginated_response 方法,并且不包括计数。"借调,是的。您可能还需要考虑将 CursorPagination 用于大型数据集,因为它不会随着用户页面越远而变慢,这与页面和偏移样式不同。 我试过了,它从结果中删除了计数,但仍然根据 django 调试工具包运行它。但是,覆盖 paginate_queryset(self, queryset, request, view=None): 并设置 self.count = 0 而不是 self.count = _get_count(queryset) 对其进行排序。我也只是禁用了下一个和上一个,因为我不需要它们。 你是如何覆盖 paginate_queryset 的?【参考方案3】:

如果没有计数就可以了,可以使用下一个和上一个链接,以下自定义类。

import sys
from collections import OrderedDict

from django.core.paginator import Paginator
from django.utils.functional import cached_property
from rest_framework.pagination import PageNumberPagination
from rest_framework.response import Response


class CustomPaginatorClass(Paginator):
    @cached_property
    def count(self):
        return sys.maxsize


# To Avoid large table count query, We can use this paginator class
class LargeTablePagination(PageNumberPagination):
    django_paginator_class = CustomPaginatorClass

    def get_paginated_response(self, data):
        return Response(OrderedDict([
            ('page', self.page.number),
            ('results', data)
        ]))

【讨论】:

PageNumberPagination 似乎没有 django_paginator_class 吗?或许曾经如此?不过,这是在正确的轨道上。【参考方案4】:

其他答案要么对我不起作用,要么仍在执行额外的COUNT(*) 查询。

这将摆脱所有分页、计数查询,并且只返回 JSON 响应:

from rest_framework.pagination import PageNumberPagination


class NoCountPagination(PageNumberPagination):
    page_size = None

    def get_paginated_response(self, data):
        return Response(
            'results', data
        )

使用它:

from rest_framework import viewsets
from .models import MyModel
from .serializers import MySerializer


class CustomApiViewSet(viewsets.ReadOnlyModelViewSet):
    """
    Simple viewset for viewing MyModels (as a list, or individually).
    """
    queryset = MyModel.objects.all()
    serializer_class = MySerializer
    pagination_class = NoCountPagination

注意,这将返回查询集中的所有行。在几乎所有情况下,我认为最好按原样使用PageNumberPagination,或者使用上面的@Florian 解决方案来加快速度。

【讨论】:

【参考方案5】:

除了 getup8 的响应之外,我还能让它在不返回计数但也不返回所有行的情况下工作(假设您在站点的 REST_FRAMEWORK 设置中将 PAGE_SIZE 设置为合理的数字)。

from rest_framework.pagination import LimitOffsetPagination
    
class NoCountPaginator(LimitOffsetPagination):
    def get_count(self, queryset):
        return 99999999

    def get_paginated_response(self, data):
        return Response(OrderedDict([
            ('results', data)
        ]))

并使用它:

from rest_framework import viewsets
from .models import MyModel
from .serializers import MySerializer


class CustomApiViewSet(viewsets.ReadOnlyModelViewSet):
    queryset = MyModel.objects.all()
    serializer_class = MySerializer
    pagination_class = NoCountPaginator

我将 PAGE_SIZE 设置为 25,因此 API 始终只返回前 25 行,并且不再运行计数查询。

【讨论】:

以上是关于Django Rest Framework 分页极慢计数的主要内容,如果未能解决你的问题,请参考以下文章

Django rest framework 之分页

Django-rest-framework多条件查询/分页/多表Json

关闭 Django Rest Framework ModelViewSet 的自动分页

Django Rest Framework:在 ViewSet 上打开分页(如 ModelViewSet 分页)

django-rest-framework框架总结之认证权限限流过滤分页及异常处理

django-rest-framework框架总结之认证权限限流过滤分页及异常处理