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
相关产品推荐
相关产品推荐

