Django多模型联合查询性能低下问题及优化咨询
问题现象
使用Django 3.2(后端Postgres、部署于Docker),存在字段结构完全一致的Chant和Sequence两个模型,分别实现了单模型搜索视图,以及通过union合并结果的联合搜索视图。测试数据显示:
- 30万条Chant + 30万条Sequence:单模型搜索耗时<0.5秒,联合搜索耗时~5秒
- 30万条Chant + 0条Sequence:单模型搜索耗时<1秒,联合搜索耗时3秒
无法合并两个模型,需排查性能差异原因并优化联合搜索性能至接近单模型水平。
性能瓶颈原因
- UNION的额外开销
Postgres的UNION操作默认会对两个结果集做去重和排序,即使其中一个表为空,数据库仍会执行完整的UNION逻辑:创建临时表存储两个结果集、进行排序去重,这会带来大量CPU和IO开销。 - 无索引的模糊搜索
std_full_text__icontains对应Postgres的ILIKE '%query%',未加索引时会触发全表扫描。单表扫描已存在性能损耗,UNION还要合并两个全表扫描的结果,进一步放大耗时。 - Django分页的低效处理
Django的Paginator会先执行完整的QuerySet获取总记录数,再做分页。对于UNION后的结果集,这意味着先拉取所有符合条件的记录(最多60万条),再在内存中分页,远慢于单模型搜索时直接通过LIMIT/OFFSET的数据库级分页。
优化方案
1. 给模糊搜索加Trigram索引
Postgres的pg_trgm扩展支持对模糊匹配(LIKE/ILIKE '%...%')创建高效的GIN/GIST索引,消除全表扫描:
- 第一步:在Postgres中启用扩展
CREATE EXTENSION pg_trgm; - 第二步:修改模型添加索引
from django.db import models from django.contrib.postgres.indexes import GinIndex class Chant(models.Model): std_full_text = models.TextField(blank=True, null=True) class Meta: indexes = [ GinIndex(fields=['std_full_text'], name='chant_std_full_text_gin'), ] class Sequence(models.Model): std_full_text = models.TextField(blank=True, null=True) class Meta: indexes = [ GinIndex(fields=['std_full_text'], name='sequence_std_full_text_gin'), ] - 第三步:执行迁移
python manage.py makemigrations && python manage.py migrate
2. 用UNION ALL替代UNION
如果两个模型的记录不存在重复(无需去重),使用UNION ALL代替UNION——它不会做去重和排序,性能提升显著。修改联合搜索视图的get_queryset方法:
def get_queryset(self): query = self.request.GET.get('q') if query: chant_results = Chant.objects.filter(std_full_text__icontains=query) sequence_results = Sequence.objects.filter(std_full_text__icontains=query) # 添加all=True参数使用UNION ALL return chant_results.union(sequence_results, all=True) else: return Chant.objects.union(Sequence.objects.all(), all=True)
3. 优化分页逻辑,避免全量查询
放弃Django默认的Paginator对UNION结果的内存分页,改用原生SQL实现数据库级分页:
from django.db import connections def get_queryset(self): query = self.request.GET.get('q', '') page = int(self.request.GET.get('page', 1)) per_page = self.paginate_by offset = (page - 1) * per_page instances = [] with connections['default'].cursor() as cursor: if query: sql = """ SELECT id, std_full_text, 'chant' AS source FROM chant WHERE std_full_text ILIKE %s UNION ALL SELECT id, std_full_text, 'sequence' AS source FROM sequence WHERE std_full_text ILIKE %s LIMIT %s OFFSET %s """ cursor.execute(sql, [f'%{query}%', f'%{query}%', per_page, offset]) else: sql = """ SELECT id, std_full_text, 'chant' AS source FROM chant UNION ALL SELECT id, std_full_text, 'sequence' AS source FROM sequence LIMIT %s OFFSET %s """ cursor.execute(sql, [per_page, offset]) # 将查询结果转为模型实例 for row in cursor.fetchall(): if row[2] == 'chant': instances.append(Chant(id=row[0], std_full_text=row[1])) else: instances.append(Sequence(id=row[0], std_full_text=row[1])) return instances
这种方式直接在数据库层面完成分页,避免拉取全量数据,大幅降低内存占用和耗时。
4. 进阶:改用Postgres全文搜索
如果模糊匹配的需求可以兼容全文搜索逻辑,使用Django的Postgres全文搜索功能,性能比icontains更优:
- 模型添加全文搜索向量字段及索引:
from django.db import models from django.contrib.postgres.indexes import GinIndex from django.contrib.postgres.search import SearchVectorField class Chant(models.Model): std_full_text = models.TextField(blank=True, null=True) search_vector = SearchVectorField(null=True) class Meta: indexes = [ GinIndex(fields=['search_vector'], name='chant_search_vector_gin'), ] class Sequence(models.Model): std_full_text = models.TextField(blank=True, null=True) search_vector = SearchVectorField(null=True) class Meta: indexes = [ GinIndex(fields=['search_vector'], name='sequence_search_vector_gin'), ] - 视图中使用全文搜索:
from django.contrib.postgres.search import SearchVector, SearchQuery def get_queryset(self): query = self.request.GET.get('q') if query: search_query = SearchQuery(query) chant_results = Chant.objects.filter(search_vector=search_query) sequence_results = Sequence.objects.filter(search_vector=search_query) return chant_results.union(sequence_results, all=True) else: return Chant.objects.union(Sequence.objects.all(), all=True) - 注意:需要通过信号或Postgres触发器定期更新
search_vector字段,确保与std_full_text同步。
内容的提问来源于stack exchange,提问作者Jacob dGM
相关产品推荐
相关产品推荐

