Apa perbedaan antara select_related dan prefetch_related di Django ORM?

291

Dalam Django doc,

select_related() "mengikuti" hubungan kunci-asing, memilih data objek terkait tambahan ketika mengeksekusi kueri.

prefetch_related() melakukan pencarian terpisah untuk setiap hubungan, dan apakah "bergabung" dengan Python.

Apa yang dimaksud dengan "melakukan penggabungan dengan python"? Dapatkah seseorang mengilustrasikan dengan sebuah contoh?

Pemahaman saya adalah bahwa untuk hubungan kunci asing, gunakan select_related; dan untuk hubungan M2M, gunakan prefetch_related. Apakah ini benar?

NeoWang
sumber
2
Melakukan join dengan python berarti join tidak akan terjadi di database. Dengan select_related, bergabung Anda terjadi di database dan Anda hanya menderita satu permintaan basis data. Dengan prefetch_related, Anda akan mengeksekusi dua kueri dan kemudian hasilnya akan 'bergabung' oleh ORM sehingga Anda masih bisa mengetikkan object.related_set
Mark Galloway
3
Sebagai catatan kaki, Timmy O'Mahony juga dapat menjelaskan perbedaan mereka menggunakan hit basis data: link
Mærcos
Ini dapat membantu Anda belajar batta.com/blog/working-with-select_related-in-django-89
anjaneyulubatta505

Jawaban:

424

Pemahaman Anda sebagian besar benar. Anda menggunakan select_relatedketika objek yang akan Anda pilih adalah objek tunggal, jadi OneToOneFieldatau a ForeignKey. Anda menggunakan prefetch_relatedketika Anda akan mendapatkan "set" hal-hal, begitu juga ManyToManyFieldseperti yang Anda katakan atau membalikkan ForeignKey. Hanya untuk memperjelas apa yang saya maksud dengan "membalikkan ForeignKey" berikut ini sebuah contoh:

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

Perbedaannya adalah select_relatedapakah SQL bergabung dan karenanya mendapatkan hasilnya kembali sebagai bagian dari tabel dari server SQL. prefetch_relateddi sisi lain mengeksekusi kueri lain dan karenanya mengurangi kolom redundan pada objek asli ( ModelAdalam contoh di atas). Anda dapat menggunakan prefetch_relateduntuk apa pun yang dapat Anda gunakan select_relateduntuk.

Pengorbanan adalah yang prefetch_relatedharus membuat dan mengirim daftar ID untuk memilih kembali ke server, ini bisa memakan waktu cukup lama. Saya tidak yakin apakah ada cara yang baik untuk melakukan ini dalam transaksi, tetapi pemahaman saya adalah bahwa Django selalu hanya mengirim daftar dan mengatakan SELECT ... WHERE pk IN (..., ..., ...) pada dasarnya Dalam hal ini jika data yang diambil sebelumnya jarang (misalkan objek Negara AS yang ditautkan ke alamat orang) ini bisa sangat baik, namun jika lebih dekat dengan satu-ke-satu, ini dapat membuang banyak komunikasi. Jika ragu, coba keduanya dan lihat mana yang berkinerja lebih baik.

Segala sesuatu yang dibahas di atas pada dasarnya adalah tentang komunikasi dengan database. Namun di sisi Python prefetch_relatedmemiliki manfaat tambahan bahwa satu objek digunakan untuk mewakili setiap objek dalam database. Dengan select_relatedduplikat objek akan dibuat dengan Python untuk setiap objek "induk". Karena objek dalam Python memiliki sedikit memori yang layak, ini juga dapat menjadi pertimbangan.

CrazyCasta
sumber
3
apa yang lebih cepat?
elad silver
24
select_relatedadalah satu permintaan sementara prefetch_relateddua, jadi yang pertama lebih cepat. Tetapi select_relatedtidak akan membantu Anda untuk ManyToManyField's
bhinesley
31
@eladsilver Maaf atas balasan yang lambat. Itu sebenarnya tergantung. select_relatedmenggunakan BERGABUNG dalam SQL saat prefetch_relatedmenjalankan kueri pada model pertama, mengumpulkan semua ID yang perlu prefetch dan kemudian menjalankan kueri dengan klausa IN di WHERE dengan semua ID yang dibutuhkan. Jika Anda mengatakan 3-5 model menggunakan kunci asing yang sama, select_relatedhampir pasti akan lebih baik. Jika Anda memiliki 100 atau 1000 model yang menggunakan kunci asing yang sama, prefetch_relatedsebenarnya bisa lebih baik. Di antara Anda harus menguji dan melihat apa yang terjadi.
CrazyCasta
1
Saya akan membantah komentar Anda tentang prefetch terkait "umumnya tidak masuk akal". Itu benar untuk bidang FK yang ditandai unik, tetapi di mana saja di mana beberapa baris memiliki nilai FK yang sama (penulis, pengguna, kategori, kota, dll.) Prefetch mengurangi bandwidth antara Django dan DB tetapi tidak menduplikasi baris. Biasanya juga menggunakan lebih sedikit memori pada DB. Salah satu dari ini seringkali lebih penting daripada overhead dari satu permintaan ekstra. Mengingat ini adalah jawaban teratas pada pertanyaan yang cukup populer, saya pikir itu harus dicatat dalam jawabannya.
Gordon Wrigley
1
@ GordonWrigley Ya, sudah lama sejak saya menulis itu, jadi saya kembali dan mengklarifikasi sedikit. Saya tidak yakin saya setuju dengan bit "menggunakan lebih sedikit memori pada DB", tapi ya untuk semuanya. Dan itu pasti dapat menggunakan lebih sedikit memori di sisi Python.
CrazyCasta
26

