この記事で書いていること
- ORMを使っていると、知らぬ間に大量のSQLが発行されてる恐れがある
- クエリ数を発行しないための工夫がいくつかあるので、それを紹介する
- 新しい使えるテクニックを見つけたら、随時加筆予定
ORMでは知らぬうちに大量のクエリが発行されているかもしれない
私が触ったことのあるフレームワークはRails、Djangoだけなので、ORマッパーがあるのが当たり前という環境で育ってきました。
そのため、DBにアクセスするためにSQLが発行されていることをあまり意識してきませんでした。(お恥ずかしい限りですが)
これから職業プログラマーとして成長していくためにはそんなんじゃあかんという訳で、絶賛試行錯誤中です。
今回は私が調べたり、先輩から教えてもらったクエリ数を抑える工夫やテクニックを紹介していこうと思います。
クエリ数を観測するには、「django-debug-toolbar」を使う
まず、クエリ数を改善するためには、何のSQLがどれくらい走っているのか計測できる必要があります。
そこで取り入れるのが、「django-debug-toolbar」というパッケージです。
こんな感じのサイドバーが画面に表示されて、直前の処理でどれくらいSQLが走ったのか、見れるようになります。
インストール
公式DOCSにインストールの手順が書かれています。順にコピペしていくだけでオッケーです。
https://django-debug-toolbar.readthedocs.io/en/stable/installation.html
クエリ数を減らすのに役立つテクニック集
では、ここから実際にクエリ数を減らすテクニックを書いていきます。
ループの中でクエリが走る処理を書かない
テクニックというか、心がけ。
ループの中でUser.objects.filter(name="hoge")とか書いてるコードに違和感を持てるようになろう。
select_related()
公式DOCSのここに詳細が書いてあります。
https://docs.djangoproject.com/ja/2.1/ref/models/querysets/#select-related
公式の例をそのまま引っ張ってくると、
# Hits the database.
e = Entry.objects.select_related('blog').get(id=5)
# Doesn't hit the database, because e.blog has been prepopulated
# in the previous query.
b = e.blog
Entryクラスのリレーション先である、Blogクラスのデータもselect_related()が呼ばれたタイミングでDBから引っ張ってきているので、最後の行でe.blog
を実行した時にクエリが走りません。
的なことが書いてあります。
もし、select_related()を実行せず、get()だけを使ってEntryクラスのインスタンスをe
に格納した場合、最後のe.blog
を実行したタイミングでもう一度クエリが走ってしまうことになります。
上記のような簡単な例では、クエリ数にさほど差は出てきませんが、取得したインスタンスのリレーション先のインスタンスをループ処理の中などで取り出す処理を書いていたりしたら、select_relatedの有無による差は絶大になります。
ちなみに、リレーション先が複数ある場合、
e = Entry.objects.select_related('blog', 'something').get(id=5)
みたいに、コンマつなぎで一気に書けます。
bulk_create()
公式DOCSはこちら
https://docs.djangoproject.com/en/2.1/ref/models/querysets/#bulk-create
何かデータをcreateする時は、当然ながらクエリが走ります。
1件ずつ逐一createしていると、登録件数が増えるほど、クエリ数が増えてしまいます。
ある程度まとまった量のデータをまるっと処理することも当然よくあるので、それでは困ってしまいます。
Djangoはbulk_createというやつを用意してくれています。
以下、公式記載の例
>>> Entry.objects.bulk_create([
... Entry(headline='This is a test'),
... Entry(headline='This is only a test'),
... ])
このように、インスタンスを要素にした配列を引数にとって、1クエリで全てのレコードを作成することができます。
バッチ処理とも相性が良く、公式にはその例も載っているので、一目見ておくと良いと思います。
bulk_update()
複数のレコードを更新したい時に便利なパッケージ。
何かのレコードを変更し、save()で保存する時、UPDATEというクエリが走ります。
変更レコードが1件増えるごとに1クエリ増えていってしまいます。
そこで、bulk_update
を使うと、変更したいインスタンスを配列に格納しておけば、1つのクエリで全て更新できるようになります。
使用イメージとしては、こんな感じ。
from django_bulk_update.helper import bulk_update
updates = []
for instance in instances:
instance.hoge = "fuga"
updates.append(instance)
bulk_update(updates, update_fields=['hoge'])
インストール
Djangoのbuilt-inではなく、パッケージをインストールして使います。
詳細は公式のGithubを見てください。
https://github.com/aykut/django-bulk-update
こちらサイトも参考にさせてもらいました。調べてると良くお見かけするサイトさんです。いつもお世話になっております。
https://tokibito.hatenablog.com/entry/20160128/1453914673