بهینه سازی دسترسی به دیتابیس در جنگو

یه مدت میشه که جنگو رو شروع کردم و از این به بعد اینجا قراره از تجربیاتم بنویسم.

خب، بریم که شروع کنیم.

حل مشکلN+1 کوئری در جنگو

یکی از اولین مشکلاتی که هنگام کار کردن با دیتای واقعی بهش برمی‏خورید مسئلهN+1 کوئریه. میگم دیتای واقعی چون وقتی رکوردهای یک جدول زیاد بشه و زمان پاسخ سرور بالا بره تازه متوجه میشید که یه جای کار میلنگه. اینجا سعی شده با یک مثال ساده، اصل مسئله و راه حلش توضیح داده بشه.
فرض کنید این مدل‏ها رو داریم:

class Author(models.Model):
        name = models.CharField(max_length=30)
class Book(models.Model):
        name = models.CharField(max_length=50)
        author = models.ForeignKey(Author, on_delete=’models.Cascade’, related_name=books)

حالا می‏خوایم یک کتاب با id مشخص رو بگیریم و نام نویسنده ‏اش رو بیرون بکشیم:

book = Book.objects.get(pk=1)
book.author.name

اینجا یک کوئری برای خط اول و select کردن کتاب و یه کوئری دیگه برای خط دوم و گرفتن نام نویسنده کتاب اجرا میشه.

تا اینجا به نظر نمیاد مشکل خاصی وجود داشته باشه.

جایی به مشکل برمی‏خوریم که تعداد زیادی کتاب هست و میخوایم لیستی از نام کتاب به همراه نام نویسنده کتاب داشته باشیم.

output = []
for book in Book.objects.all():
        output.append({
        ‘book’: book.name,
        ‘author’: book.author.name
        })

یکبار همه کتاب‏ها رو select می‏ کنیم و بعدش برای هر کتاب نام نویسنده ‏اش رو می گیریم. هربار دسترسی به کلید خارجی نیاز به یک کوئری داره و اینجا ما به تعداد کتاب‏ ها کوئری خواهیم داشت به علاوه کوئری اول که همه کتاب‏ ها رو گرفتیم. N+1 از اینجا میاد.

هر چه تعداد کتاب ها بالاتر بره تعداد کوئری ‏ها بیشتر میشه و سرعت بیرون کشیدن اطلاعات کم و کمتر.

دلیل این اتفاق هم این هست که در جنگو اساساً Querysetها Lazy هستند و تا وقتی نیاز به اونها نباشه، اجرا نمیشن. در اینجا هم وقتی select روی کتاب ‏ها اتفاق میوفته، آبجت‏های مرتبط (related_object) مثل نویسنده کتاب لود نمیشه. درست وقتی که نام نویسنده رو میخوایم کوئری مربوط به اون اجرا میشه.

در مقابل این مفهوم، Eager loading رو داریم. در Eager loading تمام داده‏های مورد نیاز بصورت یکجا لود میشن و بنابراین فقط یک کوئری کافیه تا نام کتاب ‏ها و نویسندها‏شون رو داشته باشیم.

در جنگو از طریق تابع select_related میتونیم Eager loading رو پیاده کنیم:

for book in Book.objects.all().select_related(‘author’):
        output.append({
        ‘book’: book.name,
        ‘author’: book.author.name
        })

این select_related درواقع یک Join در SQL هست که برای ارتباطات یک به یک و یک به چند کاربرد داره.

تابع دیگری به اسم prefetched_related وجود داره که همین Join رو نه در SQL که در پایتون انجام میده و برای روابط یک به چند و چند به چند استفاده میشه.