DjangoのQuerysetは直接データベースを操作することなく、必要なデータを取得できます。
今回はデータベースの最適化をするために、このQuerysetの書き方を工夫する方法をご紹介します。

💁‍♀️

この記事を読む前にQuerysetの基本を理解しておいてね。






    Strory
  1. 前提条件
  2. get()の使用
  3. iterator()の使用
  4. select_related()の使用
  5. prefetch_related()の使用
  6. aggregate()の使用
  7. Foreign Keyの使用
  8. F オブジェクトの使用
  9. 参考にしたサイト
  10. まとめ

前提条件

QuerySetの基本的な理解

get()の使用

取得したいレコードが1つの場合はget()を使用します。
もしfilter()などを使ってしまうとテーブルの全レコードを調べに行くため、速度が遅くなってしまいます。 get()のエラー(exception)はマッチするレコードがなかった場合(DoesNotExist)とレコードが1つ以上ある場合(MultipleObjectsReturned)に検出されます。
このexceptionたちを使ったget()の使い方の例は以下になります。

try:
    entry = Entry.objects.get(id=10)
except Entry.DoesNotExist:
    print("レコードが存在しません。")
except Entry.MultipleObjectsReturened:
    print("マッチするレコードが複数あります。")
else:
    print(entry)

iterator()の使用

querySetは実行された時に自身に結果を保存(cache)します。
そして次に実行された時に、まずその保存先を探してから無ければデータベースに探しにいくようになります。
そのため、下のコードのように全てのレコードを取得してループさせる場合はかなりのロスタイムが発生します。

entries = Entry.objects.all()
for entry in entries:
    ...

iterator()は自身に結果をcacheすることなく、データーベース に直接アクセスしてくれるため、 上記のような場合はiterator()を使うことでロスタイムを削減することができます。

entries = Entry.objects.all().iterator();
for entry in entries:
    ...

select_related()の使用

あるモデル1(テーブル)からOneToOneFieldやForeignKeyで紐づけられたモデル2を参照する時のことを考えてみましょう。
モデルの例として、djangoのデフォルトのモデルUserには誕生日のフィールドがないので、新しく誕生日のフィールドをもつモデルProfileをUSERをForeign Keyとして作成します。

models.py
class Profile(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    barthday = models.DateField(blank=True, null=True)
    def __str__(self):
        return self.user

モデルUserにはユーザー名のデータがあり、ProfileからUserを参照する場合は以下のように書きます。

# DBにアクセスする
profile = Profile.objects.get(id=1)

# またDBにアクセスする
username = profile.user.username

上記の書き方では2回(Profileのデータを取得する時とusernameを取りに行くとき)データベースにアクセスすることになります。

このロスタイムはselect_related()を使用することで改善することができます。
以下のようにselect_related()を使うことでProfileのデータを取得するときにProfileに紐付くUserのデータも一緒に取得して保存してくれるようになります。

# DBにアクセスする  
profile = Profile.objects.select_related('user').get(id=1)

# 保存先から参照する(再度DBにアクセスしない)
username = profile.user.username

select_related()が使えるのは1対1のリレーション関係(OneToOne, ForeignKey)で、
どのall()やfilter()などどのQuerySetにも使えます。

profile = Profile.objects.select_related('user').all()
profile = Profile.objects.select_related('user').filter(...)

prefetch_related()の使用

prefetch_related()はselect_related()と同じような仕組みですが、**1対多のリレーション関係(ManyToMany)に使用することができます。
select_related()とprefetch_related()については別の記事で詳しく書くので、今回は割愛します。

aggregate()

まずは以下のような記事のデータを持ったモデルArticlesから一番Likeがついた記事を取得する時のことを考えてみましょう。

models.py
class Articles(models.Model):
    title = models.CharField('Title', max_length=200)
    body = TexTField('Contents')
        likes = models.DecimalField("Liked", blank=True, null=True, default=0.00)

forループでArticlesの記事(レコード)に一つずつアクセスして、like数が一番多い記事を取得します。

most_liked = 0
most_liked_article=None :
for article in Articles.objects.all();
    if article.likes > most_likes:
        most_liked = article.likes
        most_liked_article = article

ここでaggregate() を使うことによってforループを使用せずに一番Likeのついた記事を取得することができます。

from django.db.models import Max
most_liked_article=Articles.objects.all().aggregate(Max('likes'))

上記の例のようにaggregate() は複数のレコードの平均や最大値、最小値などを集計してくれます。

Foreign Keyの使用

DjangoのORMにはあるモデルからレコードが取得された時に、自動で1対1(OneToOneやForeignKey)のリレーション先のForeign Keyを保存してくれる仕組みがあります。
この保存されたForeign Keyを直接参照することでリレーション先のカラム全体を見なくても済むようになります。

以下のモデルを例にしてみます。

models.py
class Profile(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    barthday = models.DateField(blank=True, null=True)
    def str(self):
        return self.user


Profileに紐付くUserのForeign Keyのみを参照する場合は以下のようになります。
id = Profile.objects.get(id=1).user.id

保存されたForeign Keyを使用すると以下のようにと書き換えることができます。
id = Profile.objects.get(id=1).user_id

ちなみに先ほどのselect_related()と併せて使うとDBへのアクセス回数も減らすことができます。

id = Profile.objects.select_related('user').get(id=1).blog_id

F オブジェクトの使用

F オブジェクトの概念は少し難しいので、ざっくり説明すると
モデルのフィールドを参照して、Pythonのメモリを消費することなく更新できる機能です。

例えばある記事がLikeされたので、合計のLike数を更新する時のことを考えてみます。

article = Articles.objects.get(id=1)
article.likes += 1
article.save()

F オブジェクトを使うと以下のように書き換えることができます。

from django.db.models import F
article = Articles.objects.get(id=1) article.likes = F('likes') + 1 article.save()

F オブジェクトを使うと、データベースレベルで更新を完結できるようにるため、Pythonのメモリーを節約することができます。

参考にしたサイト

Database access optimization

まとめ

QuerySetのパフォーマンス改善方法をいくつかご紹介しました。
色んな機能をざぁーっと説明してたので、一個一個については深く説明できませんでしたが、 「あ、こんな改善方法あるんだ💡」ぐらいに知っていただけると嬉しいです。

ここまで読んでくれてありがとうございました
またね😚