【Django】データベース操作:テーブルを結合する方法(select_related、prefetch_related)

2021.10.16 /

【Django】データベース操作:テーブルを結合する方法(select_related、prefetch_related)

本記事ではDjangoのデータベース操作における、テーブル結合する方法について解説していきます。

Djangoではクエリを実行してデータベース操作を行います。

クエリ実行時にテーブルを結合するかしないかで、結果は変わりませんがWebアプリケーションの処理速度は大きく変わります。

ぜひ本記事を処理速度が速いWebアプリケーション開発の参考にしてください。

テーブル結合

テーブル結合とは

データベース内のすべてのデータは「テーブル」に保存されています。
どのWebアプリケーションもこのテーブルから目的のデータを抽出して利用します。

データベースについてより詳しく知りたい方は以下記事をご参照ください。

例えばAとB、2つのテーブルがあるとします。両方からそれぞれデータを取得したい場合、各テーブルに対して別々にアクセスしてデータを取ることもできます。

また2つのテーブル同士が外部キーで紐づいている場合、2つのテーブルを結合させ、そこから目的のデータを抽出することもできます。

つまり複数のテーブルを1つにすることをテーブル結合と言います。SQLではJOINを利用します。

テーブル結合を行う理由

Django ORMはSQLを意識せずにデータベース操作ができるというメリットがあります。

しかしその反面、実行するクエリがボトルネックになり、Djangoアプリケーションの応答速度が遅くなる傾向があります。

つまり意識せずに実行したクエリが大量のSQLを発行し、それが原因で深刻なパフォーマンス低下に繋がるのです。

Djangoの応答速度が遅いと感じたら、まずはクエリを疑ってみてください。そしてこの後に解説する、テーブル結合(select_related)を使って処理速度の向上を図ってください。

テーブル結合を行うことによって、発行されるSQL文を大幅に削減することができます。

テーブル結合を行わない場合のSQL

テーブル結合を行わないでテーブルからデータを取得した場合のSQL発行について見てみましょう。

ここでは次に示すモデルPersonとBookを利用します。

class Person(models.Model):
    first_name = models.CharField(max_length=50)
    last_name = models.CharField(max_length=50)
    age = models.PositiveIntegerField()

    def __str__(self):
        return self.first_name

class Book(models.Model):
    name = models.CharField(max_length=255)
    author = models.ForeignKey(Person, on_delete=models.CASCADE)

    def __str__(self):
        return self.name

モデルBookからauthorを取得すると以下のように2回SQL文を発行し、データベースへのアクセスを行います。

book = Book.objects.get(pk=4)
# 1回目のSQLを発行
author = book.author
# 2回目のSQLを発行

では次の場合は何回SQLを発行するでしょうか。

books = Book.objects.all()[5]
for k in books:
    print(k.name + ":" + k.author.first_name)

上記の場合、実は6回もSQL文を発行します。
これはPersonテーブルのfirst_nameフィールドの値を取得するために、print()のたびにSQL文を発行するからです。

これが1000件のデータであれば、1000件のSQLが発行されデータベースにアクセスするということです。

これでは処理速度が遅くなるのも無理はないですね。

Djangoでテーブル結合する方法

Djangoでデータベース操作を行うにはORMという技法を利用します。

そしてクエリを実行してデータ取得を行います。このクエリにテーブル結合をするメソッドを利用することでテーブル結合を実行します。

Djangoにおけるデータベース操作の基本は以下記事をご参照ください。

クエリでテーブル結合を行う方法として以下メソッドがあります。

  • select_related()メソッド
  • prefetch_related()メソッド

それぞれのテーブル結合で利用するメソッドについて見ていきましょう。

select_related()メソッド

2つのテーブルを結合するためにselect_related()メソッドを利用する方法があります。

構文

select_related(フィールド名)

テーブル同士はForeignKeyまたはOneToOneFieldで紐づいている必要があります。
つまり対一の関係である必要があるということです。

select_related()メソッドの引数には関係を持つフィールドを指定します。
引数を指定しなかった場合、null=Falseの外部キーが対象になりますが、通常は引数を指定しましょう。

クエリを実行してテーブル結合したテーブルからデータを取得した場合、外部キーのデータも一緒に取得します。

そのためテーブル結合を行わない場合と比べて非常に少ない、1回のSQL発行で目的のデータを取り出せます。

book = Book.objects.select_related('author').get(pk=4)
# 1回目のSQLを発行
author = book.author
# 前のクエリでbook.authorのデータは保持しているのでSQLは発行しません

また次の場合でも1回のSQL発行で済みます。

books = Book.objects.select_related('author').all()[5]
for k in books:
    print(k.name + “:” + k.author.first_name)

このようにテーブル結合を行うとデータベースへのアクセス回数が減り、処理速度が格段に速くなります。

重要なポイント
  • select_related()の引数にはテーブル同士で関係を持つフィールドを指定する
  • テーブル同士はForeignKeyまたはOneToOneFieldで紐づいている
  • 引数は特別なことがない限り指定する

prefetch_related()メソッド

select_related()メソッドはテーブル同士が対一の関係のときに利用できます。つまりForeignKeyやOneToOneFieldでのみ使用できるということです。

それ以外の場合(many-to-many、many-to-one、逆参照)ではprefetch_related()メソッドを利用します。

prefetch_related()は対多のときでもテーブル結合ができます。
なぜならSQLで結合するわけではなく、Pythonコードでテーブル結合を行うからです。

今回のモデルでいうと、PersonからBookを逆参照すると関係は対多です。
これは一人の作者が複数の本を作成している可能性があるためです。

Person.objects.prefetch_related('book_set').get(pk=4)

まとめ

本記事「【Django】データベース操作:テーブルを結合する方法(select_related、prefetch_related)」はいかがでしたか。

データベース操作時に複数のテーブルがターゲットの場合、テーブル結合を利用するようにしましょう。

select_related()やprefetch_related()を使うことで、無駄なSQLを発行せずに済みます。

できるだけ処理速度が速いWebアプリケーションの開発を心がけてみてください。