Djangoでprefetch_relatedを使ってクエリ数を減らす

prefetch_relatedはDjango 1.4で追加された機能です。
親子関係を表すモデル(多対多になってるものなど)をツリー状に表示する場合、ループ内でクエリを実行しってしまうと、クエリ数が多くて極端に遅くなります(特に2段目とか3段目)。
prefetch_relatedを使うと、事前にリレーション先のデータを取得しておき、ループ内で新たにクエリが実行されないようにできます。
試したバージョンは、Python2.7、Django1.6.5です。

ソースコード

完全なコードはgithubに置いてます。
sample_nullpobug/django/market at main · tokibito/sample_nullpobug · GitHub

shop/models.py
# coding: utf-8
from django.db import models

class Category(models.Model):
    "カテゴリ"
    name = models.CharField(max_length=40)

    def __unicode__(self):
        return self.name

    class Meta:
        db_table = 'category'

class Item(models.Model):
    "商品"
    name = models.CharField(max_length=40)
    code = models.CharField(max_length=10, unique=True)
    price = models.IntegerField()
    category = models.ForeignKey('Category')

    def __unicode__(self):
        return self.name

    class Meta:
        db_table = 'item'

class Bundle(models.Model):
    "まとめ売り"
    name = models.CharField(max_length=40)
    price = models.IntegerField()
    items = models.ManyToManyField('Item', through='BundleItem')

    def __unicode__(self):
        return self.name

    class Meta:
        db_table = 'bundle'

class BundleItem(models.Model):
    "まとめ売り商品"
    bundle = models.ForeignKey('Bundle')
    item = models.ForeignKey('Item')

    class Meta:
        db_table = 'bundle_item'

django-extensionsのgraph_modelsコマンドで出力すると、こういう構造になっています。

shop/templates/index.html

テンプレートファイルは、どちらのビュー関数でも同じものを使います。

<html>
<body>
<ul>
{% for bundle in bundles %}
  <li>{{ bundle.name }}
    <ul>
    {% for item in bundle.items.all %}
      <li>{{ item }} - {{ item.category }}</li>
    {% endfor %}
    </ul>
  </li>
{% endfor %}
</ul>
</body>
</html>

bundle、itemのところで2重のforループになっています。また2つ目のほうは、bundleからitemを、itemからcategoryを取得して使うようなテンプレートになっています。

shop/views.py

prefetch_relatedを使わない場合と使う場合のビュー関数をそれぞれ用意して、urls.pyに設定しておきます。

# coding: utf-8
from django.shortcuts import render
from shop.models import Category, Item, Bundle


def no_prefetch_view(request):
    # prefetch_relatedを使わない場合
    bundles = Bundle.objects.all()
    return render(request, 'index.html', {'bundles': bundles})


def prefetch_view(request):
    # prefetch_relatedを使う場合
    bundles = Bundle.objects.prefetch_related('items__category')
    return render(request, 'index.html', {'bundles': bundles})

prefetch_relatedを使う場合、今回はテンプレート中でbundle→item→categoryの順でツリー状に表示するため、「items__category」までをプリフェッチ対象に指定しています。

実行結果

URLにアクセスすると、どちらも同じ表示になります。

prefetch_relatedを使わない場合

debug-toolbarでSQLをみてみると、クエリ数は9回になっています。
テンプレート中の「bundle.items.all」の部分と、「item.category」の部分でクエリが実行されているためです。

prefetch_relatedを使う場合

debug-toolbarでSQLをみてみると、クエリ数は4回になっています。
bundle_idをINで指定してitemテーブル、category_idをINで指定してcategoryテーブルのデータをまとめて取得しています。

このようにprefetch_relatedを使うことで、いくらかクエリ数を減らせます。