تحسين سرعة Django ORM: تجنب N+1 Queries واستخدام Select Related

تحسين ORM Django: تجنب N+1 Queries واستخدام select_related وprefetch_related

تحسين ORM Django ليس مجرد ترف برمجي، بل خطوة أساسية إذا كنت تبني تطبيقات حقيقية تحتاج لأداء عالٍ واستجابة سريعة. كثير من المطورين يبدأون باستخدام Django ORM بسهولة، لكن مع نمو المشروع تظهر مشاكل بطء مفاجئة، وغالبًا السبب يكون في ما يُعرف بمشكلة N+1 Queries.

في هذا المقال سنشرح بشكل عملي:

  • ما هي مشكلة N+1 Queries ولماذا تجعل تطبيقك بطيئًا؟
  • كيف تستخدم select_related وprefetch_related لتحسين ORM Django جذريًا.
  • أمثلة حقيقية قبل/بعد مع قياس عدد الاستعلامات.
  • أفضل الممارسات لتجنب الوقوع في مشاكل الأداء مستقبلاً.

إذا كنت جديدًا على Django ORM، من المفيد الرجوع أولًا إلى دليل استخدام Django ORM، ثم العودة لهذا المقال للتعمق في تحسين الأداء.

ما هي مشكلة N+1 Queries في Django ORM؟

مشكلة N+1 Queries تحدث عندما تقوم بتحميل قائمة من الكائنات من قاعدة البيانات، ثم لكل كائن تقوم بطلب استعلام إضافي لجلب بيانات مرتبطة به. النتيجة: عدد ضخم من الاستعلامات الصغيرة بدلًا من عدد قليل من الاستعلامات الذكية.

مثال بسيط يوضّح المشكلة

افترض أن لدينا نموذجين:


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)

وفي view تقوم بجلب كل الكتب وعرض اسم الكاتب:


def books_list(request):
    books = Book.objects.all()
    for book in books:
        print(book.title, book.author.name)

ما يحدث بالفعل في الخلفية:

  • استعلام واحد لجلب جميع الكتب: 1 Query
  • لكل كتاب، استعلام إضافي لجلب الكاتب المرتبط: N Queries

النتيجة: N+1 Queries. لو عندك 500 كتاب، سيكون لديك 501 استعلام إلى قاعدة البيانات! في بيئة Production ومع ضغط عدد المستخدمين، هذا كفيل بقتل أداء التطبيق.

كيف نكتشف N+1 Queries في مشروع Django؟

أول خطوة في تحسين ORM Django هي كشف هذه المشاكل. هناك أكثر من أداة تساعدك:

  • Django Debug Toolbar: تعرض عدد الاستعلامات لكل صفحة والزمن المستغرق لكل استعلام.
  • الاستفادة من خاصية .query في QuerySet أثناء التطوير.

استخدام Django Debug Toolbar

بعد تثبيت وتفعيل Django Debug Toolbar، افتح صفحة تعرض قائمة بيانات كبيرة (مثل قائمة الكتب، المقالات، الطلبات...)، ثم:

  1. انتقل إلى تبويب SQL في الـ Toolbar.
  2. راجع عدد الاستعلامات: إذا وجدت 100+ استعلام لصفحة بسيطة، فهذه علامة على وجود N+1 Queries.
  3. افتح بعض الاستعلامات المتكررة، غالبًا ستجد استعلامًا مشابهًا يتكرر لكل صف/عنصر.

للتعمق أكثر في تحسين الاستعلامات، يمكنك الاطلاع أيضًا على مقال: تحسين استعلامات Django ORM لأداء أعلى.

select_related: الحل المثالي للعلاقات ForeignKey وOneToOne

دالة select_related تُستخدم لتحميل العلاقات من نوع ForeignKey أو OneToOne في نفس الاستعلام باستخدام JOIN على مستوى قاعدة البيانات. هذا يقلل عدد الاستعلامات بشكل كبير.

إعادة كتابة المثال السابق باستخدام select_related


def books_list(request):
    books = Book.objects.select_related('author').all()
    for book in books:
        print(book.title, book.author.name)

الآن ما يحدث في الخلفية:

  • استعلام واحد فقط يجلب الكتب ومعها الكاتب باستخدام JOIN.
  • لا توجد استعلامات إضافية عند الوصول إلى book.author.

بدل 501 استعلام، أصبح لدينا استعلام واحد فقط.

متى نستخدم select_related؟

  • عندما تكون العلاقة Book → Author من نوع ForeignKey أو OneToOneField.
  • عندما تكون جهة الوصول من الكائن إلى العلاقة (مثال: book.author)، وليس العكس.
  • عندما تحتاج هذه العلاقات في نفس الصفحة أو الـ API Response.

