Django ORM da N+1 Muammosi: Saytni 10x Tezlashtirish

Muallif: Javohir Abdullayev · · Django

Django ORM da N+1 Muammosi: Saytni 10x Tezlashtirish

Real Loyihada Kutilmagan Muammo

Kichik va o'rta hajmdagi loyihalarda Django ORM dasturchilarga juda katta qulaylik beradi. SQL so'rovlarini qo'lda yozmasdan, bir nechta Python satrlari orqali ma'lumotlar bazasi bilan oson muloqot qilamiz. Ammo loyiha foydalanuvchilari ko'payib, ma'lumotlar hajmi oshganda kutilmagan sekinlashuvlar yuzaga keladi.

Yaqinda mijozimizning e-tijorat platformasida jiddiy muammo yuzaga keldi: asosiy katalog sahifasi atigi 50 ta mahsulotni ko'rsatishi kerak bo'lsa-da, yuklanish vaqti 4.8 soniyagacha cho'zilib ketayotgan edi. Server protsessori (CPU) muntazam 90-95% yuklama bilan ishlay boshladi. Muammoni tahlil qilganimizda asosiy aybdor aynan N+1 so'rovlar muammosi ekanligi aniqlandi.

N+1 Muammosi O'zi Nima?

N+1 muammosi — dastur bitta asosiy obyektlar ro'yxatini olish uchun 1 ta SQL so'rov, so'ngra ro'yxatdagi har bir elementning bog'liq ma'lumotlari uchun alohida N ta qo'shimcha so'rov yuborganida paydo bo'ladi. Natijada umumiy so'rovlar soni N + 1 taga yetadi.

Keling, sodda model misolida ko'ramiz:

# models.py
from django.db import models

class Category(models.Model):
    name = models.CharField(max_length=100)

class Product(models.Model):
    title = models.CharField(max_length=200)
    price = models.DecimalField(max_digits=10, decimal_places=2)
    category = models.ForeignKey(Category, on_delete=models.CASCADE)
    tags = models.ManyToManyField('Tag')

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

Dasturchi mahsulotlar ro'yxatini API yoki shablonga chiqarish uchun quyidagicha kod yozadi:

# views.py (Yomon amaliyot)
products = Product.objects.all()[:50]

for product in products:
    print(product.category.name)  # Har bir qadamda yangi SQL so'rov!

Bu yerda nima sodir bo'ladi? Avval 50 ta mahsulotni olish uchun 1 ta so'rov yuboriladi. Keyin har bir mahsulotning kategoriyasini o'qish paytida Django bazaga yana 50 ta alohida so'rov jo'natadi. Agar sahifada teglar ham chiqarilsa, so'rovlar soni bir zumda 150-200 tadan oshib ketadi.

Muammoni Qanday Aniqladik?

Ushbu holatni vizual ko'rish va isbotlash uchun loyihaga Django Debug Toolbar vositasini uladik. Natija hayratlanarli edi: bitta sahifani yuklash uchun ma'lumotlar bazasiga 254 ta deyarli bir xil SQL so'rov yuborilayotgan edi. Aynan shu sababli sahifa ochilishi bir necha soniyani talab qilgan.

Yechim: select_related va prefetch_related

Django ORM bunday vaziyatlar uchun ikkita kuchli optimallashtirish qurolini taqdim etadi: select_related() va prefetch_related(). Ular bir-biridan ishlash mexanizmi bilan farq qiladi.

1. select_related() — ForeignKey va OneToOne Uchun

select_related() SQL darajasida JOIN operatorini ishlatadi. Bu bitta SQL so'rovining o'zida asosiy jadval va unga bog'langan ForeignKey yoki OneToOneField jadvallarini birlashtirib olib keladi.

# Optimallashgan variant
products = Product.objects.select_related('category').all()[:50]

for product in products:
    print(product.category.name)  # Baza bilan qo'shimcha so'rov yo'q!

Endi 50 ta so'rov o'rniga faqat 1 ta yagona SQL JOIN so'rovi bajariladi.

2. prefetch_related() — ManyToMany va Teskari Bog'lanishlar Uchun

Ko'pdan-ko'pga (ManyToMany) yoki teskari ForeignKey bog'lanishlarida JOIN ishlatish juda og'ir bo'lishi mumkin. prefetch_related() esa masalani boshqacha hal qiladi: u har bir jadval uchun alohida so'rov jo'natadi va natijalarni Python xotirasida o'zaro bog'laydi.

# ManyToMany va ForeignKey birgalikda
products = Product.objects.select_related('category').prefetch_related('tags').all()[:50]

Bu holda bor-yo'g'i 2 ta SQL so'rov bajariladi: biri mahsulotlar va ularning kategoriyalari uchun, ikkinchisi esa ushbu mahsulotlarga tegishli barcha teglar uchun (WHERE product_id IN (...) sharti orqali).

3. Murakkab Filtrlash: Prefetch Obyekti

Ba'zan faol bo'lgan yoki saralangan bog'liq obyektlarni yuklash talab etiladi. Buning uchun Prefetch obyektidan foydalanish eng to'g'ri yo'l hisoblanadi:

from django.db.models import Prefetch

active_tags_prefetch = Prefetch(
    'tags',
    queryset=Tag.objects.filter(is_active=True)
)

products = Product.objects.select_related('category').prefetch_related(active_tags_prefetch)

Case Study Natijalari

Ushbu o'zgarishlarni kiritgach va shablon ichidagi yashirin chaqiruvlarni bartaraf etgach, loyihada quyidagi natijalarga erishdik:

  • SQL so'rovlar soni: 254 tadan 3 tagacha qisqardi.
  • Sahifaning yuklanish vaqti: 4.8 soniyadan 230 millisekundgacha tushdi (20x dan ortiq tezlashuv).
  • Server yuki (CPU): Eng tig'iz vaqtda 90% dan 15-20% gacha kamaydi.
  • Foydalanuvchi tajribasi: Mahsulot katalogi bir zumda ochiladigan bo'ldi.

Xulosa

Django ORM qulay vosita, lekin uning qanday SQL hosil qilayotganini tushunmaslik loyihani og'irlashtirib qo'yishi mumkin. Agar sizning loyihangizda ham sahifalar sekin ishlayotgan bo'lsa, birinchi navbatda SQL so'rovlar sonini tekshiring. select_related va prefetch_related metodlarini o'z o'rnida qo'llash orqali siz dasturingiz tezligini bir necha barobar oshirishingiz mumkin.

Teglar: #Django #Django ORM #Backend Optimallashtirish #Python