10
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

select_related, prefetch_relatedについて

Posted at

select_related、prefetch_relatedについて、自分の理解を記載します。
間違っていたらすみません。

大まかな解釈

まず、Djangoでリレーションフィールドのデータを取得するときはN+1を避けるため、
対1(順参照)のデータを取得する場合はselect_related
対多(逆参照)のデータを取得する場合はprefetch_related
をそれぞれ使用する。という覚え方で、基本的には良いと思っています。
(要件によってはそうでない場合もあるかもしれませんが、、、)

なぜそうなのかを考えていきます。

動作の説明

公式URL
select_related
prefetch_related

select_related は、SQL の結合を生成して SELECT 句に関連オブジェクトを含むことでクエリ発行を抑制します。このため、select_related は同一のデータベースクエリ内で関連オブジェクトを取得します。しかしながら、'多量の' リレーションシップを結合することで非常に大きな結果になってしまうのを防ぐため、select_related は単一値のリレーションシップ - つまり外部キーと一対一 - のみに制限されています。
prefetch_related はこれとは異なり、各リレーションシップに対して別々に検索を行い、Python で '結合’ を行います。これにより、select_related で可能な外部キーおよび一対一のリレーションシップだけでなく、多対多および多対一のオブジェクトも事前に読み込んでおけるようになります。また、GenericRelation や GenericForeignKey の事前読み込みも可能となりますが、使用できるのは結果が同種の場合に限ります。例えば、GenericForeignKey が参照するオブジェクトの事前読み込みは、クエリが 1 つの ContentType に制限されている場合のみ可能です。

僕なりに要約すると

  • select_related
    結合先のテーブルを一つのSQL内でjoin句を使用することで、結合元のデータと一緒に取得します。
  • prefetch_related
    結合先のテーブルを、結合元のデータのidを利用したSQLをもう一つ発行し、Pythonで結合することで、データを取得します。

といった感じですが
よくわからん

動作を見ていく

例えば、下記のようなテーブル構造があった場合に

class ProductCategory(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    name = models.CharField(max_length=30)

class Product(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    name = models.CharField(max_length=255)
    category = models.ForeignKey(ProductCategory, on_delete=models.PROTECT)

順参照(対1)の場合で考える

select_related

Productからみたselect_relatedのクエリセットを書くと

qs = Product.objects.select_related("category")

のようになり、発行されるSQLは下記のようになります。

select
	"product"."id",
	"product"."name",
	"product"."category_id",
	"product_category"."id",
	"product_category"."name"
from
	"product"
inner join "product_category" on
	("product"."category_id" = "product_category"."id"); args=()

このようにSQLの中でjoinを行って、結合するのが、select_relatedです。

prefetch_related

これをprefetch_relatedで書くと

qs = Product.objects.prefetch_related("category")

となり、SQLは

select
	"product"."id",
	"product"."name",
	"product"."category_id"
from
	"product"; args=()

select
	"product_category"."id",
	"product_category"."name"
from
	"product_category"
where
	"product_category"."id" in (ここは上のSQLで取得したデータのすべてのcategory_idが入ります)

と2つのSQLが発行されます。
2つのSQLを発行してそれぞれのデータを取得した後で、Django君がPythonで紐づけを行って結合してくれるのが、prefetch_relatedです。

つまり

順参照(対1)の場合は、どちらを使用しても、リレーションフィールドへのアクセスの仕方、は変わらないため、コード上での違いは特にありません。
上記の場合だと、0番目の商品のカテゴリーの名前を名前をとろうと思ったら、

qs[0].category.name

で取れると思います。
prefetchのほうもこれで取れるのはDjango君が裏でうまいことやってくれているからです。

どちらも同じように取得できますが、prefetchで行うと

  • SQLの発行回数が1回多くなる
  • データベースから取得した後に、Djangoが紐づけ処理を行う必要がある

と無駄なことが増えてしまうため、順参照(対1)の場合はselect_relatedが適している。

というのが僕の解釈です。

逆参照(対多)の場合を考える

prefetch_related

ProductCategoryからみたprefetch_relatedを書くと、今回はrelated_nameを記載していない、ForeignKeyフィールドに対してなので

qs = ProductCategory.objects.prefetch_related("product_set")

となり、SQLは

select
	"product_category"."id",
	"product_category"."name"
from
	"product_category"; args=()

select
	"product"."id",
	"product"."name",
	"product"."category_id"
from
	"product"
where
	"product"."category_id" in (ここは上のSQLで取得したデータのすべてのcategory_idが入ります)

の2つが発行されます。

0番目product_categoryの、0番目のproductのnameの取り方は下記です

qs[0].product_set.all()[0].name

(普段は逆参照のため、紐づきがないことを考えて、if等を挟むのをお勧めします。)

select_related

ProductCategoryからみたselect_relatedのクエリセットを書くと

qs = ProductCategory.objects.select_related("product")

となりそうですが、こちらはエラーになります。

qs = ProductCategory.objects.select_related("product_set")

と書いてもエラーになります。

理由はdjangoの公式に書かれていたように、select_relatedは順参照もしくは対1の場合のみ使用できるように制限されているためです。

そのためもしも下記のようにProductテーブルが、ProductCategoryに対して、unique=Trueになった場合は

class Product(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    name = models.CharField(max_length=255)
    category = models.ForeignKey(ProductCategory, on_delete=models.PROTECT, unique=True)

先ほどのselect_relatedがエラーにならず、SQLが発行されます。

select
	"product_category"."id",
	"product_category"."name",
	"product"."id",
	"product"."name",
	"product"."category_id"
from
	"product_category"
left outer join "product" on
	("product_category"."id" = "product"."category_id"); args=()

left joinになる理由は、逆参照のため、product_detailが存在しない場合があるので、left joinになってます。

ただ、joinしたSQLを発行することはできましたが、こちらの値を取得することはできません。

qs[0].product_name
qs[0].product.name

これらはエラーになります

qs[0].product_set.all()[0].name

こちらは新しいSQLが発行されN+1になります。

よって逆参照の際は、select_relatedは意味を成しません
(たぶん(ここ一番自信ないです))

なぜ、逆参照(対多)の時は、prefetch_relatedなのか

いろいろ書くと長くなってめんどくさいので、僕が考える理由の結論だけ書くと、
仮に、逆参照(対多)の時にselct_relatedを使ってもエラーにならず正しく取得できたときに、親となるメインテーブルのデータが重複しまくるためだと思っています。

重複しても一緒に出したいのであれば、メインテーブルを逆にして、順参照でselect_relatedを使えばいい話なので、
逆参照の時にselect_relatedは意味をなさないのかなと思いました。

filterで逆参照先のデータを使用したいときは、prefetch_relatedは関係ないですしおすし

結論

順参照の時は、select_related, prefetch_relatedどちらも使用できるが、inner joinで取得したほうが、SQLの発行数と内部の処理が少なくすむため、select_relatedを使用する。
逆参照の時は、prefetch_relatedでないと、そもそもエラーになる、もしくはデータが取得できないため、prefetch_relatedを使用する。

10
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?