أمثلة أخرى على select_related

افترض النماذج التالية:


class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    avatar = models.ImageField()

class Order(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    address = models.ForeignKey('Address', on_delete=models.SET_NULL, null=True)

سيناريوهات استخدام:


# جلب بروفايل المستخدم مع بيانات الـ User
profile = Profile.objects.select_related('user').get(pk=1)

# جلب الطلبات مع المستخدم والعنوان في استعلام واحد
orders = Order.objects.select_related('user', 'address').all()

prefetch_related: للعلاقات ManyToMany وReverse ForeignKey

على عكس select_related، دالة prefetch_related تعمل بطريقة مختلفة: تقوم بتنفيذ عدة استعلامات منفصلة لكنها تقوم بدمج النتائج في الذاكرة، بطريقة تمنع تكرار الاستعلام لكل عنصر.

تُستخدم مع:

  • العلاقات ManyToMany.
  • العلاقات العكسية (مثل author.book_set أو related_name).

مثال على prefetch_related مع ManyToMany

افترض النماذج التالية:


class Tag(models.Model):
    name = models.CharField(max_length=50)

class Article(models.Model):
    title = models.CharField(max_length=255)
    tags = models.ManyToManyField(Tag, related_name='articles')

View سيئ الأداء:


def articles_list(request):
    articles = Article.objects.all()
    for article in articles:
        print(article.title)
        for tag in article.tags.all():
            print(tag.name)

هنا يحدث التالي:

  • استعلام لجلب جميع المقالات.
  • لكل مقال، استعلام لجلب الـ tags الخاصة به.

النتيجة مرة أخرى: N+1 Queries.

إصلاح المثال باستخدام prefetch_related


def articles_list(request):
    articles = Article.objects.prefetch_related('tags').all()
    for article in articles:
        print(article.title)
        for tag in article.tags.all():
            print(tag.name)

الآن ما يحدث في الخلفية:

  • استعلام لجلب جميع المقالات.
  • استعلام واحد إضافي لجلب جميع الـ tags المرتبطة بهذه المقالات.
  • لا توجد استعلامات إضافية عند استدعاء article.tags.all() داخل الحلقة.

أيًا كان عدد المقالات، سيظل عدد الاستعلامات ثابتًا (2 فقط).

prefetch_related مع العلاقات العكسية (Reverse FK)

بالعودة لمثال الكتب والكتّاب:


class Author(models.Model):
    name = models.CharField(max_length=255)

class Book(models.Model):
    title = models.CharField(max_length=255)
    author = models.ForeignKey(Author, related_name='books', on_delete=models.CASCADE)

نريد عرض المؤلفين مع جميع كتبهم:


def authors_list(request):
    authors = Author.objects.all()
    for author in authors:
        print(author.name)
        for book in author.books.all():
            print(book.title)

هذا يؤدي إلى N+1 Queries. الحل:


def authors_list(request):
    authors = Author.objects.prefetch_related('books').all()
    for author in authors:
        print(author.name)
        for book in author.books.all():
            print(book.title)

مرة أخرى، سيقوم Django بتنفيذ استعلامين فقط: واحد للكتّاب وآخر للكتب، ثم يربط النتائج في الذاكرة.

select_related أم prefetch_related؟ متى أختار أيهما؟

قاعدة بسيطة لتحسين ORM Django بشكل صحيح:

  • استخدم select_related مع:
    • ForeignKey
    • OneToOneField
  • استخدم prefetch_related مع:
    • ManyToManyField
    • العلاقات العكسية (Reverse relations / related_name)
    • عندما تحتاج تخصيص QuerySet للعلاقة، مثل استخدام Prefetch مع شروط filter.

ولا تنسَ أنه يمكنك الجمع بينهما في نفس الاستعلام:


books = (
    Book.objects
    .select_related('author', 'publisher')
    .prefetch_related('tags', 'reviews')
)

أمثلة حقيقية لتحسين ORM Django في تطبيقات إنتاجية

سيناريو 1: صفحة قائمة الطلبات في لوحة تحكم متجر إلكتروني

النماذج المبسّطة:


class Customer(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)

class Order(models.Model):
    customer = models.ForeignKey(Customer, on_delete=models.CASCADE)
    address = models.ForeignKey('Address', on_delete=models.SET_NULL, null=True)

class OrderItem(models.Model):
    order = models.ForeignKey(Order, related_name='items', on_delete=models.CASCADE)
    product = models.ForeignKey('Product', on_delete=models.CASCADE)

الـ view الأول (البطيء):


def admin_orders_list(request):
    orders = Order.objects.all()
    for order in orders:
        print(order.customer.user.username)
        print(order.address.city)
        for item in order.items.all():
            print(item.product.name)

المشكلة:

  • استعلامات متكررة لجلب customer و address.
  • استعلامات متكررة لجلب items لكل طلب.
  • استعلامات متكررة لجلب product لكل عنصر.

التحسين باستخدام select_related وprefetch_related


from django.db.models import Prefetch

def admin_orders_list(request):
    orders = (
        Order.objects
        .select_related('customer__user', 'address')
        .prefetch_related(
            Prefetch(
                'items',
                queryset=OrderItem.objects.select_related('product')
            )
        )
    )

    for order in orders:
        print(order.customer.user.username)
        print(order.address.city if order.address else '')
        for item in order.items.all():
            print(item.product.name)

النتيجة:

  • تقليل عدد الاستعلامات من عشرات أو مئات إلى عدد ثابت صغير.
  • تحسين كبير في زمن استجابة الصفحة.

أخطاء شائعة عند تحسين ORM Django

1. الإفراط في استخدام select_related على علاقات عميقة جدًا

مثال:


qs = Order.objects.select_related(
    'customer__user__profile__company__country'
)

كلما زادت عمق العلاقات، كبر حجم الـ JOIN وتعقّد الاستعلام، وقد يصبح أبطأ من عدة استعلامات منفصلة. جرّب، وقِس الأداء قبل وبعد.

2. استخدام prefetch_related على QuerySet ضخم جدًا بلا حاجة

إذا كنت تجلب آلاف الصفوف مع علاقاتها مرة واحدة، قد تستهلك الكثير من الذاكرة. في هذه الحالة:

  • استخدم pagination (تجزئة النتائج على صفحات).
  • اجلب الحقول الضرورية فقط باستخدام only() أو values() عند الحاجة.

3. نسيان الفهارس (Indexes) في قاعدة البيانات

حتى مع أفضل تحسينات ORM، إذا كانت الجداول بلا فهارس مناسبة، ستبقى الاستعلامات بطيئة. راجع مقال: أهم خوارزميات الفهرسة المستخدمة في قواعد البيانات، وراجع بنية الجداول خاصة مع PostgreSQL كما شرحنا في: ربط Django مع PostgreSQL بطريقة احترافية.

نصائح عملية لتحسين ORM Django على المدى الطويل

  • راقب عدد الاستعلامات مبكرًا: استخدم Django Debug Toolbar من بداية المشروع، ولا تنتظر حتى مرحلة الإنتاج.
  • اعزل طبقة الوصول للبيانات: أنشئ دوال/Services مسؤولة عن جلب البيانات مع select_related/prefetch_related بدلًا من تكرار المنطق في كل View.
  • اكتب Tests لعدد الاستعلامات: في المشاريع الكبيرة، يمكنك كتابة اختبارات تتحقق من ألا تتجاوز بعض الـ views عددًا معينًا من الاستعلامات.
  • تعلّم قراءة SQL الذي ينتجه ORM: استخدم str(queryset.query) لفهم الاستعلامات وتحسينها أو إعادة كتابة الـ QuerySet.
  • احذر من الوصول الكسول (Lazy loading) داخل الحلقات: إذا وجدت نفسك تستخدم نقطة (.) داخل Loop للوصول إلى علاقات أخرى، فاسأل نفسك: هل أحتاج select_related أو prefetch_related هنا؟

خلاصة: تحسين ORM Django يبدأ من فهم الاستعلامات

تحسين ORM Django لا يعني فقط استخدام دوال سحرية مثل select_related وprefetch_related، بل يعني فهم كيف يعمل ORM تحت الغطاء، وكيف تُترجَم أوامرك إلى SQL فعلي.

باختصار:

  • تجنب N+1 Queries قدر الإمكان، فهي العدو الأول لأداء Django.
  • استخدم select_related لعلاقات ForeignKey/OneToOne من جهة الكائن.
  • استخدم prefetch_related لعلاقات ManyToMany والعلاقات العكسية.
  • راقب عدد الاستعلامات دائمًا، ولا تفترض أن ORM يقوم بكل شيء بشكل مثالي دون تدخل منك.

إذا أردت فهم أعمق لكيفية بناء النماذج في Django 5 لتسهيل تحسين الأداء لاحقًا، راجع أيضًا: دليلك الشامل إلى Django 5 Models. بهذه الأدوات والخبرات، ستتمكن من بناء تطبيقات Django سريعة وقابلة للتوسع دون مفاجآت مزعجة في الأداء.

حول المحتوى:

شرح أمثلة حقيقية لكيف يؤدي ORM إلى بطء التطبيق، وكيفية تحسين الاستعلامات بشكل جذري.

هل كان هذا مفيدًا لك؟

أضف تعليقك