کوئریهای دیتابیس جنگو را با select_related و prefetch_related برای افزایش کارایی بهینهسازی کنید. مثالهای عملی و بهترین شیوهها را بیاموزید.
بهینهسازی کوئری در ORM جنگو: مقایسه select_related و prefetch_related
با رشد اپلیکیشن جنگوی شما، کوئریهای بهینه دیتابیس برای حفظ عملکرد مطلوب، حیاتی میشوند. ORM جنگو ابزارهای قدرتمندی برای به حداقل رساندن درخواستها به دیتابیس و بهبود سرعت کوئریها فراهم میکند. دو تکنیک کلیدی برای رسیدن به این هدف select_related و prefetch_related هستند. این راهنمای جامع این مفاهیم را توضیح داده، کاربرد آنها را با مثالهای عملی نشان میدهد و به شما کمک میکند تا ابزار مناسب را برای نیازهای خاص خود انتخاب کنید.
درک مشکل N+1
قبل از پرداختن به select_related و prefetch_related، درک مشکلی که آنها حل میکنند ضروری است: مشکل کوئری N+1. این مشکل زمانی رخ میدهد که اپلیکیشن شما یک کوئری اولیه برای واکشی مجموعهای از اشیاء اجرا میکند و سپس کوئریهای اضافی (N کوئری، که N تعداد اشیاء است) برای بازیابی دادههای مرتبط برای هر شیء اجرا میکند.
یک مثال ساده با مدلهایی که نویسندگان و کتابها را نشان میدهند در نظر بگیرید:
class Author(models.Model):
name = models.CharField(max_length=255)
class Book(models.Model):
title = models.CharField(max_length=255)
author = models.ForeignKey(Author, on_delete=models.CASCADE)
حالا تصور کنید میخواهید لیستی از کتابها را به همراه نویسندگان مربوطه نمایش دهید. یک رویکرد سادهانگارانه ممکن است به این شکل باشد:
books = Book.objects.all()
for book in books:
print(f"{book.title} by {book.author.name}")
این کد یک کوئری برای واکشی تمام کتابها و سپس یک کوئری برای هر کتاب جهت واکشی نویسندهاش ایجاد میکند. اگر ۱۰۰ کتاب داشته باشید، ۱۰۱ کوئری اجرا خواهید کرد که منجر به سربار عملکرد قابل توجهی میشود. این همان مشکل N+1 است.
معرفی select_related
select_related برای بهینهسازی کوئریهایی که شامل روابط یک-به-یک و کلید خارجی هستند، استفاده میشود. این متد با الحاق (join) جدول(های) مرتبط در کوئری اولیه کار میکند و به طور موثر دادههای مرتبط را در یک درخواست به دیتابیس واکشی میکند.
بیایید به مثال نویسندگان و کتابها برگردیم. برای رفع مشکل N+1، میتوانیم از select_related به این صورت استفاده کنیم:
books = Book.objects.all().select_related('author')
for book in books:
print(f"{book.title} by {book.author.name}")
اکنون، جنگو یک کوئری واحد و پیچیدهتر اجرا میکند که جداول Book و Author را به هم ملحق میکند. وقتی در حلقه به book.author.name دسترسی پیدا میکنید، دادهها از قبل در دسترس هستند و هیچ کوئری اضافی به دیتابیس انجام نمیشود.
استفاده از select_related با روابط چندگانه
select_related میتواند روابط چندگانه را طی کند. برای مثال، اگر مدلی با یک کلید خارجی به مدل دیگر داشته باشید که آن مدل نیز به نوبه خود یک کلید خارجی به مدل دیگری دارد، میتوانید از select_related برای واکشی تمام دادههای مرتبط در یک مرحله استفاده کنید.
class Country(models.Model):
name = models.CharField(max_length=255)
class AuthorProfile(models.Model):
author = models.OneToOneField(Author, on_delete=models.CASCADE)
country = models.ForeignKey(Country, on_delete=models.CASCADE)
# Add country to Author
Author.profile = models.OneToOneField(AuthorProfile, on_delete=models.CASCADE, null=True, blank=True)
authors = Author.objects.all().select_related('profile__country')
for author in authors:
print(f"{author.name} is from {author.profile.country.name if author.profile else 'Unknown'}")
در این حالت، select_related('profile__country') مدل AuthorProfile و Country مرتبط با آن را در یک کوئری واحد واکشی میکند. به علامت دو زیرخط (__) توجه کنید که به شما امکان میدهد درخت روابط را پیمایش کنید.
محدودیتهای select_related
select_related بیشترین کارایی را با روابط یک-به-یک و کلید خارجی دارد. این متد برای روابط چند-به-چند یا روابط کلید خارجی معکوس مناسب نیست، زیرا هنگام کار با مجموعه دادههای مرتبط بزرگ، میتواند منجر به کوئریهای بزرگ و ناکارآمد شود. برای این سناریوها، prefetch_related انتخاب بهتری است.
معرفی prefetch_related
prefetch_related برای بهینهسازی کوئریهای شامل روابط چند-به-چند و کلید خارجی معکوس طراحی شده است. به جای استفاده از join، prefetch_related کوئریهای جداگانهای برای هر رابطه اجرا کرده و سپس از پایتون برای «الحاق» نتایج استفاده میکند. اگرچه این کار شامل چندین کوئری است، اما هنگام کار با مجموعه دادههای مرتبط بزرگ، میتواند کارآمدتر از استفاده از join باشد.
سناریویی را در نظر بگیرید که در آن هر کتاب میتواند چندین ژانر داشته باشد:
class Genre(models.Model):
name = models.CharField(max_length=255)
class Book(models.Model):
title = models.CharField(max_length=255)
author = models.ForeignKey(Author, on_delete=models.CASCADE)
genres = models.ManyToManyField(Genre)
برای واکشی لیستی از کتابها به همراه ژانرهایشان، استفاده از select_related مناسب نخواهد بود. به جای آن، از prefetch_related استفاده میکنیم:
books = Book.objects.all().prefetch_related('genres')
for book in books:
genre_names = [genre.name for genre in book.genres.all()]
print(f"{book.title} ({', '.join(genre_names)}) by {book.author.name}")
در این حالت، جنگو دو کوئری اجرا میکند: یکی برای واکشی تمام کتابها و دیگری برای واکشی تمام ژانرهای مرتبط با آن کتابها. سپس از پایتون برای مرتبط کردن کارآمد ژانرها با کتابهای مربوطهشان استفاده میکند.
prefetch_related با کلیدهای خارجی معکوس
prefetch_related همچنین برای بهینهسازی روابط کلید خارجی معکوس مفید است. مثال زیر را در نظر بگیرید:
class Author(models.Model):
name = models.CharField(max_length=255)
country = models.CharField(max_length=255, blank=True, null=True) # Added for clarity
def __str__(self):
return self.name
class Book(models.Model):
title = models.CharField(max_length=255)
author = models.ForeignKey(Author, related_name='books', on_delete=models.CASCADE)
برای بازیابی لیستی از نویسندگان و کتابهایشان:
authors = Author.objects.all().prefetch_related('books')
for author in authors:
book_titles = [book.title for book in author.books.all()]
print(f"{author.name} has written: {', '.join(book_titles)}")
در اینجا، prefetch_related('books') تمام کتابهای مرتبط با هر نویسنده را در یک کوئری جداگانه واکشی میکند و از مشکل N+1 هنگام دسترسی به author.books.all() جلوگیری میکند.
استفاده از prefetch_related با یک کوئریست (queryset)
شما میتوانید با ارائه یک کوئریست سفارشی برای واکشی اشیاء مرتبط، رفتار prefetch_related را بیشتر سفارشی کنید. این ویژگی به ویژه زمانی مفید است که نیاز به فیلتر کردن یا مرتبسازی دادههای مرتبط دارید.
from django.db.models import Prefetch
authors = Author.objects.prefetch_related(Prefetch('books', queryset=Book.objects.filter(title__icontains='django')))
for author in authors:
django_books = author.books.all()
print(f"{author.name} has written {len(django_books)} books about Django.")
در این مثال، شیء Prefetch به ما امکان میدهد یک کوئریست سفارشی تعیین کنیم که فقط کتابهایی را واکشی میکند که عنوانشان شامل «django» باشد.
زنجیرهسازی prefetch_related
مشابه select_related، میتوانید فراخوانیهای prefetch_related را برای بهینهسازی روابط چندگانه زنجیرهوار کنید:
authors = Author.objects.all().prefetch_related('books__genres')
for author in authors:
for book in author.books.all():
genres = book.genres.all()
print(f"{author.name} wrote {book.title} which is of genre(s) {[genre.name for genre in genres]}")
این مثال کتابهای مرتبط با نویسنده و سپس ژانرهای مرتبط با آن کتابها را پیشواکشی (prefetch) میکند. استفاده از prefetch_related زنجیرهای به شما امکان میدهد روابط عمیقاً تودرتو را بهینه کنید.
select_related در مقابل prefetch_related: انتخاب ابزار مناسب
خب، چه زمانی باید از select_related و چه زمانی از prefetch_related استفاده کرد؟ در اینجا یک راهنمای ساده وجود دارد:
select_related: برای روابط یک-به-یک و کلید خارجی که نیاز به دسترسی مکرر به دادههای مرتبط دارید، استفاده کنید. این متد یک join در دیتابیس انجام میدهد، بنابراین معمولاً برای بازیابی مقادیر کوچک دادههای مرتبط سریعتر است.prefetch_related: برای روابط چند-به-چند و کلید خارجی معکوس، یا هنگام کار با مجموعه دادههای مرتبط بزرگ استفاده کنید. این متد کوئریهای جداگانهای اجرا کرده و از پایتون برای الحاق نتایج استفاده میکند، که میتواند کارآمدتر از joinهای بزرگ باشد. همچنین زمانی که نیاز به فیلتر کردن کوئریست سفارشی بر روی اشیاء مرتبط دارید، از آن استفاده کنید.
به طور خلاصه:
- نوع رابطه:
select_related(ForeignKey، OneToOne)،prefetch_related(ManyToManyField، کلید خارجی معکوس) - نوع کوئری:
select_related(JOIN)،prefetch_related(کوئریهای جداگانه + الحاق در پایتون) - حجم داده:
select_related(دادههای مرتبط کوچک)،prefetch_related(دادههای مرتبط بزرگ)
مثالهای عملی و بهترین شیوهها
در اینجا چند مثال عملی و بهترین شیوه برای استفاده از select_related و prefetch_related در سناریوهای واقعی آورده شده است:
- تجارت الکترونیک: هنگام نمایش جزئیات محصول، از
select_relatedبرای واکشی دستهبندی و سازنده محصول استفاده کنید. ازprefetch_relatedبرای واکشی تصاویر محصول یا محصولات مرتبط استفاده کنید. - رسانههای اجتماعی: هنگام نمایش پروفایل کاربر، از
prefetch_relatedبرای واکشی پستها و دنبالکنندگان کاربر استفاده کنید. ازselect_relatedبرای بازیابی اطلاعات پروفایل کاربر استفاده کنید. - سیستم مدیریت محتوا (CMS): هنگام نمایش یک مقاله، از
select_relatedبرای واکشی نویسنده و دستهبندی استفاده کنید. ازprefetch_relatedبرای واکشی برچسبها و نظرات مقاله استفاده کنید.
بهترین شیوههای عمومی:
- کوئریهای خود را پروفایل کنید: از نوار ابزار دیباگ جنگو یا ابزارهای پروفایلینگ دیگر برای شناسایی کوئریهای کند و مشکلات بالقوه N+1 استفاده کنید.
- ساده شروع کنید: با یک پیادهسازی ساده شروع کنید و سپس بر اساس نتایج پروفایلینگ بهینهسازی کنید.
- کامل تست کنید: اطمینان حاصل کنید که بهینهسازیهای شما باگهای جدید یا افت عملکرد ایجاد نمیکنند.
- کش کردن را در نظر بگیرید: برای دادههایی که به طور مکرر به آنها دسترسی پیدا میشود، استفاده از مکانیزمهای کش (مانند فریمورک کش جنگو یا Redis) را برای بهبود بیشتر عملکرد در نظر بگیرید.
- از ایندکسها در دیتابیس استفاده کنید: این یک امر ضروری برای عملکرد بهینه کوئری است، به ویژه در محیط پروداکشن.
تکنیکهای بهینهسازی پیشرفته
فراتر از select_related و prefetch_related، تکنیکهای پیشرفته دیگری نیز وجود دارد که میتوانید برای بهینهسازی کوئریهای ORM جنگو خود استفاده کنید:
only()وdefer(): این متدها به شما امکان میدهند مشخص کنید کدام فیلدها از دیتابیس بازیابی شوند. ازonly()برای بازیابی فقط فیلدهای ضروری و ازdefer()برای مستثنی کردن فیلدهایی که فوراً مورد نیاز نیستند، استفاده کنید.values()وvalues_list(): این متدها به شما امکان میدهند دادهها را به صورت دیکشنری یا تاپل بازیابی کنید، به جای نمونههای مدل جنگو. این روش زمانی که فقط به زیرمجموعهای از فیلدهای مدل نیاز دارید، میتواند کارآمدتر باشد.- کوئریهای SQL خام: در برخی موارد، ORM جنگو ممکن است کارآمدترین راه برای بازیابی دادهها نباشد. میتوانید برای کوئریهای پیچیده یا بسیار بهینهشده از کوئریهای SQL خام استفاده کنید.
- بهینهسازیهای مختص دیتابیس: دیتابیسهای مختلف (مانند PostgreSQL، MySQL) تکنیکهای بهینهسازی متفاوتی دارند. در مورد ویژگیهای مختص دیتابیس تحقیق کرده و از آنها برای بهبود بیشتر عملکرد استفاده کنید.
ملاحظات بینالمللیسازی
هنگام توسعه اپلیکیشنهای جنگو برای مخاطبان جهانی، در نظر گرفتن بینالمللیسازی (i18n) و محلیسازی (l10n) مهم است. این موضوع میتواند به طرق مختلف بر کوئریهای دیتابیس شما تأثیر بگذارد:
- دادههای مختص زبان: ممکن است نیاز به ذخیره ترجمههای محتوا در دیتابیس خود داشته باشید. از فریمورک i18n جنگو برای مدیریت ترجمهها استفاده کنید و اطمینان حاصل کنید که کوئریهای شما نسخه زبان صحیح دادهها را بازیابی میکنند.
- مجموعه کاراکترها و Collationها: مجموعه کاراکترها و collationهای مناسبی را برای دیتابیس خود انتخاب کنید تا از طیف گستردهای از زبانها و کاراکترها پشتیبانی کند.
- مناطق زمانی: هنگام کار با تاریخ و زمان، به مناطق زمانی توجه داشته باشید. تاریخ و زمان را به صورت UTC ذخیره کرده و هنگام نمایش، آنها را به منطقه زمانی محلی کاربر تبدیل کنید.
- قالببندی ارز: هنگام نمایش قیمتها، از نمادها و قالببندی ارزی مناسب بر اساس منطقه کاربر استفاده کنید.
نتیجهگیری
بهینهسازی کوئریهای ORM جنگو برای ساخت اپلیکیشنهای وب مقیاسپذیر و با عملکرد بالا ضروری است. با درک و استفاده مؤثر از select_related و prefetch_related، میتوانید تعداد کوئریهای دیتابیس را به طور قابل توجهی کاهش داده و پاسخگویی کلی اپلیکیشن خود را بهبود بخشید. به یاد داشته باشید که کوئریهای خود را پروفایل کنید، بهینهسازیهای خود را به طور کامل تست کنید و تکنیکهای پیشرفته دیگر را برای افزایش بیشتر عملکرد در نظر بگیرید. با پیروی از این بهترین شیوهها، میتوانید اطمینان حاصل کنید که اپلیکیشن جنگوی شما، صرفنظر از اندازه یا پیچیدگی آن، تجربه کاربری روان و کارآمدی را ارائه میدهد. همچنین در نظر داشته باشید که طراحی خوب دیتابیس و ایندکسهای به درستی پیکربندی شده برای عملکرد بهینه ضروری هستند.