حول المحتوى:
استراتيجيات لخفض عدد الاستعلامات وتسريع استجابة قواعد البيانات في Django: استخدام select_related, prefetch_related، الفهارس المخصصة، وتحليل الـ QuerySet بالأمثلة.
في تطبيقات Django المتوسطة والكبيرة، غالبًا ما يكون عنق الزجاجة في الأداء هو الاتصال بقاعدة البيانات، وليس منطق الكود نفسه. أي استعلام إضافي غير ضروري، أو عدم استخدام الفهارس بشكل صحيح، يمكن أن يضاعف زمن الاستجابة ويستهلك موارد الخادم.
في هذا الدليل سنركز على تحسين Django ORM، من خلال استراتيجيات عملية لتقليل عدد الاستعلامات وتسريع استجابة قاعدة البيانات: select_related، prefetch_related، الفهارس المخصصة، وتحليل الـ QuerySet بالأمثلة. للمقدمة العامة حول ORM يمكنك الرجوع إلى مقال: ما المقصود بالـ ORM.
Django ORM يوفر طبقة مريحة للتعامل مع قاعدة البيانات عبر Python بدلًا من SQL الخام. لكنه سيف ذو حدين: استخدامه بدون وعي يمكن أن ينتج عنه:
النتيجة: صفحات أبطأ، ضغط أعلى على قاعدة البيانات، وتكلفة أعلى للبنية التحتية. لذلك تحسين Django ORM ليس رفاهية، بل ضرورة في أي مشروع جاد.
مشكلة N+1 تحدث عندما تجلب قائمة من العناصر (استعلام واحد)، ثم تقوم بعمل استعلام إضافي لكل عنصر لجلب بيانات مرتبطة. النتيجة: N+1 استعلام بدل استعلام أو اثنين فقط.
لنفترض وجود موديلات بسيطة:
class Author(models.Model):
name = models.CharField(max_length=200)
class Book(models.Model):
title = models.CharField(max_length=200)
author = models.ForeignKey(Author, on_delete=models.CASCADE)
وفي الـ view:
def book_list(request):
books = Book.objects.all() # استعلام واحد
for book in books:
print(book.author.name) # استعلام إضافي لكل كتاب
في هذه الحالة، Django ينفذ استعلامًا لجلب الكتب، ثم لكل كتاب استعلام آخر لجلب الـ author. لو عندك 100 كتاب، ستنفذ 101 استعلام.
select_related يستخدم في العلاقات التي تعيد سطرًا واحدًا فقط من الجدول المرتبط:
فكر فيه على أنه تنفيذ JOIN على مستوى قاعدة البيانات.
def book_list(request):
books = Book.objects.select_related('author') # JOIN مع جدول Author
for book in books:
print(book.author.name) # لا استعلام إضافي
الآن يتم تنفيذ استعلام واحد فقط يجلب الكتب مع المؤلفين دفعة واحدة باستخدام JOIN، ويختفي تمامًا نمط N+1.
Book.objects.select_related('author', 'publisher')
Book.objects.select_related('author__profile')
على عكس select_related، الدالة prefetch_related مناسبة للعلاقات التي يمكن أن تعيد أكثر من صف:
Django في هذه الحالة لا يمكنه استخدام JOIN بسيط بدون تكرار الصفوف، لذلك ينفذ عدة استعلامات منفصلة ثم يدمج النتائج في الذاكرة بكفاءة.
class Category(models.Model):
name = models.CharField(max_length=200)
class Book(models.Model):
title = models.CharField(max_length=200)
categories = models.ManyToManyField(Category, related_name='books')
المشكلة الكلاسيكية:
def book_list(request):
books = Book.objects.all()
for book in books:
for cat in book.categories.all(): # N+1 على مستوى الفئات
print(cat.name)
الحل باستخدام prefetch_related:
def book_list(request):
books = Book.objects.prefetch_related('categories')
for book in books:
for cat in book.categories.all(): # لا استعلام إضافي لكل كتاب
print(cat.name)
Django هنا ينفذ استعلامًا لجلب الكتب، واستعلامًا آخر لجلب جميع العلاقات بين الكتب والفئات، ثم يربطها في الذاكرة، فيختفي نمط N+1.
غالبًا تحتاج إلى كليهما في نفس الوقت:
books = (
Book.objects
.select_related('author') # علاقة 1-1 أو 1-N
.prefetch_related('categories') # علاقة N-N
)
بهذه الطريقة تحصل على الحد الأدنى من الاستعلامات مع الحد الأقصى من البيانات الجاهزة للاستخدام.
ليس دائمًا تحتاج إلى كل أعمدة الجدول. جلب أعمدة كثيرة، خصوصًا الحقول الكبيرة (TextField، JSONField)، يزيد حجم البيانات المنقولة وزمن الاستجابة.
books = Book.objects.only('id', 'title')
for book in books:
print(book.title)
في هذه الحالة، Django ينشئ استعلامًا يجلب فقط id وtitle. إذا حاولت لاحقًا الوصول إلى حقل غير موجود في only، سيقوم ORM بجلبه عند الطلب (استعلام lazy إضافي).
books = Book.objects.defer('long_description', 'notes')
هذه الطريقة مفيدة عندما تحتاج معظم الحقول لكن تريد تأجيل الحقول الكبيرة إلى وقت الحاجة. لكن انتبه: الوصول لهذه الحقول لاحقًا سيولد استعلامات إضافية.
الفهرسة عنصر أساسي في تحسين Django ORM، لأن معظم بطء الاستعلامات يعود إلى البحث الخطي في جداول كبيرة. الفهارس تسمح لقاعدة البيانات بالوصول للسجلات المطلوبة بسرعة كبيرة بدلًا من فحص كل الصفوف.
لشرح أعمق لمفهوم الفهرسة يمكنك مراجعة: الفهرسة في قواعد البيانات و أهميتها وأيضًا: أهم خوارزميات الفهرسة المستخدمة في قواعد البيانات.
يمكنك تعريف فهارس بعدة طرق:
class Book(models.Model):
isbn = models.CharField(max_length=13, unique=True, db_index=True)
# أو
code = models.CharField(max_length=50, db_index=True)
class Book(models.Model):
title = models.CharField(max_length=200)
published_at = models.DateField()
class Meta:
indexes = [
models.Index(fields=['title', 'published_at']),
]
filter()، exclude()، order_by()، distinct()لكن: الإكثار من الفهارس بدون خطة يبطئ عمليات INSERT وUPDATE لأنه يجب تحديث الفهارس في كل مرة. استخدم الفهرسة بناءً على الاستعلامات الفعلية التي تنفذها في التطبيق.
Django ORM يسهّل كتابة الاستعلامات، لكن يظل من المهم أن تفهم الاستعلام SQL الفعلي الناتج. هذا يساعدك على تحسينه.
يمكنك طباعة الاستعلام عبر خاصية query:
qs = Book.objects.filter(title__icontains='django')
print(qs.query) # يظهر SQL الفعلي
في الـ shell أو أثناء التطوير، هذا مفيد لفهم ما إذا كان Django ينتج JOINs غير ضرورية أو شروط معقدة.
.explain() لتحليل خطة التنفيذ (PostgreSQL, MySQL…)في الإصدارات الحديثة من Django يمكنك استخدام:
qs = Book.objects.filter(title__icontains='django')
print(qs.explain())
قاعدة البيانات هنا تعرض لك خطة التنفيذ (Execution Plan)، والتي تبين:
إذا لاحظت أن الاستعلام لا يستخدم فهرس رغم أنك تتوقع ذلك، فغالبًا تحتاج لإنشاء فهرس جديد أو تعديل بنية الاستعلام.
استعلامات معقدة جدًا داخل filter() مع الكثير من Q() والشروط المتداخلة قد تولد SQL ثقيلًا. حاول دائمًا:
Lower(field)) إلا للضرورة لأنها تعيق الاستفادة من الفهارس التقليديةلشرح أوسع عن filter() يمكنك مراجعة: دليل شامل لفهم واستخدام دالة filter() في Django ORM.
حتى مع أفضل الاستعلامات، أحيانًا التنفيذ المتكرر لنفس الاستعلام مكلف. هنا يأتي دور:
تذكر أن QuerySet في Django كسول (lazy) ويتم تنفيذه عند الحاجة فقط. لكن بمجرد تقييمه، يمكنك إعادة استخدامه:
books_qs = Book.objects.filter(is_published=True).select_related('author')
# التقييم لأول مرة
books_list = list(books_qs)
# إعادة الاستخدام بدون استعلام جديد
for book in books_list:
...
for book in books_list:
...
المهم: لا تعيد استدعاء Book.objects.filter(...) مرات عديدة بنفس الشروط داخل نفس الطلب.
الترتيب order_by عملية مكلفة إذا لم يكن هناك فهرس يدعمها، لأن قاعدة البيانات قد تضطر لفرز كامل الجدول. عند استخدام order_by() بشكل متكرر، تأكد أن الحقول المستخدمة مفهرسة.
يمكنك مراجعة مقال: تعلم طرق استخدام order_by في إطار عمل Django لفهم مزيد من تفاصيل الترتيب في ORM.
موديلات مبسطة:
class Author(models.Model):
name = models.CharField(max_length=200)
class Category(models.Model):
name = models.CharField(max_length=200)
class Article(models.Model):
title = models.CharField(max_length=200, db_index=True)
author = models.ForeignKey(Author, on_delete=models.CASCADE)
categories = models.ManyToManyField(Category)
published_at = models.DateTimeField(db_index=True)
الإصدار البطيء:
def article_list(request):
articles = Article.objects.filter(is_published=True).order_by('-published_at')
data = []
for article in articles:
data.append({
'title': article.title,
'author': article.author.name, # N+1
'categories': [c.name for c in article.categories.all()], # N+1
})
الإصدار المحسن:
def article_list(request):
articles = (
Article.objects
.filter(is_published=True)
.select_related('author')
.prefetch_related('categories')
.only('id', 'title', 'author__name', 'published_at')
.order_by('-published_at')
)
data = []
for article in articles:
data.append({
'title': article.title,
'author': article.author.name,
'categories': [c.name for c in article.categories.all()],
})
title و published_at لتسريع الترتيب والبحثالاستعلام:
qs = Article.objects.filter(title__icontains='django').order_by('-published_at')
print(qs.explain())
إذا أظهر explain أن هناك Seq Scan بدون استخدام Index، فكر في:
titleicontains العادية.بهذه الممارسات يمكنك الانتقال من تطبيق Django بطيء يرسل مئات الاستعلامات في كل صفحة، إلى تطبيق محسّن يستخدم أقل عدد ممكن من الاستعلامات، مستفيدًا من قوة الفهارس وخطة التنفيذ في قاعدة البيانات، مع المحافظة على سهولة ومرونة Django ORM.
لمن يريد تعميق فهمه لـ Django ككل، يمكن الرجوع إلى: دليل شامل حول إطار Django لبناء تطبيقات الويب و دليل استخدام Django ORM. هذه المعرفة التأسيسية ستجعل استراتيجيات تحسين الأداء أكثر وضوحًا وأسهل تطبيقًا في مشاريعك.
استراتيجيات لخفض عدد الاستعلامات وتسريع استجابة قواعد البيانات في Django: استخدام select_related, prefetch_related، الفهارس المخصصة، وتحليل الـ QuerySet بالأمثلة.
مساحة اعلانية