N+1 query problem وقتی است که برنامه دیتایی که با یک query می توانست به دست بیاره رو N بار بیشتر query بزنه. طبیعتا مشکل پرفورمنسیه.
توی مدیوم یه مقاله هست که این مشکل رو با یک مثال توضیح داده که باعث میشه خیلی خوب مشکل N+1 query رو بفهمیم.
فرض کنید که میخاید کیک درست کنید. یخچال توی آشپزخونه نیست و توی اتاق زیر شیروانیه بنابراین برای برداشتن هر چیزی از تو یخچال باید پله ها رو بالا بریم. شما توی آشپزخونه هستید ولی کتاب شیرینی پزی توی اتاق خوابه.
اول میرید از اتاق خواب کتاب آشپزی رو میارید به آشپزخونه بعد میبینید گفته که به 200 گرم آرد نیاز دارید. میرید اتاق زیرشیروانی و آرد رو میارید. دستور پخت نوشته 3 عدد تخم مرغ، دوباره میرید اتاق زیرشیروانی و 3 تا تخم مرغ میارید. دوباره میبینید که نوشته یه لیوان شیر و ….
میبینید که با این روش شما N بار اضافه رفتید بالا در حالی که همون دفعه اول که رفتید کتاب آشپزی رو بیارد بهتر بود همه وسایل مورد نیاز رو هم بیارید.
حالا یه مثال از ORM جنگو ببینیم که این مشکل رو چطور حل میکنه.
فرض کنید که یه مدل Post داریم که هر Post یه Author داره و طبیعتا هر Author میتونه چند تا Post داشته باشه. رابطه 1->N هست. حالا میخایم پست ها و نویسنده هاشون رو چاپ کنیم.
|
posts = Post.objects.order_by("created_on") for post in posts: print(post.title, "written by", post.author) |
توی کد بالا مشکل N+1 وجود داره.
توی خط اول چند query به دیتابیس فرستاده میشه؟ هیچ به دلیل اینکه ORM جنگو Lazy هست (تا زمان درخواست داده query ارسال نمیشه)
توی خط دوم که توی حلقه درخواست داده شده یک query به دیتابیس ارسال میشه و لیست پست ها رو میگیره و میریزه توی queryset. توی قسمت print وقتی title رو میخات چاپ کنه دیگه query زده نمیشه چون title یکی از property های Post هست که قبلا گرفته شده ولی وقتی میخات author رو چاپ کنه دوباره یک query به دیتابیس زده میشه که Author مربوط به اون پست رو بگیره (چون دو تا model جدا با رابطه 1->N هستن) بنابراین توی هر iteration از حلقه یک query دیگه زده میشه و این پرهزینه است و بهش میگن N+1 problem.
برای حل این مشکل باید برای پست ها Author ها رو هم یکجا با یک query بگیریم. برای حل این موضوع باید روی دو تا model یک join زده بشه.
ORM جنگو این مشکل رو با select_related() و prefetch_related() حل کرده.
select_related()
|
posts = Post.objects.order_by("created_on").select_related("author") for post in posts: print(post.title, "written by", post.author.name) |
prefetch_related()
|
posts = Post.objects.order_by("created_on").prefetch_related("author") for post in posts: print(post.title, "written by", post.author.name) |
توی دو تا کد بالا فقط 1 query به دیتابیس زده میشه.
فرق select_related و prefetch_related چیه؟
select_related به یک Join تبدیل شده و به دیتابیس ارسال میشود و برای مدل هایی که ارتباط 1->N یا one to one دارند استفاده میشود اما prefetch_related دو داده را گرفته و به صورت پایتونی join میزنه در ضمن برای رابطه های چند به چند یا Generic استفاده میشه.
مثالی که توی مستندات جنگو هست:
|
from django.utils import timezone # Find all the blogs with entries scheduled to be published in the future. blogs = set() for e in Entry.objects.filter(pub_date__gt=timezone.now()).select_related("blog"): # Without select_related(), this would make a database query for each # loop iteration in order to fetch the related blog for each entry. blogs.add(e.blog) |
مثال زیبای بعدی
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
|
from django.db import models class City(models.Model): # ... pass class Person(models.Model): # ... hometown = models.ForeignKey( City, on_delete=models.SET_NULL, blank=True, null=True, ) class Book(models.Model): # ... author = models.ForeignKey(Person, on_delete=models.CASCADE) # Hits the database with joins to the author and hometown tables. b = Book.objects.select_related("author__hometown").get(id=4) p = b.author # Doesn't hit the database. c = p.hometown # Doesn't hit the database. # Without select_related()... b = Book.objects.get(id=4) # Hits the database. p = b.author # Hits the database. c = p.hometown # Hits the database. |