web-dev-qa-db-ja.com

Django ORMのselect_relatedとprefetch_relatedの違いは何ですか?

Djangoのドキュメントでは、

select_related() は、外部キーの関係に「従い」、クエリの実行時に追加の関連オブジェクトデータを選択します。

prefetch_related() はそれぞれの関係について別々の検索を行い、Pythonでは「結合」を行います。

「Pythonで参加する」とはどういう意味ですか?誰かが例で説明できますか?

私の理解するところでは、外部キーの関係にはselect_relatedを使います。 M2Mの関係では、prefetch_relatedを使用してください。これは正しいです?

208
NeoWang

あなたの理解はほとんど正しいです。選択しようとしているオブジェクトが単一のオブジェクトである場合はselect_relatedを使用するので、OneToOneFieldまたはForeignKeyとなります。一連のものを取得するときはprefetch_relatedを使用します。そのため、ManyToManyFieldsは前述のとおり、またはForeignKeysを逆にします。 "reverse ForeignKeys"の意味を明確にするために、例を示します。

class ModelA(models.Model):
    pass

class ModelB(models.Model):
    a = ForeignKey(ModelA)

ModelB.objects.select_related('a').all() # Forward ForeignKey relationship
ModelA.objects.prefetch_related('modelb_set').all() # Reverse ForeignKey relationship

違いは、select_relatedはSQL結合を実行するため、結果がSQLサーバーからテーブルの一部として返されることです。一方、prefetch_relatedは別のクエリを実行するため、元のオブジェクトの冗長列を減らします(上記の例ではModelA)。 prefetch_relatedを使用できるものなら何でもにselect_relatedを使用できます。

トレードオフは、prefetch_relatedがIDのリストを作成してサーバーに送信する必要があることです。これにはしばらく時間がかかります。トランザクション内でこれを行う良い方法があるかどうかはわかりませんが、Djangoは常にリストを送信してSELECT ... WHERE pk IN(...、...、...)と言っているだけです。基本的に。この場合、プリフェッチされたデータがまばらである場合(たとえば、米国の州オブジェクトが人の住所にリンクされているとします)、1対1に近いと通信が無駄になります。疑問がある場合は、両方を試して、どちらがより良いパフォーマンスを見せるかを確認してください。

上記で説明したことはすべて、基本的にデータベースとの通信に関するものです。しかしPython側ではprefetch_relatedにはデータベース内の各オブジェクトを表すために単一のオブジェクトが使用されるという追加の利点があります。 select_relatedを使用すると、Pythonでは "親"オブジェクトごとに重複するオブジェクトが作成されます。 Pythonのオブジェクトにはかなりの量のメモリオーバーヘッドがあるため、これも考慮する必要があります。

327
CrazyCasta

どちらの方法でも同じ目的が達成され、不要なdbクエリが不要になります。しかしそれらは効率のために異なったアプローチを使用します。

これらの方法のいずれかを使用する唯一の理由は、単一の大きなクエリが多くの小さなクエリよりも望ましい場合です。 Djangoはデータベースに対してオンデマンドでクエリを実行するのではなく、大規模なクエリを使用してメモリにモデルをプリエンプティブに作成します。

select_relatedは各検索で結合を実行しますが、結合されたすべてのテーブルの列を含むようにselectを拡張します。ただし、この方法には注意が必要です。

結合は、クエリ内の行数を増やす可能性があります。外部キーまたは1対1フィールドで結合を実行しても、行数は増えません。ただし、多対多結合にはこの保証はありません。そのため、Djangoはselect_relatedを予期せず大量の結合にならないような関係に制限します。

prefetch_related "pythonに参加する"はもう少し憂慮すべきです。結合するテーブルごとに個別のクエリを作成します。次のように、WHERE IN句を使用してこれらの各テーブルをフィルタリングします。

SELECT "credential"."id",
       "credential"."uuid",
       "credential"."identity_id"
FROM   "credential"
WHERE  "credential"."identity_id" IN
    (84706, 48746, 871441, 84713, 76492, 84621, 51472);

潜在的に多すぎる行を使用して単一の結合を実行するのではなく、各テーブルは別々のクエリに分割されます。

13
cdosborn