Kedua metode mencapai tujuan yang sama, untuk melepaskan pertanyaan db yang tidak perlu. Tetapi mereka menggunakan pendekatan berbeda untuk efisiensi.

Satu-satunya alasan untuk menggunakan salah satu metode ini adalah ketika satu kueri besar lebih disukai daripada banyak kueri kecil. Django menggunakan permintaan besar untuk membuat model dalam memori lebih dulu daripada melakukan permintaan permintaan terhadap database.

select_relatedmelakukan gabungan dengan setiap pencarian, tetapi memperluas pemilihan untuk menyertakan kolom dari semua tabel yang bergabung. Namun pendekatan ini memiliki peringatan.

Bergabung memiliki potensi untuk mengalikan jumlah baris dalam kueri. Saat Anda melakukan penggabungan atas kunci asing atau bidang satu-ke-satu, jumlah baris tidak akan bertambah. Namun, banyak-ke-banyak bergabung tidak memiliki jaminan ini. Jadi, Django membatasi select_relatedhubungan yang tidak terduga menghasilkan gabungan besar.

The "bergabung dalam python" untuk prefetch_relatedsedikit lebih mengkhawatirkan maka harus. Ini membuat kueri terpisah untuk setiap tabel yang akan digabung. Ini memfilter masing-masing tabel ini dengan klausa WHERE IN, seperti:

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

Alih-alih melakukan gabungan tunggal dengan berpotensi terlalu banyak baris, setiap tabel dibagi menjadi kueri yang terpisah.

cdosborn
sumber
1

Seperti yang dikatakan dokumentasi Django:

prefetch_related ()

Mengembalikan QuerySet yang secara otomatis akan mengambil, dalam satu batch, objek terkait untuk masing-masing pencarian yang ditentukan.

Ini memiliki tujuan yang mirip dengan select_related, karena keduanya dirancang untuk menghentikan banjir permintaan basis data yang disebabkan oleh mengakses objek terkait, tetapi strateginya sangat berbeda.

Select_related bekerja dengan membuat gabungan SQL dan termasuk bidang objek terkait dalam pernyataan SELECT. Karena alasan ini, select_related mendapatkan objek terkait dalam kueri basis data yang sama. Namun, untuk menghindari set hasil yang jauh lebih besar yang akan dihasilkan dari bergabung di hubungan 'banyak', select_related terbatas pada hubungan bernilai tunggal - kunci asing dan satu-ke-satu.

prefetch_related, di sisi lain, melakukan pencarian terpisah untuk setiap hubungan, dan apakah 'bergabung' dengan Python. Ini memungkinkannya untuk mengambil banyak objek ke banyak dan banyak-ke-satu, yang tidak dapat dilakukan menggunakan select_related, di samping kunci asing dan hubungan satu-ke-satu yang didukung oleh select_related. Ini juga mendukung prefetching GenericRelation dan GenericForeignKey, namun, itu harus dibatasi pada set hasil yang homogen. Misalnya, objek pengambilan awal yang dirujuk oleh GenericForeignKey hanya didukung jika kueri dibatasi ke satu ContentType.

Informasi lebih lanjut tentang ini: https://docs.djangoproject.com/en/2.2/ref/models/querysets/#prefetch-related

Amin.B
sumber
1

Pergi melalui jawaban yang sudah diposting. Hanya berpikir akan lebih baik jika saya menambahkan jawaban dengan contoh aktual.

Katakanlah Anda memiliki 3 model Django yang terkait.

class M1(models.Model):
    name = models.CharField(max_length=10)

class M2(models.Model):
    name = models.CharField(max_length=10)
    select_relation = models.ForeignKey(M1, on_delete=models.CASCADE)
    prefetch_relation = models.ManyToManyField(to='M3')

class M3(models.Model):
    name = models.CharField(max_length=10)

Di sini Anda dapat meminta M2model dan M1objek relatifnya menggunakan select_relationbidang dan M3objek menggunakan prefetch_relationbidang.

Namun seperti yang telah kami sebutkan M1relasinya dari M2a ForeignKey, hanya mengembalikan 1 record untuk M2objek apa pun . Hal yang sama berlaku OneToOneFieldjuga.

Tapi M3hubungan dari M2adalah ManyToManyFieldyang dapat mengembalikan sejumlah M1objek.

Pertimbangkan kasus di mana Anda memiliki 2 M2objek m21, m22yang memiliki 5M3 objek terkait yang sama dengan ID 1,2,3,4,5. Ketika Anda mengambil M3objek terkait untuk masing-masing M2objek tersebut, jika Anda menggunakan pilih terkait, ini adalah cara kerjanya.

Langkah:

  1. Temukan m21objek.
  2. Permintaan semua M3objek yang terkait dengan m21objek yang memiliki ID 1,2,3,4,5.
  3. Ulangi hal yang sama untuk m22objek dan semua M2objek lainnya .

Karena kami memiliki 1,2,3,4,5ID yang sama untuk keduanya m21, m22objek, jika kami menggunakan opsi select_related, itu akan meminta DB dua kali untuk ID yang sama yang sudah diambil.

Alih-alih jika Anda menggunakan prefetch_related, ketika Anda mencoba untuk mendapatkan M2objek, itu akan membuat catatan dari semua ID yang dikembalikan objek Anda (Catatan: hanya ID) saat melakukan kueri M2tabel dan sebagai langkah terakhir, Django akan membuat kueri ke M3tabel dengan set semua ID yang M2objek Anda telah kembali. dan bergabung dengan mereka ke M2objek menggunakan Python, bukan database.

Dengan cara ini Anda hanya menanyakan satu M3objek sekali saja yang meningkatkan kinerja.

Jarvis
sumber