Mundarija (25)
- Bu darsda
- 1. Nega bu kerak?
- 2. Muammoni sanash
- 2.1 Soxta baza bilan
- 2.2 Haqiqiy Postgres bilan: kanon o'lchovi
- 3. Yechim 1: batch — = ANY($1)
- 3.1 G'oya
- 3.2 SQL tomoni
- 4. Yechim 2: bitta so'rov
- 4.1 JOIN va uning tuzog'i
- 4.2 Subquery + json_agg
- 4.3 Qaysi biri yaxshi
- 5. DataLoader g'oyasi
- 5.1 Muammo: kod tuzilmasi
- 5.2 ORM'larda
- 6. N+1 ni qanday aniqlash
- 7. Node qadami: API ro'yxatida N+1 yo'q
- 8. Ko'p uchraydigan xatolar
- 9. Mashqlar
- 1-mashq (oson): to'ldiring
- 2-mashq (o'rta): N+1 ni SQL bilan almashtiring
- 3-mashq (qiyin): mini DataLoader
- 10. Real ishda
- Xulosa
- Manbalar
N+1 so'rov muammosi: aniqlash va JOIN, ANY va DataLoader bilan tuzatish
Qisqacha: N+1 — ro'yxatni bitta so'rov bilan olib, keyin har element uchun yana bitta so'rov yuborish. 50 ta vazifa uchun 1 + 50 + 50 = 101 so'rov. Har so'rov kichik bo'lsa ham, har biri tarmoq aylanishi va bazada rejalashtirish vaqtini oladi. Kanonda 101 so'rov 152 ms oldi;
WHERE vazifa_id = ANY($1)bilan 3 so'rov — 5 ms; bitta so'rov (json_agg) — ham 5 ms. Natija uchala usulda bir xil.
Bu darsda
- N+1 muammosini kodda va so'rovlar sonida taniysiz.
- So'rovlarni sanab, muammoni o'lchov bilan isbotlaysiz.
= ANY($1)bilan batch yuklashni vajson_aggbilan bitta so'rovni yozasiz.- DataLoader g'oyasini — bitta "tick" ichidagi so'rovlarni yig'ishni — o'zingiz amalga oshirasiz.
- Qaysi yechim qachon to'g'ri ekanini tanlaysiz.
Oldin bilishingiz kerak: Sekin so'rovlarni topish, JOIN'lar, node-postgres (pg) asoslari, Parametrli so'rovlar va SQL injection, Agregatlar, GROUP BY va HAVING.
1. Nega bu kerak?
vazifalar ilovasining bosh sahifasi: Azizning 50 ta vazifasi, har birining yonida izohlari va teglari. Birinchi yozilgan kod odatda shunday ko'rinadi:
const vazifalar = await royxatniOl(egasiId); // 1 so'rov
for (const v of vazifalar) {
v.izohlar = await izohlarniOl(v.id); // 50 so'rov
v.teglar = await teglarniOl(v.id); // 50 so'rov
}Mantiqan to'g'ri, o'qish oson, testlar o'tadi. Lokal kompyuterda tez ishlaydi. Production'da esa sekin. Nega?
O'xshatish: bozorga 50 xil mahsulot uchun ro'yxat bilan borish o'rniga, har mahsulot uchun uydan alohida chiqib, bozorga borib, qaytib kelish. Har safar sotib olish 1 daqiqa — lekin yo'l 20 daqiqa. 50 × 21 = 17.5 soat. Ro'yxat bilan bitta borishda — 70 daqiqa.
Bazada "yo'l" — tarmoq aylanishi (round trip): so'rov serverga boradi, javob qaytadi. Kanon o'lchovida (Docker Desktop, bitta kompyuterda) har aylanish ~1 ms edi. Server boshqa ma'lumotlar markazida bo'lsa — 5–20 ms.
Bu muammoning nomi — N+1: 1 ta ro'yxat so'rovi + N ta element so'rovi. Bizda izoh va teg uchun ikki marta N — 2N+1, lekin nomi o'zgarmaydi.
Oldingi darsda pg_stat_statements ni o'rgandik. N+1 u yerda "sekin so'rov" bo'lib ko'rinmaydi — har bir so'rov 0.1 ms. Ko'rinadigan narsa — calls ustunidagi ulkan son.
2. Muammoni sanash
2.1 Soxta baza bilan
Avval muammoni bazasiz ko'ramiz. Har chaqiruvni sanaydigan "baza" funksiyasi:
// "Baza" o'rnida — har chaqiruvni sanaydigan soxta funksiyalar
let sorovlar = 0;
const vazifalar = Array.from(
{ length: 50 },
(_, i) => ({ id: i + 1 }),
);
const izohlar = [
{ vazifa_id: 3, matn: "Zo'r" },
{ vazifa_id: 7, matn: "?" },
];
async function sorov(filtr) {
sorovlar++;
return izohlar.filter(filtr);
}
// N+1: har vazifa uchun alohida so'rov
sorovlar = 1; // ro'yxatning o'zi
for (const v of vazifalar) {
v.izohlar = await sorov((i) => i.vazifa_id === v.id);
}
console.log("N+1:", sorovlar, "so'rov");
// Batch: hamma id'lar bitta so'rovda (= ANY($1))
sorovlar = 1;
const idlar = new Set(vazifalar.map((v) => v.id));
const hammasi = await sorov((i) => idlar.has(i.vazifa_id));
for (const v of vazifalar) {
v.izohlar = hammasi.filter((i) => i.vazifa_id === v.id);
}
console.log("Batch:", sorovlar, "so'rov");
console.log(vazifalar[2].izohlar);Konsolda:
N+1: 51 so'rov
Batch: 2 so'rov
[ { vazifa_id: 3, matn: "Zo'r" } ]Natija bir xil, so'rovlar soni 25 marta kam. Tuzatishning mohiyati shu: "har biri uchun bittadan" o'rniga "hammasi uchun bitta", keyin JavaScript'da guruhlash.
2.2 Haqiqiy Postgres bilan: kanon o'lchovi
Kanon tajribasi katta bazada (2 mln vazifa) egasi 77 ning 50 ta vazifasini izohlari va teglari bilan oldi — uch usulda, har birini 7 marta, mediana. So'rovlar soni pg_stat_statements siz, oddiy hisoblagich bilan sanaldi: pool.query ni o'rab, har chaqiruvda sanoq++.
N+1 varianti (kanon kodidan, qatorlar telefonga moslab bo'lingan):
import pg from "pg";
const p = new pg.Pool({ connectionString: URL_("katta"), max: 5 });
let sanoq = 0;
const q = (text, values) => {
sanoq++;
return p.query(text, values);
};
const EGASI = 77, LIMIT = 50;
async function n1() { // 1 + N + N
const { rows } = await q(
`SELECT id, matn FROM faol_vazifalar WHERE egasi_id = $1
ORDER BY yaratilgan_vaqt DESC, id DESC LIMIT $2`,
[EGASI, LIMIT]);
for (const v of rows) {
v.izohlar = (await q(
`SELECT matn FROM izohlar WHERE vazifa_id = $1
ORDER BY yaratilgan_vaqt`, [v.id])).rows;
v.teglar = (await q(
`SELECT t.nom FROM vazifa_teglari vt
JOIN teglar t ON t.id = vt.teg_id
WHERE vt.vazifa_id = $1`, [v.id])).rows.map((r) => r.nom);
}
return rows;
}URL_ — kanon tajribalarining yordamchi funksiyasi (lokal trust ulanish satrini qaytaradi). faol_vazifalar — Savatda bo'lmagan vazifalar ko'rinishi (soft delete darsi).
Natija (Node 24.21.0, pg 8.23.1, PostgreSQL 18.6 Docker'da):
N+1 so'rovlar 101, mediana 152.0 ms, natija N+1 bilan bir xil: true
batch (= ANY) so'rovlar 3, mediana 5.0 ms, natija N+1 bilan bir xil: true
bitta so'rov (subquery + json_agg) so'rovlar 1, mediana 5.0 ms, natija N+1 bilan bir xil: true
izoh: har so'rov ~1 ms tarmoq aylanishi (Docker Desktop, 127.0.0.1) — N+1 narxi asosan shu; uzoq server (masalan 20 ms) bilan 101 so'rov ≈ 2 s- N+1 (101 so'rov)152 ms
- Batch = ANY (3 so'rov)5 ms
- Bitta so'rov (1)5 ms
Manba: IlmHamroh kanon o'lchovi, 2026-10-24: Node 24.21.0, pg 8.23.1, PostgreSQL 18.6 (Docker, WSL2), 2 mln vazifa; 7 marta, mediana
30 marta tez. Va bu eng yaxshi holat — baza shu kompyuterda. Oxirgi qatordagi hisob: server 20 ms uzoqlikda bo'lsa, 101 so'rov ≈ 2 soniya. Bu hisob, kanon uni o'lchamagan (uzoq server sinalmagan).
Tekshirib ko'ring: Sahifada 20 ta buyurtma, har birida mijoz ismi, mahsulotlar ro'yxati va yetkazib berish manzili alohida so'rov bilan olinadi. Nechta so'rov ketadi?
Javob
1 + 20 × 3 = 61. Ro'yxat bitta, har buyurtma uchun uchta. 20 ta emas, 100 ta buyurtma ko'rsatilsa — 301. N+1 sahifadagi element soni bilan chiziqli o'sadi; batch esa doim 4 (ro'yxat + uch toifa).
3. Yechim 1: batch — = ANY($1)
3.1 G'oya
Avval ro'yxatni olamiz, undan id'larni yig'amiz va bitta so'rov bilan hamma izohlarni olamiz. Parametrli so'rovlar darsida = ANY($1::bigint[]) ni ko'rgandik: massivni bitta parametr sifatida yuborish.
Kanon kodidagi batch varianti:
async function batch() { // 1 + 1 + 1
const { rows } = await q(
`SELECT id, matn FROM faol_vazifalar WHERE egasi_id = $1
ORDER BY yaratilgan_vaqt DESC, id DESC LIMIT $2`,
[EGASI, LIMIT]);
const idlar = rows.map((v) => v.id);
const [iz, tg] = await Promise.all([
q(`SELECT vazifa_id, matn FROM izohlar
WHERE vazifa_id = ANY($1::bigint[])
ORDER BY yaratilgan_vaqt`, [idlar]),
q(`SELECT vt.vazifa_id, t.nom FROM vazifa_teglari vt
JOIN teglar t ON t.id = vt.teg_id
WHERE vt.vazifa_id = ANY($1::bigint[])`, [idlar]),
]);
for (const v of rows) {
v.izohlar = iz.rows.filter((r) => r.vazifa_id === v.id)
.map(({ matn }) => ({ matn }));
v.teglar = tg.rows.filter((r) => r.vazifa_id === v.id)
.map((r) => r.nom);
}
return rows;
}Uch nuqta:
Promise.all— izohlar va teglar so'rovlari bir vaqtda ketadi (pool'dan ikki ulanish). Ikkalasi bir-biriga bog'liq emas.filterbilan guruhlash — JavaScript'da. 50 vazifa uchun bu arzon. Katta ro'yxatdaMapbilan guruhlash tezroq (2-mashqda).v.idvar.vazifa_idikkalasi ham string —bigintustunlarpgda string bo'lib keladi (pg asoslari). Taqqoslash===ishlaydi, chunki ikkalasi ham bir xil turda. BirisetTypeParserbilan son, ikkinchisi string bo'lsa — hech narsa topilmaydi, xato ham chiqmaydi.
3.2 SQL tomoni
Biz ishga tushirdik (PostgreSQL 17.6, holat 40): Azizning to'rtta vazifasi uchun izohlar soni — bitta so'rov bilan:
SELECT vazifa_id, count(*) AS izohlar
FROM izohlar
WHERE vazifa_id = ANY ('{1,2,9,17}'::bigint[])
GROUP BY vazifa_id ORDER BY vazifa_id;Natija:
vazifa_id | izohlar
-----------+---------
1 | 3
2 | 1
(2 rows)9 va 17 yo'q — ularda izoh yo'q. Bu batch'ning bitta nozik joyi: izohsiz vazifa uchun qator kelmaydi. JavaScript'da filter bo'sh massiv beradi — to'g'ri. Lekin SQL'da "hamma id va 0" kerak bo'lsa — unnest + LEFT JOIN:
SELECT v.id, count(i.id) AS izohlar
FROM unnest('{1,2,9,17}'::bigint[]) AS v(id)
LEFT JOIN izohlar i ON i.vazifa_id = v.id
GROUP BY v.id ORDER BY v.id;Natija:
id | izohlar
----+---------
1 | 3
2 | 1
9 | 0
17 | 0
(4 rows)unnest massivni qatorlarga aylantiradi. count(i.id) — count(*) emas: LEFT JOIN'da mos izoh bo'lmasa i.id NULL, count uni sanamaydi (NULL mantiqi).
Diqqat: Massiv juda katta bo'lsa (o'n minglab id) — bitta parametr bo'lib qoladi,
$1, $2, …cheklovi (ommaviy ma'lumot darsida kanon 65 535 chegarasini topgan) bu yerda yo'q. Lekin bunday ro'yxatni bitta sahifada ko'rsatish o'zi muammo — sahifalash kerak (keyingi dars).
4. Yechim 2: bitta so'rov
4.1 JOIN va uning tuzog'i
Birinchi fikr — hammasini JOIN bilan olish:
SELECT v.id, v.matn, i.matn AS izoh, t.nom AS teg
FROM faol_vazifalar v
LEFT JOIN izohlar i ON i.vazifa_id = v.id
LEFT JOIN vazifa_teglari vt ON vt.vazifa_id = v.id
LEFT JOIN teglar t ON t.id = vt.teg_id
WHERE v.egasi_id = $1;Muammo: 3 izohli va 2 tegli vazifa 3 × 2 = 6 qator bo'lib keladi — izohlar va teglar bir-biriga ko'payadi (JOIN darsida ko'rgan dekart ko'paytma). JavaScript'da ularni qayta "yig'ish" va takrorlarni tashlash kerak. Ikki xil "ko'p" bog'lanishni bitta tekis JOIN bilan olmang.
4.2 Subquery + json_agg
To'g'ri yo'l — har vazifa uchun izohlarni ichki so'rovda massivga yig'ish (agregatlar darsi — json_agg, array_agg). Biz ishga tushirdik (PostgreSQL 17.6, holat 40):
SELECT v.id, left(v.matn, 25) AS matn,
coalesce((SELECT json_agg(i.matn ORDER BY i.yaratilgan_vaqt)
FROM izohlar i WHERE i.vazifa_id = v.id), '[]')
AS izohlar
FROM faol_vazifalar v
WHERE v.egasi_id = 1
ORDER BY v.id LIMIT 4;Natija:
id | matn | izohlar
----+---------------------------+-------------------------------------------------------------------------------------------------------------------
1 | JavaScript kursining 26-q | ["Docker darslaridan boshladim", "psql buyruqlari esda qolmayapti — \\? yordam beradi", "JOIN tushunarli bo'ldi"]
2 | Postgres'ni Docker'da ko' | ["Port 5432 band ekan, 5499 ga o'tdim"]
9 | Onamga dori olib borish # | []
17 | Portfolio saytiga blog yo | []
(4 rows)Har vazifa — bitta qator. Izohlar — tayyor JSON massivi. pg uni JavaScript massiviga o'zi aylantiradi. coalesce(…, '[]') — izohsiz vazifada json_agg NULL qaytaradi, biz uni bo'sh massivga almashtiramiz. \\? — psql JSON ichidagi teskari chiziqni qochirib ko'rsatgan (asl matnda \?). 8-vazifa yo'q — u Savatda.
Kanon varianti teglar uchun array_agg ham qo'shadi (1 so'rov, 5.0 ms):
async function bittaSorov() { // 1
const { rows } = await q(`SELECT v.id, v.matn,
coalesce((SELECT json_agg(json_build_object('matn', i.matn)
ORDER BY i.yaratilgan_vaqt)
FROM izohlar i WHERE i.vazifa_id = v.id), '[]')
AS izohlar,
coalesce((SELECT array_agg(t.nom) FROM vazifa_teglari vt
JOIN teglar t ON t.id = vt.teg_id
WHERE vt.vazifa_id = v.id), '{}') AS teglar
FROM faol_vazifalar v WHERE v.egasi_id = $1
ORDER BY v.yaratilgan_vaqt DESC, v.id DESC LIMIT $2`,
[EGASI, LIMIT]);
return rows;
}Bu yerdagi ichki so'rovlar ham har qator uchun bajariladi — N+1 ga o'xshaydi! Farqi: ular baza ichida, tarmoq aylanishisiz va bitta rejada. Indeks bo'lsa (izohlar (vazifa_id, yaratilgan_vaqt) — kanon sxemasida bor), har biri mikrosekundlar oladi. Natija: 1 so'rov, 5 ms.
4.3 Qaysi biri yaxshi
Batch (= ANY) |
Bitta so'rov | |
|---|---|---|
| So'rovlar | 1 + toifalar soni | 1 |
| SQL murakkabligi | Oddiy | O'rtacha (subquery, JSON) |
| Guruhlash | JavaScript'da | Bazada |
| Qayta ishlatish | Har toifa — alohida funksiya | Bitta katta so'rov |
| Kanon vaqti | 5.0 ms | 5.0 ms |
Kanonda ikkalasi teng. Amalda: batch — kodni bo'laklarga ajratish oson (izohlar moduli, teglar moduli), ORM va DataLoader shunday ishlaydi. Bitta so'rov — eng kam aylanish, lekin so'rov kattalashib boradi. Ikkalasi ham N+1 dan o'nlab marta yaxshi.
Tekshirib ko'ring:
json_aggli bitta so'rovda ichki subquery har vazifa uchun bajariladi. Nega bu N+1 emas?
Javob
N+1 ning narxi — har so'rov uchun alohida tarmoq aylanishi, rejalashtirish va ilova tomonidagi ishlov. Ichki subquery esa bitta so'rov rejasining ichida, bazaning o'zida, indeks orqali bajariladi: tarmoq yo'q, rejalashtirish bir marta. Ichki tomonda indeks bo'lmasa esa haqiqatan sekin bo'lardi — har qator uchun Seq Scan (EXPLAIN darsida ko'rgan Nested Loop xavfi).
5. DataLoader g'oyasi
5.1 Muammo: kod tuzilmasi
Batch yechimi bitta funksiyada hammasini biladi: "avval ro'yxat, keyin hamma izohlar". Katta ilovada esa kod bo'laklarga bo'lingan. Masalan, GraphQL serverida yoki komponentlar daraxtida har vazifa "o'z izohlarini" so'raydi — ular bir-birini bilmaydi. Har biri izohlarniOl(id) chaqiradi va N+1 qaytadi.
DataLoader (Facebook yaratgan kutubxona, keyin hamma tillarga ko'chgan) buni shunday hal qiladi: yukla(id) darhol so'rov yubormaydi. U id'ni navbatga qo'yadi va joriy sinxron kod tugaguncha kutadi (microtask navbati). Keyin to'plangan hamma id'lar uchun bitta batch so'rov yuboradi va har chaqiruvchiga o'z natijasini qaytaradi.
sequenceDiagram
participant V1 as Vazifa 1
participant V2 as Vazifa 2
participant L as DataLoader
participant DB as Baza
V1->>L: yukla(1)
V2->>L: yukla(2)
Note over L: microtask: navbat [1, 2]
L->>DB: izohlar WHERE vazifa_id = ANY('{1,2}')
DB-->>L: hammasi
L-->>V1: 1 ning izohlari
L-->>V2: 2 ning izohlariDiagrammaga qarang: ikki vazifa bir-birini bilmaydi, lekin bazaga bitta so'rov ketdi. Mustaqil kod va batch so'rov — ikkalasi bir vaqtda.
3-mashqda DataLoader'ning soddalashtirilgan versiyasini o'zingiz yozasiz (20 qatorga yaqin). Haqiqiy dataloader npm paketi qo'shimcha imkoniyatlarga ega: kesh (bir so'rov ichida bir id ikki marta yuklanmaydi), batch hajmi chegarasi. U GraphQL chuqur darsida ishlatiladi; bu darsda o'rnatilmagan va sinalmagan.
5.2 ORM'larda
ORM'lar (27-qismda: Prisma, Drizzle) bog'liq ma'lumotni "include" bilan so'rashga imkon beradi: vazifalar.findMany({ include: { izohlar: true } }). Ichida ular batch yoki JOIN so'rovi yuboradi. Lekin ORM bilan ham N+1 yozish oson: for ichida findMany chaqirish. ORM muammoni yashirmaydi — faqat to'g'ri vositani beradi. ORM'lar bu darsda sinalmagan, 27-qismda o'lchanadi.
6. N+1 ni qanday aniqlash
- So'rovlarni sanash — kanon usuli:
pool.queryni o'rab, har so'rov uchun hisoblagich. Testda: "bu endpoint ko'pi bilan 3 ta so'rov yuborsin" degan tekshiruv. So'rovlar soni element soniga bog'liq bo'lsa — N+1. pg_stat_statements(oldingi dars) —callsjuda katta,mean_exec_timejuda kichik so'rov shakli:SELECT … FROM izohlar WHERE vazifa_id = $1,calls = 2 000 000.- Log —
log_min_duration_statement = 0qisqa muddatga: bitta sahifa ochilganda logda 100 ta bir xil so'rov ketma-ket. - APM vositalari (Sentry, Datadog) — so'rov "sharshara"sini chizadi: bitta HTTP so'rov ichida 100 ta mayda DB chaqiruvi (hujjat bo'yicha, sinalmagan).
7. Node qadami: API ro'yxatida N+1 yo'q
GET /v1/vazifalar har vazifani teglari bilan qaytaradi. N+1 xavfi aynan shu yerda edi: "har vazifa uchun teglarini olish". PgOmbori buni bazaga bormasdan hal qiladi — teglar vazifa matnidan ajratiladi, 25-qismdagi xotira ombori bilan bir xil qoida (kanondan, qisqartirildi):
function vazifa(q: VazifaQatori): Vazifa {
return {
id: q.id,
matn: q.matn,
// teglar matndan — xotira ombori bilan aynan bir xil qoida
teglar: teglariniOl(q.matn),
// … vaqtlar, tartib, versiya
};
}vazifa_teglari jadvali (uni trigger to'ldiradi) faqat filtr (?teg=ish) va /v1/teglar hisobi uchun ishlatiladi. Izohlar esa API'da hali yo'q.
So'rovlar sonini kanon "So'rovlarni sanash" usuli bilan tekshirdi. EXPLAIN ANALYZE'ni o'qish darsidagi reja-olchov.ts ulanishning query metodini o'raydi va har royxat() chaqiruvida nechta so'rov ketganini yozadi. Katta bazada (013/014 bilan qayta qurilgan) har holatning sarlavha qatori (reja-64.txt):
=== sukut (tartib, id), 1-sahifa: royxat() 3.1 ms, so'rovlar 2
=== sukut, kursor bilan 2-sahifa: royxat() 3.2 ms, so'rovlar 2
=== teg=ish + bajarildi=false: royxat() 5.2 ms, so'rovlar 2
=== sort=matn (COLLATE "C"): royxat() 2.8 ms, so'rovlar 2
=== jami=true: royxat() 2.9 ms, so'rovlar 3Nima bo'ldi? So'rovlar soni sahifadagi vazifalar soniga (20) bog'liq emas: ro'yxatning o'zi + ro'yxat versiyasi (SELECT vazifalar_versiyasi …), jami=true bo'lsa yana count(*). "So'rovlar soni element soniga bog'liq emas" — N+1 yo'qligining mezoni shu. Teg filtri esa SQL ichidagi correlated ichki so'rov — bu N+1 emas, chunki u bazaning ichida bajariladi («Qaysi biri yaxshi» bo'limidagi savolga qarang).
Yana bitta tuzoq loyihada yo'q. Batch'dagi "bigint id: biri son, biri string" muammosi PgOmbori da uchramaydi. Hovuz o'z TURLAR parseri bilan ochiladi: INT8 → xavfsiz Number (node-postgres asoslari). Shart bitta — hamma so'rov shu hovuzdan o'tsin.
Bu darsda loyiha kodi o'zgarmadi. Holat 64 ning commit'i — Keyset pagination darsida.
8. Ko'p uchraydigan xatolar
| Xato | Oqibat | To'g'risi |
|---|---|---|
for ichida await query |
N+1 | Id'larni yig'ib, = ANY($1) |
| Ikki "ko'p" bog'lanish bitta tekis JOIN'da | Qatorlar ko'payadi (3 × 2) | Har biri uchun subquery + json_agg |
| Batch'da izohsiz element unutiladi | undefined yoki xato |
?? [], SQL'da unnest + LEFT JOIN |
bigint id: biri son, biri string |
filter hech narsa topmaydi |
Bir xil tur (setTypeParser hamma joyda) |
json_agg NULL |
Ilovada null.map xatosi |
coalesce(…, '[]') |
| "Lokalda tez ishlayapti" | Production'da tarmoq aylanishi 10–20 marta katta | So'rovlar sonini o'lchang |
9. Mashqlar
Mashqlar kurs/mashqlar/26/63-n1/ da. 2-mashq holat 40 bazasida, 3-mashq — Node 24 da tekshirilgan (biz ishga tushirdik).
1-mashq (oson): to'ldiring
- 1 ta ro'yxat + har element uchun 1 ta so'rov — muammosi
- Id'lar massivini bitta parametr qilib yuborish —
WHERE vazifa_id = ($1::bigint[]) - Ichki so'rov natijasini JSON massivga yig'adigan funksiya —
- Bir "tick" ichidagi so'rovlarni bittaga yig'adigan naqsh —
- 50 vazifa + izohlar + teglar: N+1 da nechta so'rov?
Yechim
- N+1 — 1 ro'yxat + N element.
- ANY —
= ANY($1::bigint[]), bo'sh massivda 0 qator (xato emas). - json_agg — bo'sh bo'lsa NULL,
coalescebilan'[]'. - DataLoader — microtask'gacha kutib, batch yuboradi.
- 101 — 1 + 50 + 50.
2-mashq (o'rta): N+1 ni SQL bilan almashtiring
Holat 40 bazasida uchta so'rov yozing. (a) Azizning 1, 2, 9, 17-vazifalari uchun izohlar sonini bitta so'rov bilan chiqaring. (b) Izohsiz vazifalar ham 0 bilan chiqsin. (c) Azizning birinchi 4 ta faol vazifasini (id bo'yicha) izohlari JSON massivi bilan bitta so'rovda oling.
Yechim
So'rovlar «Batch» va «Bitta so'rov» bo'limlaridagilar bilan bir xil:
-- a) id'lar ro'yxati bo'yicha izohlar — bitta so'rovda
SELECT vazifa_id, count(*) AS izohlar
FROM izohlar
WHERE vazifa_id = ANY ('{1,2,9,17}'::bigint[])
GROUP BY vazifa_id ORDER BY vazifa_id;
-- b) izohsiz vazifalar ham chiqsin (0) — unnest + LEFT JOIN
SELECT v.id, count(i.id) AS izohlar
FROM unnest('{1,2,9,17}'::bigint[]) AS v(id)
LEFT JOIN izohlar i ON i.vazifa_id = v.id
GROUP BY v.id ORDER BY v.id;
-- c) Azizning birinchi 4 vazifasi izohlari bilan — bitta so'rov
SELECT v.id, left(v.matn, 25) AS matn,
coalesce((SELECT json_agg(i.matn ORDER BY i.yaratilgan_vaqt)
FROM izohlar i WHERE i.vazifa_id = v.id), '[]')
AS izohlar
FROM faol_vazifalar v
WHERE v.egasi_id = 1
ORDER BY v.id LIMIT 4;Natijalar — yuqoridagi bo'limlarda. (a) da faqat 2 qator — izohsiz vazifalar yo'qoladi; (b) to'rttasini ham beradi. Ilova uchun (a) yetarli: JavaScript'da Map bilan guruhlab, topilmaganiga 0 qo'yiladi:
const qatorlar = [
{ vazifa_id: "1", izohlar: "3" },
{ vazifa_id: "2", izohlar: "1" },
];
const soni = new Map(
qatorlar.map((r) => [r.vazifa_id, Number(r.izohlar)]),
);
const idlar = ["1", "2", "9", "17"];
console.log(idlar.map((id) => `${id}: ${soni.get(id) ?? 0}`));Konsolda:
[ '1: 3', '2: 1', '9: 0', '17: 0' ]qatorlar — pg qaytaradigan shaklda: bigint va count string bo'lib keladi, shuning uchun id'lar ham string, soni Number() bilan.
3-mashq (qiyin): mini DataLoader
yuklovchiYarat(toplamFn) funksiyasini yozing. U yukla(id) funksiyasini qaytaradi: chaqiruv Promise beradi, lekin so'rov darhol ketmaydi. Bir sinxron qism ichidagi hamma yukla chaqiruvlari yig'ilib, queueMicrotask da bitta toplamFn(idlar) chaqiriladi; har Promise o'z natijasini oladi. Xato bo'lsa — hamma kutayotganlar reject bo'lsin. Soxta "baza" bilan sinang: 4 ta vazifa — 1 ta so'rov.
Yechim
let sorovlar = 0;
const izohlarBazasi = {
1: ["Zo'r", "Ertaga"], 2: [], 3: ["Bajarildi"], 4: ["?"],
};
// "Baza": id lar massivi -> har id uchun izohlar (= ANY($1) kabi)
async function izohlarniOl(idlar) {
sorovlar++;
return idlar.map((id) => izohlarBazasi[id] ?? []);
}
function yuklovchiYarat(toplamFn) {
let navbat = [];
return function yukla(id) {
return new Promise((resolve, reject) => {
navbat.push({ id, resolve, reject });
if (navbat.length > 1) return;
queueMicrotask(async () => {
const joriy = navbat;
navbat = [];
try {
const natija = await toplamFn(joriy.map((n) => n.id));
joriy.forEach((n, i) => n.resolve(natija[i]));
} catch (xato) {
joriy.forEach((n) => n.reject(xato));
}
});
});
};
}
const izohYukla = yuklovchiYarat(izohlarniOl);
const vazifalar = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }];
const natija = await Promise.all(
vazifalar.map(async (v) => ({
...v,
izohlar: await izohYukla(v.id),
})),
);
console.log(JSON.stringify(natija));
console.log("so'rovlar:", sorovlar);Konsolda:
[{"id":1,"izohlar":["Zo'r","Ertaga"]},{"id":2,"izohlar":[]},{"id":3,"izohlar":["Bajarildi"]},{"id":4,"izohlar":["?"]}]
so'rovlar: 1Qanday ishlaydi:
- Birinchi
yuklanavbatni bo'sh topadi vaqueueMicrotaskbilan "tozalovchi" ni rejalashtiradi. Keyingilari faqat navbatga qo'shiladi (navbat.length > 1— rejalashtirilgan). Promise.all(vazifalar.map(…))— to'rttayuklabitta sinxron qismda chaqiriladi. Microtask ular hammasidan keyin ishlaydi.toplamFnnatijasi id'lar tartibida bo'lishi shart — DataLoader'ning asosiy shartnomasi. SQL'dan kelgan qatorlar tartibsiz bo'lsa, ularniMapbilan id bo'yicha qayta tartiblash kerak.navbat = []microtask boshida — tozalash paytida kelgan yangiyuklalar keyingi batch'ga tushadi.
10. Real ishda
- Eng ko'p uchraydigan sekinlik — ayniqsa ORM bilan yozilgan loyihalarda. Intervyuda deyarli har doim so'raladi: "N+1 nima va qanday tuzatasiz?"
- GraphQL serverlari DataLoader'siz deyarli ishlamaydi: har maydon o'z resolver'ida ma'lumot so'raydi.
- REST API'da ro'yxat endpoint'i uchun test: "so'rovlar soni elementlar soniga bog'liq emas" — N+1 qaytib kelmasligi kafolati.
- Mikroservislarda xuddi shu muammo HTTP darajasida: har buyurtma uchun "mijoz servisiga" alohida so'rov. Yechim ham shu — batch endpoint (
GET /mijozlar?id=1,2,3).
Xulosa
- N+1 — ro'yxat + har element uchun so'rov; kanonda 101 so'rov, 152 ms.
- Narxning asosi — har so'rovning tarmoq aylanishi; uzoq serverda farq yanada katta.
- Batch: id'larni yig'ib
= ANY($1::bigint[])— 3 so'rov, 5 ms; izohsiz elementlarni unutmang. - Bitta so'rov: subquery +
json_agg/array_agg— 1 so'rov, 5 ms; ikki "ko'p" bog'lanishni tekis JOIN qilmang. - DataLoader — mustaqil kod qismlari so'ragan id'larni microtask'gacha yig'ib, bitta batch yuboradi.
- Aniqlash: so'rovlarni sanash,
pg_stat_statementsdagi ulkancalls.
Keyingi dars: Keyset pagination va katta OFFSET — 100 000-sahifani ochish nega sekin (kanonda 350 ms) va qanday qilib 0.2 ms ga tushiriladi.
Manbalar
- PostgreSQL 18 hujjati — Array Functions and Operators (
ANY,unnest): https://www.postgresql.org/docs/18/functions-array.html - PostgreSQL 18 hujjati — Aggregate Functions (
json_agg,array_agg): https://www.postgresql.org/docs/18/functions-aggregate.html - DataLoader (GraphQL Foundation): https://github.com/graphql/dataloader
- node-postgres hujjati — Queries (massiv parametr): https://node-postgres.com/features/queries
- IlmHamroh kanon o'lchovi (Node 24.21.0, pg 8.23.1, PostgreSQL 18.6, 2 mln vazifa, 2026-10-24); SQL misollar va mashqlar — biz ishga tushirdik (PostgreSQL 17.6, Node 24.21.0, Windows 11).
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!