※このブログではサーバー運用、技術の検証等の費用のため広告をいれています。
記事が見づらいなどの問題がありましたらContactからお知らせください。


【Django2.2】QuerySetでクエリが実行されるタイミングについて解説

パフォーマンス Django2 python3 Django QuerySet

投稿日:2019年12月3日

このエントリーをはてなブックマークに追加
Djangoでデータベースを操作する際に使うQuerySetは裏でSQLを発行しています。しかし、このクエリが実行されるタイミングが少し複雑。この記事ではDjangoのQuerySetのクエリが実行されるタイミングについて、実例を交えて解説します。

はじめに

この記事について

Djangoでデータベースを扱う際には十中八九QuerySetを使う事になるでしょう。このQuerySetはクエリが実行されるタイミングが少し特殊です。その部分をきちんと理解することできちんと最適化されたWebアプリケーションが作成できます。
この記事ではQuerySetのクエリが実際に実行されるタイミングについて、大量のレコードを使った実例をみながら詳しく解説します。

環境

  • Ubuntu 18.04LTS
  • Python 3.6.3
  • Django2.2.1
  • SQLite

今回はデータベースとしてDjangoのデフォルトのデータベースSQLiteを使用しています。パフォーマンスのテストをする際には本来使用するDB(おそらくMySQLPostgresなど)を使用しなければいけませんが、この記事で試すのはあくまでどこでクエリが実行されるのか確認するだけなので、とりあえずSQLiteで良いでしょう。


テスト環境の準備

はじめに

ここではテストする環境を準備します。

サンプルWebアプリケーションを準備

まずはテストする対象のWebアプリケーションを準備します。プロジェクト名はsample_site、そしてblogというアプリケーションを一つ作成します。
以下の様なディレクトリ構成になります。

ディレクトリの構成
.
├── blog
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── auto_insert.py
│   ├── migrations
│   │   └── __init__.py
│   ├── models.py
│   ├── tests.py
│   └── views.py
├── manage.py
└── sample_site
    ├── __init__.py
    ├── settings.py
    ├── urls.py
    └── wsgi.py
sample_site/sample_site/setings.py
[...]

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'blog',
]


[...]

サンプルとして、ありがちな構成で適当なテーブルを作ってみます。

sample_blog/blog/models.py
from django.db import models


class Post(models.Model):
    title = models.CharField(max_length=123,default="no-title")
    body = models.TextField(default="asfasdfasdfasdfasdfasdf,asdfasdfasfadf\r\nasdfasdfasdfasdf\r\nasdfasdfasdf\r\n")
    created = models.DateField(auto_now_add=True)
    edited = models.DateField(auto_now=True)

データベースに反映させます。

ターミナル
$ python manage.py makemigrations
$ python manage.py migrate

これでとりあえずサンプルのアプリケーションの作成は完了です。

大量のサンプルレコードを生成

つぎに大量のレコードの生成をしてみます。Djangoでのレコードの生成方法はいくつかありますが、大量のサンプルレコードを作成するときにはDjangoシェルのスクリプトファイルを作成してそれを実行するのが良いでしょう。
まずはスクリプトファイルを新しく作成します。

sample_blog/blog/auto_insert_data.py
from blog.models import Post

insert_posts = []

for i in range(100000):
    insert_post = Post()
    insert_posts.append(insert_post)

Post.objects.bulk_create(insert_posts)

実行します。

ターミナル
$ python manage.py shell < blog/auto_insert_data.py

これで10万行のレコードが新しく作成できました。


QuerySet

クエリの実行タイミング

公式ドキュメントによるとQuerySetのクエリは以下のタイミングで実行されます。

  • Iteration:QuerySetをfor文で回したとき、その初回イテレーションで実行されます。
  • Slicing:DjangoのQuerySetはpythonの配列の様にブラケットで要素数を制限できます。[ start_index : end_index ]の様な場合にはクエリは実行されないのですが、[ stat_index : end_index : step]のようにstepが入ったときにはここでクエリが実行されます。
  • Pickling/Caching:Pickle化やキャッシュをしたときなどにはクエリが実行されます。
  • 特定のメソッド:repr()len()list()bool()といったメソッドでQuerySetを評価したときクエリが実行されます。

視覚的に確認してみよう

Djangoのshellを起動してクエリの実行タイミングについて確認してみます。
ここで本来ならコードをスクリプトファイルとして作成しpython manage.py shell < スクリプトファイルのように実行したかったのですが、何故かtimeモジュールのimportが上手く行かず...(知っている方いたら教えてください...)。そのため、少し面倒ですが以下の手順を踏みます。

  1. すぐしたのコードをコピー
  2. Djangoのシェルを起動
  3. コピーしたコードをDjangoのシェルにペースト
  4. 実行

コピーするコード
from blog.models import Post
import time
# 計測開始
start = time.time()
def print_exec_time(sign="0"):
    """
    経過時間を図って表示
    """
    execution_time = time.time() - start
    print("{}:{}".format(sign, execution_time))
# 1:QuerySetの生成
posts = Post.objects.all()
print_exec_time("1")
# 2:QuerySetを分割
posts_splited = posts[0:100]
print_exec_time("2")
# 3:長さを取得
length_query = len(posts_splited)
print_exec_time("3")

Djangoシェルを起動して実行してみます。

ターミナル
$ python manage.py shell
1:0.0006344318389892578
2:0.0011267662048339844
3:2.4477956295013428

1,2までの実行時間は非常に短いのにたいして、3に到達するまでに大きく時間がかかっています。

グラフで表すと明らかですね。

▲それぞれのタイミングの実行時間

QuerySetの生成や単純なSliceではクエリは実行されず、特定のメソッドでの評価のタイミングでクエリが実行されたためそこでの時間がかかっているわけですね。

このエントリーをはてなブックマークに追加


関連記事

記事へのコメント