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

Django多模型联合查询性能低下问题及优化咨询

问题现象

使用Django 3.2(后端Postgres、部署于Docker),存在字段结构完全一致的Chant和Sequence两个模型,分别实现了单模型搜索视图,以及通过union合并结果的联合搜索视图。测试数据显示:

  • 30万条Chant + 30万条Sequence:单模型搜索耗时<0.5秒,联合搜索耗时~5秒
  • 30万条Chant + 0条Sequence:单模型搜索耗时<1秒,联合搜索耗时3秒
    无法合并两个模型,需排查性能差异原因并优化联合搜索性能至接近单模型水平。
性能瓶颈原因
  1. UNION的额外开销
    Postgres的UNION操作默认会对两个结果集做去重和排序,即使其中一个表为空,数据库仍会执行完整的UNION逻辑:创建临时表存储两个结果集、进行排序去重,这会带来大量CPU和IO开销。
  2. 无索引的模糊搜索
    std_full_text__icontains对应Postgres的ILIKE '%query%',未加索引时会触发全表扫描。单表扫描已存在性能损耗,UNION还要合并两个全表扫描的结果,进一步放大耗时。
  3. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.30 22:27:23