N+1 so‘rovlar muammosi: qanday topish va tuzatish
Amaliyot3 daqInnovialabs tahririyati
Qisqacha
N+1 — kod ro‘yxat uchun bitta so‘rov va har bir element uchun yana bittadan so‘rov yuborishi; bog‘liq ma’lumotlarni eager loading, JOIN yoki batching orqali bir-ikki so‘rovda oldindan yuklash bilan tuzatiladi.
N+1 nima va nega bu muammo
N+1 — ilova yozuvlar ro‘yxati uchun 1 ta so‘rov yuborib, keyin bog‘liq ma’lumotlarni olish uchun har bir yozuvga bittadan, ya’ni N ta alohida so‘rov yuboradigan holat.
Misol: 100 ta postli sahifa, har birida muallif ko‘rsatiladi. Kod postlarni bitta so‘rovda oladi, keyin siklda har bir post uchun muallifni so‘raydi. Natijada 1–2 ta o‘rniga 101 ta so‘rov.
O‘nta yozuvli lokal bazada buni sezmaysiz. Baza boshqa serverda turgan va har bir so‘rov tarmoq orqali borib-kelishni talab qiladigan prodakshnda sahifa sekinlashadi, bazaga yuklama esa ro‘yxat hajmi bilan birga o‘sadi.
ORM’lar N+1’ni qanday sezdirmay yaratadi
ORM’ning qulayligi shundaki, bog‘lanishlar obyektning oddiy maydonlaridek ko‘rinadi. Lekin bunday maydonga murojaat so‘rov bajarishi mumkin.
Django:
posts = Post.objects.all()[:100]
for post in posts:
print(post.author.name) # har iteratsiyada bazaga so‘rov
Laravel Eloquent:
$posts = Post::take(100)->get();
foreach ($posts as $post) {
echo $post->author->name; // lazy loading: har bir post uchun so‘rov
}
Prisma va TypeORM bog‘lanishlarni o‘zi yuklamaydi, shuning uchun u yerda N+1 ko‘pincha ochiq ko‘rinishda paydo bo‘ladi — so‘rov sikl ichida yoki ro‘yxat bo‘yicha Promise.all ichida yozilganda:
const posts = await prisma.post.findMany({ take: 100 });
const result = await Promise.all(
posts.map((p) => prisma.user.findUnique({ where: { id: p.authorId } }))
);
N+1’ning alohida manbai — GraphQL: author maydoni rezolveri har bir post uchun alohida chaqiriladi.
N+1’ni qanday topish mumkin
- SQL loglari. Dev muhitida so‘rovlarni loglashni yoqing. N+1 belgisi — faqat
idparametri bilan farq qiladigan bir xil so‘rovlar seriyasi. - APM va treysing. So‘rov treysida N+1 bitta HTTP-so‘rov ichidagi o‘nlab bir xil qisqa baza spanlaridan iborat «zinapoya» bo‘lib ko‘rinadi.
- Freymvork vositalari. Django Debug Toolbar so‘rovlar soni va takrorlarni ko‘rsatadi. Laravel’da dev rejimida
Model::preventLazyLoading()ni yoqish mumkin — shunda lazy loading istisno chiqaradi. - So‘rovlar soniga testlar. Masalan, Django’dagi
assertNumQueries: endpoint kutilganidan ko‘p so‘rov yuborsa, test yiqiladi.
Qanday tuzatish kerak
Eager loading
ORM’ga qaysi bog‘lanishlar kerakligini oldindan ayting:
| ORM | Yechim |
|---|---|
| Django | ForeignKey uchun select_related("author") (JOIN), M2M va teskari bog‘lanishlar uchun prefetch_related("tags") (IN bilan alohida so‘rov) |
| Laravel Eloquent | Post::with('author')->take(100)->get() |
| Prisma | prisma.post.findMany({ take: 100, include: { author: true } }) |
| TypeORM | find({ relations: { author: true } }) yoki QueryBuilder’da leftJoinAndSelect |
Oldin: postlar uchun 1 ta + mualliflar uchun 100 ta = 101 ta so‘rov.
Keyin: 2 ta so‘rov (postlar + IN orqali mualliflar) yoki JOIN bilan 1 ta so‘rov.
Qo‘lda JOIN
Agar bir-ikki maydon kerak bo‘lsa, bitta so‘rov yozish osonroq:
SELECT p.id, p.title, u.name AS author_name
FROM posts p
JOIN users u ON u.id = p.author_id
ORDER BY p.created_at DESC
LIMIT 100;
Batching va DataLoader
Bog‘lanishlar kodning turli joylaridan (GraphQL rezolverlari, servislar) yuklanganda DataLoader ishlatiladi: u bitta tik davomida so‘ralgan barcha idlarni yig‘ib, bitta WHERE id IN (...) so‘rovini bajaradi.
const userLoader = new DataLoader(async (ids: readonly number[]) => {
const users = await prisma.user.findMany({ where: { id: { in: [...ids] } } });
const byId = new Map(users.map((u) => [u.id, u]));
return ids.map((id) => byId.get(id) ?? null);
});
Muhim: DataLoader natijalarni ids bilan bir xil tartibda qaytarishi shart. Kesh turli foydalanuvchilar ma’lumotlarini aralashtirmasligi uchun har bir kiruvchi so‘rovga yangi nusxa yarating.
Tuzatishdagi ko‘p uchraydigan xatolar
- Hammasini yuklab olish. Besh darajali
includeortiqcha ma’lumotlarni tortadi — bu boshqa samaradorlik muammosi. - Paginatsiya bilan «birga-ko‘p» bog‘lanishda JOIN. Qatorlar takrorlanadi va
LIMITpostlarni emas, natija qatorlarini kesadi. Bunday bog‘lanishlar uchunINbilan alohida so‘rov yaxshiroq. - Bitta endpointni tuzatib, qolganlarini unutish. So‘rovlar soniga testlarsiz N+1 keyingi o‘zgarishda qaytib keladi.