select_related・prefetch_related

2018-11-05 / PythonDjangoDjangoライブラリ

概要

DjangoのORMは大変便利ですが、データが増えたり、リレーションを多用していると、処理に少し時間が掛かるようになってきます。 今回はQeurysetのメソッドであるselect_related()prefetch_related()を使って改善していきます。

django-debug-toolbarの導入

その前に、効率の悪いSQLを発見するためのツールとして、django-debug-toolbarを導入しましょう。これを使うと、どのようなSQLが発行されたかを簡単に確認できます。

pipでインストールをします。

pip install django-debug-toolbar

settings.pyの下にでも、下記を追加します。

if DEBUG:
    INTERNAL_IPS = ['127.0.0.1']
    INSTALLED_APPS += ['debug_toolbar']
    MIDDLEWARE += ['debug_toolbar.middleware.DebugToolbarMiddleware']
    DEBUG_TOOLBAR_PANELS = [
        'debug_toolbar.panels.versions.VersionsPanel',
        'debug_toolbar.panels.timer.TimerPanel',
        'debug_toolbar.panels.settings.SettingsPanel',
        'debug_toolbar.panels.headers.HeadersPanel',
        'debug_toolbar.panels.request.RequestPanel',
        'debug_toolbar.panels.sql.SQLPanel',
        'debug_toolbar.panels.staticfiles.StaticFilesPanel',
        'debug_toolbar.panels.templates.TemplatesPanel',
        'debug_toolbar.panels.cache.CachePanel',
        'debug_toolbar.panels.signals.SignalsPanel',
        'debug_toolbar.panels.logging.LoggingPanel',
        'debug_toolbar.panels.redirects.RedirectsPanel',
    ]

プロジェクトのurls.pyも、以下のような感じにしておきます。

from django.conf import settings
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('blog.urls')),  # blogアプリケーションをこれから作る
]

if settings.DEBUG:
    import debug_toolbar
    urlpatterns = [path('__debug__/', include(debug_toolbar.urls))] + urlpatterns

django-debug-toolbarの準備はこれだけです。簡単ですね。

今回使うモデル

シンプルなモデルを作成します。記事と、カテゴリと、タグです。カテゴリは1記事につき1つ指定し、タグはいくつでも指定ができます。

from django.db import models


class Category(models.Model):
    """カテゴリー"""
    name = models.CharField('カテゴリ名', max_length=255)

    def __str__(self):
        return self.name


class Tag(models.Model):
    """タグ"""
    name = models.CharField('タグ名', max_length=255)

    def __str__(self):
        return self.name


class Post(models.Model):
    """記事"""
    title = models.CharField('タイトル', max_length=255)
    category = models.ForeignKey(Category, verbose_name='カテゴリ', on_delete=models.PROTECT)
    tag = models.ManyToManyField(Tag, verbose_name='タグ')

    def __str__(self):
        return self.title

今回のビュー

今のところはシンプルなListViewです。後程、このビューは改良することになります。

from django.views import generic
from .models import Post


class PostIndex(generic.ListView):
    model = Post

記事の一覧テンプレート

post_list.htmlを作り、次のような中身にしておきます。各記事は、記事名、カテゴリ、タグを表示します。

{% extends 'blog/base.html' %}

{% block content %}
    <h1>記事リスト</h1>
    {% for post in post_list %}
        <p">
            記事タイトル: {{ post.title }}<br>
            カテゴリ: {{ post.category }}<br>
            タグ: {% for tag in post.tag.all %}{{ tag }},{% endfor %}
        </p>
        <hr>
    {% endfor %}
{% endblock %}

SQLを確認する

既にdjango-debug-toolbarは導入していますので、127.0.0.1:8000 にアクセスするだけです。

ページの右側にツールバーがありますね。これがdjango-debug-toolbarが作成したものです。

「SQL」という項目があるのでクリックすると、発行されたSQLが確認できます。

左側の「+」を押すと、どこで発行されたかが見えますし...

右側の「sel」を押すと、実際に取得されたデータが確認できます。  

それでは本題に戻ります。 SQLですが、セッションやログインユーザー等Djangoデフォルトの操作を除けば、発行されたSQLは以下です。多いですね。最初の1行目は全ての記事を取得しているとわかりますが、それ以降はどこで呼び出されているのでしょうか。

SELECT ••• FROM "blog_post"
SELECT ••• FROM "blog_category" WHERE "blog_category"."id" = '1'
SELECT ••• FROM "blog_tag" INNER JOIN "blog_post_tag" ON ("blog_tag"."id" = "blog_post_tag"."tag_id") WHERE "blog_post_tag"."post_id" = '1'
SELECT ••• FROM "blog_category" WHERE "blog_category"."id" = '1'
SELECT ••• FROM "blog_tag" INNER JOIN "blog_post_tag" ON ("blog_tag"."id" = "blog_post_tag"."tag_id") WHERE "blog_post_tag"."post_id" = '2'
SELECT ••• FROM "blog_category" WHERE "blog_category"."id" = '1'
SELECT ••• FROM "blog_tag" INNER JOIN "blog_post_tag" ON ("blog_tag"."id" = "blog_post_tag"."tag_id") WHERE "blog_post_tag"."post_id" = '3'
SELECT ••• FROM "blog_category" WHERE "blog_category"."id" = '1'
SELECT ••• FROM "blog_tag" INNER JOIN "blog_post_tag" ON ("blog_tag"."id" = "blog_post_tag"."tag_id") WHERE "blog_post_tag"."post_id" = '4'
SELECT ••• FROM "blog_category" WHERE "blog_category"."id" = '1'
SELECT ••• FROM "blog_tag" INNER JOIN "blog_post_tag" ON ("blog_tag"."id" = "blog_post_tag"."tag_id") WHERE "blog_post_tag"."post_id" = '5'

テンプレートにてpost.categorypost.tag.allとしていましたが、実はここで毎回SQLが発行されています。この挙動は覚えておきましょう。

            カテゴリ: {{ post.category }}<br>
            タグ: {% for tag in post.tag.all %}{{ tag }},{% endfor %}

ユーザーのアクセス数が増えたり、格納しているデータが多くなってくると、これは問題になる可能性があります。

改善する

問題点もわかったので改善しましょう。views.pyを書き換えます。

from django.views import generic
from .models import Post


class PostIndex(generic.ListView):
    model = Post
    queryset = Post.objects.select_related('category').prefetch_related('tag')

queryset属性を上書きし、select_relatedprefetch_relatedを使います。今回のリレーションの関係ならば、カテゴリはselect_relatedで、タグはprefetch_relatedが使えます。

queryset = Post.objects.select_related('category').prefetch_related('tag')

発行されたSQLを確認してみると、よく動作しているようです。blogアプリケーション内で発行されたのは、下の2つだけですね。

select_relatedを使うと、記事と一緒にカテゴリ情報も一緒に取得されるようになっています。

prefetch_relatedは、各記事に対応するタグを全て取得しています。

今回はあくまで一例で、思いもよらぬ場所で時間がかかっていることはよくあります。 私の旧ブログの場合は右側に最新のコメント10件が表示されており、テンプレートにて

href="{% url 'blog:detail' comment.post.pk %}#comment-area"

のようにして、クリックでその記事のコメント欄に遷移する作りでしたが、urlタグ内のcomment.post で毎回SQLが発行されていました。 django-debug-toolbarを使うと問題箇所も察しがつくので、ぜひ有効活用していきたいですね。

この記事の関連記事

関連記事はありません。

コメント欄

記事にコメントする

まだコメントはありません。