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

Django分页查询重复执行耗时翻倍,如何优化性能?

优化Django分页时重复查询的性能问题

这个问题我之前做项目时也踩过坑!Django的Paginator默认逻辑确实会这样:为了统计总数时更快,它会自动去掉ORDER BY子句执行一次COUNT(*),然后再带ORDER BY执行一次切片查询获取当前页数据。当你的查询过滤条件复杂或者数据量很大时,两次查询的开销就会被放大,导致性能翻倍。下面给你几个实用的优化思路:

1. 给查询和排序字段建立合适的索引

这是最基础也最有效的优化手段。数据库索引能让过滤和排序操作跳过全表扫描,直接定位数据。

针对你的示例代码,建议添加复合索引和针对性索引:

class Medium(models.Model):
    title = models.CharField(verbose_name=_('title'), max_length=256, null=False, blank=False, db_index=True, )
    offered_by = models.ForeignKey(Institution, verbose_name=_('Offered by'), on_delete=models.CASCADE, )
    quantity = models.IntegerField(verbose_name=_('Quantity'), validators=[MinValueValidator(0)], null=False, blank=False, )
    deleted = models.BooleanField(verbose_name=_('Deleted'), default=False, )

    class Meta:
        indexes = [
            # 覆盖过滤条件+排序字段的复合索引,让过滤和排序都能命中索引
            models.Index(fields=['deleted', 'quantity', 'title']),
            # 针对exclude的offered_by_id单独建索引,加速排除操作
            models.Index(fields=['offered_by_id']),
        ]

如果你的项目用PostgreSQL,对于title__icontains这种模糊查询,普通BTREE索引可能派不上用场,可以改用GIN索引配合Unaccent来加速:

from django.contrib.postgres.indexes import GinIndex
from django.contrib.postgres.search import Unaccent

class Medium(models.Model):
    # ... 其他字段不变
    class Meta:
        indexes = [
            GinIndex(fields=[Unaccent('title')]),
        ]

# 查询时也用Unaccent匹配
media = media.filter(title__icontains=Unaccent("funktion"))

2. 自定义分页器缓存Count结果

如果你的数据不会在请求过程中频繁更新,可以把COUNT(*)的结果缓存起来,避免重复计算。自定义一个分页器子类:

from django.core.cache import cache
from django.core.paginator import Paginator
from django.utils.functional import cached_property

class OptimizedPaginator(Paginator):
    @cached_property
    def count(self):
        # 基于QuerySet的SQL语句生成唯一缓存键
        query_str = self.object_list.query.__str__()
        cache_key = f"medium_list_count_{hash(query_str)}"
        
        # 先从缓存取,取不到再计算并存缓存
        count = cache.get(cache_key)
        if count is None:
            count = super().count
            # 缓存5分钟,根据业务调整过期时间
            cache.set(cache_key, count, 300)
        return count

然后在视图中替换默认分页器:

def index3(request):
    media = Medium.objects.filter(deleted=False, quantity__gte=0)
    media = media.exclude(offered_by_id=request.user.institution_id)
    media = media.filter(title__icontains="funktion")
    media = media.order_by('title')
    
    # 使用自定义优化分页器
    paginator = OptimizedPaginator(media, 25)
    media = paginator.page(1)
    
    return render(request, 'media/empty2.html', {'media': media})

3. 使用近似计数(适合对准确性要求不高的场景)

如果你的业务允许总数有轻微误差,可以用数据库的近似计数功能,比如PostgreSQL的reltuples:

from django.db import connection

class ApproximateCountPaginator(Paginator):
    @cached_property
    def count(self):
        # 直接查询PostgreSQL的系统表获取近似行数
        cursor = connection.cursor()
        cursor.execute("SELECT reltuples FROM pg_class WHERE relname = %s", [Medium._meta.db_table])
        return int(cursor.fetchone()[0])

这种方式速度极快,但结果是近似值,适合后台统计、列表概览等场景。

4. 分析查询执行计划

用Django的QuerySet.explain()查看两次查询的执行计划,确认索引是否被正确使用:

# 查看count查询的执行计划
print(media.count().explain())
# 查看切片查询的执行计划
print(media[:25].explain())

根据执行计划调整索引或者查询条件,比如避免无法命中索引的过滤操作。

内容的提问来源于stack exchange,提问作者NKay

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.12 05:38:11