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

Django中get_absolute_url访问外键引发重复SQL查询问题

问题描述

我使用基于类的列表视图展示对象链接及字段数据,链接的href由模型中的get_absolute_url方法生成。该方法访问模型的ForeignKey字段,每次调用都会触发数据库查询,导致大量重复SQL查询(数据量增大后问题会更严重)。我已尝试在视图的queryset中使用.select_related(),但无效果。请问如何消除get_absolute_url引发的重复查询?


代码示例

models.py

class LanguageLocale(models.Model):
    """model for representing language locale combinations"""

    class LANG_CODES(models.TextChoices):
        EN = 'en', _('English')
        ES = 'es', _('español')
        QC = 'qc', _("K'iche'")

    lang = models.CharField(max_length=2, choices=LANG_CODES.choices, blank=False)

class Scenario(models.Model):
    """model for representing interpreting practice scenarios"""

    scenario_id = models.CharField(max_length=20, primary_key=True)

    lang_power = models.ForeignKey(LanguageLocale)
    lang_primary_non_power = models.ForeignKey(LanguageLocale)

    class SCENARIO_STATUSES(models.TextChoices):
        PROD = 'PROD', _('Production')
        STGE = 'STGE', _('Staged')
        EXPR = 'EXPR', _('Experimental')

    status = models.CharField(
        max_length=4, choices=SCENARIO_STATUSES.choices, default='EXPR')

    def get_absolute_url(self):
        """Returns the URL to access a detail record for this scenario."""
        return reverse('dialogue-detail', kwargs={
            'lang_power': self.lang_power.lang,
            'lang_primary_non_power': self.lang_primary_non_power.lang,
            'pk': self.scenario_id
            }
        )

views.py

class ScenarioListView(generic.ListView):
    """View class for list of Scenarios"""

    queryset = Scenario.objects.select_related(
        'domain_subdomain', 'lang_power', 'lang_primary_non_power'
    )
    
    demo = Scenario.objects.get(scenario_id='demo')
    prod = Scenario.objects.filter(status='PROD')
    staged = Scenario.objects.filter(status='STGE')
    experimental = Scenario.objects.filter(status='EXPR')
    
    extra_context = {
        'demo': demo,
        'prod': prod,
        'staged': staged,
        'experimental': experimental,
    }

scenario_list.html

{% extends "home.html" %}

{% block content %}
  <h1>Practice Dialogues</h1>
  <section>
    {% if prod %}
      <ul>
        {% for scenario in prod %}
          <li>
            <a href="{{ scenario.get_absolute_url }}">{{  scenario.title }}</a> ({{ 
            scenario.domain_subdomain.domain }})
          </li>
        {% endfor %}
      </ul>
    {% else %}
      <p>There are no practice scenarios. Something went wrong.</p>
    {% endif %}
  </section>
  {% if user.is_staff %}
    <section>
      {% if staged %}
        <article>
          <h2>Staged Dialogues</h2>
          <ul>
            {% for scenario in staged %}
              <li>
                <a href="{{ scenario.get_absolute_url }}">{{  scenario.title }}</a> ({{ 
                scenario.domain_subdomain.domain }})
              </li>
            {% endfor %}
          </ul>
        </article>
      {% endif %}
      {% if experimental %}
        <article>
          <h2>Experimental Dialogues</h2>
          <ul>
            {% for scenario in experimental %}
              <li>
                <a href="{{ scenario.get_absolute_url }}">{{  scenario.title }}</a> ({{ 
                scenario.domain_subdomain.domain }})
              </li>
            {% endfor %}
          </ul>
        </article>
      {% endif %}
    </section>
  {% endif %}
{% endblock %}

重复查询信息(来自Django Debug Toolbar)

重复3次的查询:

SELECT "scenario_languagelocale"."id",
       "scenario_languagelocale"."lang_locale",
       "scenario_languagelocale"."lang"
  FROM "scenario_languagelocale"
 WHERE "scenario_languagelocale"."id" = 1
 LIMIT 21 6 similar queries.  Duplicated 3 times.

重复2次的查询:

SELECT "scenario_languagelocale"."id",
       "scenario_languagelocale"."lang_locale",
       "scenario_languagelocale"."lang"
  FROM "scenario_languagelocale"
 WHERE "scenario_languagelocale"."id" = 3
 LIMIT 21 6 similar queries.  Duplicated 2 times.

解决方案

问题核心是你视图里手动定义的prod、staged、experimental这些查询集,根本没用到配置的select_related——ListView的queryset属性只给默认的object_list用,而你直接创建的这些查询集是独立的,没有预取关联数据,所以遍历它们时,每次调用get_absolute_url都会触发额外的ForeignKey查询。

具体修复步骤:

  1. 移除视图类中直接定义的demo、prod、staged、experimental属性,这些属性会在类加载阶段就执行数据库查询,不仅没用到预取逻辑,还会导致不必要的提前查询。
  2. 重写get_context_data方法,在这个方法里基于带select_related的基础查询集,创建各状态的场景集合:

修改后的views.py代码:

class ScenarioListView(generic.ListView):
    """View class for list of Scenarios"""
    queryset = Scenario.objects.select_related(
        'domain_subdomain', 'lang_power', 'lang_primary_non_power'
    )

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        # 基于预取后的基础查询集生成各状态数据
        base_qs = self.get_queryset()
        context['demo'] = base_qs.get(scenario_id='demo')
        context['prod'] = base_qs.filter(status='PROD')
        context['staged'] = base_qs.filter(status='STGE')
        context['experimental'] = base_qs.filter(status='EXPR')
        return context

额外优化方案:

如果lang字段是固定枚举值,可以在Scenario模型中添加lang_power_code和lang_primary_non_power_code两个CharField,通过重写save方法保持与ForeignKey字段的同步。这样get_absolute_url可以直接读取本地字段,彻底避免关联查询:

class Scenario(models.Model):
    # ... 原有字段 ...
    lang_power_code = models.CharField(max_length=2, choices=LanguageLocale.LANG_CODES.choices)
    lang_primary_non_power_code = models.CharField(max_length=2, choices=LanguageLocale.LANG_CODES.choices)

    def save(self, *args, **kwargs):
        # 保存前同步code字段
        self.lang_power_code = self.lang_power.lang
        self.lang_primary_non_power_code = self.lang_primary_non_power.lang
        super().save(*args, **kwargs)

    def get_absolute_url(self):
        return reverse('dialogue-detail', kwargs={
            'lang_power': self.lang_power_code,
            'lang_primary_non_power': self.lang_primary_non_power_code,
            'pk': self.scenario_id
        })

修改完成后,打开Django Debug Toolbar验证,重复的LanguageLocale查询应该会消失。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.19 23:27:06