IlmHamroh
JavaScript Full-stack/27-qism. Ma'lumotlar bazasi chuqur: Postgres, ORM, Mongo, Redis48/65-dars19 daqiqa
Mundarija (29)

MongoDB aggregation pipeline: $match, $group, $lookup, $unwind, $facet va bosqichlar tartibi

Qisqacha: Aggregation pipeline — MongoDB'da hisobot va murakkab so'rovlar usuli: hujjatlar bosqichlar quvuridan o'tadi, har bosqich ($match, $group, $lookup, $unwind, $project, $sort, $facet …) oldingisining natijasini o'zgartiradi. Bu SQL'dagi WHERE → GROUP BY → JOIN → SELECT → ORDER BY ning hujjat bazasidagi shakli. Kanon 48 vazifada Postgres bilan bir xil sonlarni oldi. 500 000 vazifada esa bir xil GROUP BY savoliga Mongo 246.78 ms, Postgres 41.11 ms sarfladi. Mongo diskda esa 6 baravar ixcham: 18.1 MB va 115 MB.

Bu darsda

  • Pipeline nima ekanini va SQL bandlariga qanday mos kelishini tushuntira olasiz.
  • $match, $group, $lookup, $unwind, $project, $sort, $limit bilan foydalanuvchi statistikasini yozasiz.
  • $facet, $bucket va $setWindowFields bilan bitta so'rovda bir nechta natija, guruhlar va window hisob-kitob qilasiz.
  • Maydonsiz hujjatdagi $size xatosini o'qiysiz va $ifNull bilan tuzatasiz.
  • Bosqichlar tartibi, indeks va explain ni bog'laysiz; katta to'plamda Postgres bilan o'lchovni o'qiysiz.

Oldin bilishingiz kerak: Populate va embed vs reference, Agregatlar, GROUP BY va HAVING, Window funksiyalar 1: ranking, View va materialized view.

1. Nega bu kerak?

find bitta savolga javob beradi: "qaysi hujjatlar mos?". Hisobot esa boshqa savollarni beradi:

  • Har foydalanuvchida nechta vazifa va qanchasi bajarilgan?
  • Eng ko'p ishlatilgan 5 ta teg qaysi?
  • Sahifadagi 20 ta vazifa va jami soni — bitta so'rovda.

Postgres'da bularni GROUP BY, JOIN va window funksiyalari bilan yozgandik (Agregatlar, Window funksiyalar). Mongo'da — aggregation pipeline.

O'xshatish: zavoddagi konveyer. Xom ashyo bir uchidan kiradi. Birinchi stanok saralaydi ($match), ikkinchisi bo'laklarga ajratadi ($unwind), uchinchisi qutilarga joylaydi ($group), oxirgisi yorliq yopishtiradi ($project). Har stanok faqat oldingisidan kelganni ko'radi.

Bu darsdagi chiqishlar — kanon o'lchovi (IlmHamroh, 2026-10): native driver darsida ko'chirilgan 48 vazifali vazifalar bazasi va katta to'plam — Postgres'dagi 500 000 vazifa Mongo'ga bir marta ko'chirilgan. Node 24.21.0, mongodb 7.7.0, MongoDB 8.3.13, PostgreSQL 18.6 (ikkalasi Docker'da, WSL2), i5-12500H.

2. Pipeline qanday ishlaydi

2.1 Bosqichlar massivi

js
await db.collection("vazifalar").aggregate([
  { $match: { … } },    // 1-bosqich
  { $group: { … } },    // 2-bosqich
  { $sort: { … } },     // 3-bosqich
]).toArray();

aggregate massiv oladi, har element — bitta bosqich. Natija — kursor (native driver darsidagi kabi), toArray() yoki for await bilan o'qiladi.

Bosqich Nima qiladi SQL o'xshashi
$match filtrlaydi WHERE / HAVING
$group guruhlab hisoblaydi GROUP BY + agregatlar
$project, $addFields maydonlarni tanlaydi/hisoblaydi SELECT ifodalari
$sort, $limit, $skip tartib va kesish ORDER BY, LIMIT, OFFSET
$lookup boshqa kolleksiyadan biriktiradi LEFT JOIN
$unwind massivni har element — alohida hujjat unnest
$count sanaydi count(*)
$facet bir nechta ichki pipeline bir nechta so'rov
$bucket diapazonlar bo'yicha guruh CASE + GROUP BY
$setWindowFields window hisob-kitob OVER (PARTITION BY …)

$ bilan boshlangan satr ("$egasi") — "shu maydonning qiymati". Bu aggregation'ning asosiy belgisi: { $sum: "$narx" } — narx maydonlarini qo'sh.

2.2 Brauzerda modeli

Pipeline g'oyasi — oddiy massiv metodlari zanjiri (Metod zanjiri va ma'lumot pipeline). Kichik "kolleksiya"da $match → $group → $sort ni JavaScript'da yozamiz:

js
const vazifalar = [
  { egasi: "Aziz", bajarildi: true },
  { egasi: "Aziz", bajarildi: false },
  { egasi: "Dilnoza", bajarildi: true },
  { egasi: "Aziz", bajarildi: true, ochirilgan: "2026-10-20" },
];

// $match: { ochirilgan: { $exists: false } }
const mos = vazifalar.filter((v) => v.ochirilgan === undefined);

// $group: { _id: "$egasi", jami: {$sum: 1}, bajarilgan: … }
const guruhlar = new Map();
for (const v of mos) {
  const g = guruhlar.get(v.egasi) ?? { _id: v.egasi, jami: 0,
    bajarilgan: 0 };
  g.jami += 1;
  g.bajarilgan += v.bajarildi ? 1 : 0;
  guruhlar.set(v.egasi, g);
}

// $sort: { _id: 1 }
const natija = [...guruhlar.values()]
  .sort((a, b) => a._id.localeCompare(b._id));
console.log(JSON.stringify(natija));

Konsolda:

text
[{"_id":"Aziz","jami":2,"bajarilgan":1},{"_id":"Dilnoza","jami":1,"bajarilgan":1}]

Mongo xuddi shu ishni server ichida, C++ da va (iloji bo'lsa) indeks bilan qiladi. Ma'lumot ilovaga ko'chmaydi — faqat natija keladi.

flowchart LR
  A["vazifalar (48)"] --> M["$match: Savatda emas"]
  M --> G["$group: egasi bo'yicha"]
  G --> L["$lookup: foydalanuvchilar"]
  L --> U["$unwind: f"]
  U --> P["$project: foiz"]
  P --> S["$sort: id"]
  S --> R["7 qator"]

Diagramma — keyingi bo'limdagi kanon pipeline'i: har o'q — oldingi bosqich natijasi keyingisiga oqadi.

3. Asosiy bosqichlar: foydalanuvchi statistikasi

3.1 To'liq pipeline

Kanon har foydalanuvchining vazifalar statistikasini hisobladi — Postgres'dagi statistika materialized view'i (View va materialized view) bilan solishtirish uchun:

js
const stat = await V.aggregate([
  { $match: { ochirilgan: { $exists: false } } },
  { $group: {
    _id: "$egasi",
    jami: { $sum: 1 },
    bajarilgan: { $sum: { $cond: ["$bajarildi", 1, 0] } },
  } },
  { $lookup: {
    from: "foydalanuvchilar", localField: "_id",
    foreignField: "_id", as: "f",
    pipeline: [{ $project: { _id: 0, pgId: 1, ism: 1 } }],
  } },
  { $unwind: "$f" },
  { $project: {
    _id: 0, id: "$f.pgId", ism: "$f.ism", jami: 1, bajarilgan: 1,
    foiz: { $round: [{ $multiply: [
      { $divide: ["$bajarilgan", "$jami"] }, 100] }, 1] },
  } },
  { $sort: { id: 1 } },
]).toArray();

V — db.collection("vazifalar"). Natija:

text
{"jami":12,"bajarilgan":5,"id":1,"ism":"Aziz Karimov","foiz":41.7}
{"jami":6,"bajarilgan":3,"id":2,"ism":"Dilnoza Rahimova","foiz":50}
{"jami":6,"bajarilgan":3,"id":3,"ism":"Sardor Tursunov","foiz":50}
{"jami":9,"bajarilgan":4,"id":4,"ism":"Madina Yusupova","foiz":44.4}
{"jami":5,"bajarilgan":2,"id":5,"ism":"Javohir Olimov","foiz":40}
{"jami":4,"bajarilgan":2,"id":6,"ism":"Gulnora Saidova","foiz":50}
{"jami":4,"bajarilgan":2,"id":7,"ism":"Bekzod Nazarov","foiz":50}

Kanon qayd qildi: sonlar Postgres'dagi statistika bilan bir xil. Bir xil ma'lumot, ikki baza, ikki til — bir xil javob.

3.2 Har bosqichni o'qiymiz

  • $match — Savatdagilarni chiqarib tashlaydi. Har doim imkon qadar boshida: keyingi bosqichlarga kamroq hujjat boradi va indeks ishlatilishi mumkin.
  • $group — _id — guruh kaliti ("$egasi" — egasi bo'yicha). Qolgan maydonlar — akkumulyatorlar: $sum, $avg, $min, $max, $first, $push (massivga yig'ish), $addToSet. { $sum: 1 } — har hujjat uchun 1 — ya'ni sanash. $cond: [shart, ha, yo'q] — SQL'dagi CASE WHEN (Postgres'da buni count(*) FILTER (WHERE bajarildi) bilan yozgandik).
  • $lookup — oldingi darsda ko'rgan biriktirish; pipeline ichida faqat kerakli maydonlar. Natija — massiv f: [{ pgId, ism }].
  • $unwind: "$f" — massivni ochadi: bitta elementli massiv → oddiy obyekt. Ko'p elementli bo'lsa — har element uchun alohida hujjat (keyingi misol). E'tibor: egasi topilmagan guruh $unwind da yo'qoladi (bo'sh massiv — hujjat yo'q), SQL'dagi INNER JOIN kabi. Saqlab qolish uchun — { $unwind: { path: "$f", preserveNullAndEmptyArrays: true } }.
  • $project — chiqish shakli: "$f.ism" — ichma-ich maydon, 1 — o'zgarishsiz qoldir, foiz — ifoda bilan hisoblangan.
  • $sort — id bo'yicha.

4. $unwind + $group: eng ko'p teglar

js
await V.aggregate([
  { $match: { ochirilgan: { $exists: false } } },
  { $unwind: "$teglar" },
  { $group: { _id: "$teglar", soni: { $sum: 1 } } },
  { $sort: { soni: -1, _id: 1 } },
  { $limit: 5 },
]).toArray();
text
{"_id":"ish","soni":8}
{"_id":"o'qish","soni":8}
{"_id":"to'lov","soni":5}
{"_id":"bozor","soni":3}
{"_id":"oila","soni":3}

$unwind: "$teglar" — teglar: ["docker", "o'qish"] li hujjat ikkita hujjatga aylanadi: biri teglar: "docker", ikkinchisi teglar: "o'qish". Keyin teg bo'yicha guruhlash — oddiy sanash. Kanon: natija vazifalar-api ning GET /v1/teglar javobi bilan bir xil.

$sort: { soni: -1, _id: 1 } — avval soni kamayish, teng bo'lsa nomi bo'yicha. Ikkinchi kalit bo'lmasa, ish va o'qish (ikkalasi 8) tartibi kafolatlanmaydi — keyset pagination darsidagi "noyob tartib" qoidasi.

Tegsiz vazifa $unwind dan keyin yo'qoladi — bu yerda to'g'ri, chunki biz teglarni sanayapmiz.

5. $facet: bitta so'rovda bir nechta natija

API ro'yxatiga ko'pincha ikki narsa kerak: sahifa va jami soni. Ikki so'rov yubormaslik uchun — $facet: bir xil kirish ustida bir nechta ichki pipeline:

js
await V.aggregate([
  { $match: { ochirilgan: { $exists: false } } },
  { $facet: {
    sahifa: [
      { $sort: { yaratilgan: -1 } }, { $limit: 2 },
      { $project: { _id: 0, pgId: 1, matn: 1 } },
    ],
    jami: [{ $count: "n" }],
    ustuvorlik: [
      { $group: { _id: "$ustuvorlik", n: { $sum: 1 } } },
      { $sort: { _id: 1 } },
    ],
  } },
]).toArray();
text
{"sahifa":[{"pgId":48,"matn":"Bozordan kartoshka va piyoz olish #bozor"},{"pgId":40,"matn":"Sport zalga yozilish #sport"}],"jami":[{"n":47}],"ustuvorlik":[{"_id":null,"n":1},{"_id":"orta","n":29},{"_id":"past","n":7},{"_id":"yuqori","n":10}]}

Natija — bitta hujjat, uchta maydon. Ikki g'alati joy bor:

  1. jami: 47. Statistikada jami 46 edi (12+6+6+9+5+4+4). Bitta ortiqcha qayerdan?
  2. {"_id":null,"n":1} — ustuvorligi null bo'lgan bitta vazifa. Kanonda bunday vazifa yo'q edi.

Ikkalasining sababi bitta. Native driver darsidagi replaceOne tuzog'ini eslang: 3-vazifa { bajarildi: true } ga aylangan edi — egasi, ustuvorlik, ochirilgan yo'q. U $match dan o'tdi (ochirilgan yo'q!), jamiga kirdi, ustuvorligi null guruhiga tushdi. Statistikada esa $lookup + $unwind uni tashlab yubordi — egasi yo'q.

Bitta noto'g'ri replaceOne ikki darsdan keyin hisobotni jim buzdi. Sxemasizlikning haqiqiy narxi shu.

6. $bucket va $size xatosi

6.1 Xato

Kanon vazifalarni izohlar soni bo'yicha guruhlamoqchi bo'ldi. Avval har hujjatda izohlar sonini $size bilan hisobladi:

js
await V.aggregate([
  { $project: { n: { $size: "$izohlar" } } },
]).toArray();
text
$size maydonsiz hujjatda: Location17124: The argument to $size must be an array, but was of type: missing

Tarjima: "$size argumenti massiv bo'lishi kerak, lekin uning turi: yo'q (missing)". Location17124 — server xatosining kod nomi. Yana o'sha 3-vazifa: unda izohlar maydoni umuman yo'q. Postgres'da ustun yo'q bo'lishi mumkin emas edi, Mongo'da esa — bitta yarim hujjat butun hisobotni yiqitdi.

6.2 $ifNull bilan tuzatish va $bucket

js
await V.aggregate([
  { $project: {
    n: { $size: { $ifNull: ["$izohlar", []] } },
  } },
  { $bucket: {
    groupBy: "$n", boundaries: [0, 1, 2, 5], default: "5+",
    output: { vazifalar: { $sum: 1 } },
  } },
]).toArray();
text
{"_id":0,"vazifalar":38}
{"_id":1,"vazifalar":5}
{"_id":2,"vazifalar":5}
  • $ifNull: ["$izohlar", []] — maydon yo'q yoki null bo'lsa, bo'sh massiv. SQL'dagi coalesce. Sxemasiz bazada hisobot yozganda bu himoya ko'p joyda kerak.
  • $bucket — boundaries: [0, 1, 2, 5] chegaralari: [0, 1), [1, 2), [2, 5); ulardan tashqarisi — default guruhiga. Natijada _id — guruhning pastki chegarasi: 38 ta vazifada izoh yo'q, 5 tasida bitta, 5 tasida 2–4 ta. 5+ guruhi chiqmadi — bunday vazifa yo'q (bo'sh guruh qaytmaydi).

Tekshirib ko'ring: 38 + 5 + 5 = 48. Statistikada 46, $facet da 47 edi. Bu yerda nega 48?

Javob

Bu pipeline'da $match yo'q — Savatdagi vazifa ham sanaldi. Uchta son uchta savolga javob: statistika — egasi bor va Savatda emas (46), facet — Savatda emas (47, yarim hujjat bilan), bucket — hammasi (48). Hisobotda raqam farq qilsa, avval filtrlarni solishtiring.

7. $setWindowFields: window hisob-kitob

Window funksiyalar darsida row_number() OVER (PARTITION BY … ORDER BY …) va yig'ma summani yozgandik. Mongo'da — $setWindowFields:

js
await V.aggregate([
  { $match: { egasi: dilnozaId } },
  { $setWindowFields: {
    partitionBy: "$egasi",
    sortBy: { yaratilgan: 1 },
    output: {
      n: { $documentNumber: {} },
      kumulyativ_bajarilgan: {
        $sum: { $cond: ["$bajarildi", 1, 0] },
        window: { documents: ["unbounded", "current"] },
      },
    },
  } },
  { $project: { _id: 0, pgId: 1, n: 1, bajarildi: 1,
    kumulyativ_bajarilgan: 1 } },
]).toArray();

dilnozaId — Dilnozaning ObjectId'si (kanon uni foydalanuvchilar dan pgId: 2 bilan oldi).

text
{"pgId":5,"bajarildi":true,"n":1,"kumulyativ_bajarilgan":1}
{"pgId":14,"bajarildi":false,"n":2,"kumulyativ_bajarilgan":1}
{"pgId":16,"bajarildi":true,"n":3,"kumulyativ_bajarilgan":2}
{"pgId":26,"bajarildi":false,"n":4,"kumulyativ_bajarilgan":2}
{"pgId":29,"bajarildi":true,"n":5,"kumulyativ_bajarilgan":3}
{"pgId":36,"bajarildi":false,"n":6,"kumulyativ_bajarilgan":3}
Mongo SQL
partitionBy: "$egasi" PARTITION BY egasi_id
sortBy: { yaratilgan: 1 } ORDER BY yaratilgan_vaqt
$documentNumber row_number()
window: { documents: ["unbounded", "current"] } ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW

$group dan farqi: hujjatlar kamaymaydi — har biriga yangi maydon qo'shiladi. Natija: Dilnozaning 6 vazifasi tartib raqami va shu paytgacha bajarilganlar soni bilan.

8. Bosqichlar tartibi va indeks

8.1 Optimizator nimani ko'chiradi

Kanon { egasi: 1, bajarildi: 1 } indeksini yaratib, uch pipeline'ning rejasini explain("executionStats") bilan o'qidi:

js
await V.createIndex({ egasi: 1, bajarildi: 1 });
const p1 = [{ $match: { egasi, bajarildi: false } },
  { $project: { matn: 1 } }];
const p2 = [{ $project: { matn: 1, egasi: 1, bajarildi: 1 } },
  { $match: { egasi, bajarildi: false } }];
const p3 = [{ $group: { _id: "$egasi", n: { $sum: 1 } } },
  { $match: { _id: egasi } }];
text
{"match_boshida":{"ixscan":true,"collscan":false,"docsExamined":7}}
{"match_project_dan_keyin":{"ixscan":true,"collscan":false,"docsExamined":7}}
{"match_group_dan_keyin":{"ixscan":true,"collscan":false,"docsExamined":0}}
  • $match boshida — kutilganidek: indeks (IXSCAN), faqat Azizning 7 bajarilmagan vazifasi o'qildi.
  • $match $project dan keyin — xuddi shu reja! Optimizator $match ni o'zi oldinga ko'chirdi: $project filtrlangan maydonlarni o'zgartirmaydi, demak tartibni almashtirish xavfsiz.
  • $match $group dan keyin (_id — guruh kaliti) — baribir IXSCAN va 0 ta hujjat o'qildi. Guruh kaliti egasi bo'lgani uchun optimizator filtrni guruhlashdan oldinga o'tkazdi. Sanash uchun faqat egasi kerak, u indeksda bor — hujjatlarning o'zini o'qishning hojati qolmadi.

Optimizator aqlli, lekin hamma narsani ko'chira olmaydi. Masalan, $group dagi hisoblangan maydon (jami) bo'yicha $match — guruhlashdan keyin (HAVING kabi), uni ko'chirib bo'lmaydi. Qoida: $match va $sort (+$limit) ni o'zingiz boshiga qo'ying — optimizatorga tayanmang. explain — EXPLAIN ANALYZE ning Mongo'dagi o'xshashi; indekslar bilan keyingi darsda chuqur ishlaymiz.

Indeks faqat pipeline boshidagi $match/$sort ga yordam beradi. $group yoki $unwind dan keyin hujjatlar — xotiradagi oraliq natija, indeks ularga tegmaydi.

9. Katta to'plam: Mongo va Postgres

9.1 Ko'chirish va hajm

Kanon Postgres'dagi 500 000 vazifani (26-qismdagi katta to'plamning 1/4 qismi) 50 000 lik bo'laklarda Mongo'ga ko'chirdi — _id sifatida Postgres id'si, teglar massivi bilan:

text
ko'chirildi: 500000 hujjat, 13.0 s
Mongo katta.vazifalar: size 74.0 MB (siqilmagan), storageSize 18.1 MB (WiredTiger snappy), indekslar 13.3 MB
Postgres katta.vazifalar jadvali (TOAST bilan, indekssiz): 115 MB

Diskda Mongo 18.1 MB, Postgres 115 MB. Sabab — WiredTiger (Mongo'ning saqlash dvigateli) ma'lumotni sukut bo'yicha snappy algoritmi bilan siqadi. Siqilmagan holda Mongo hujjatlari 74 MB — Postgres'dan unchalik kichik emas. Postgres jadval sahifalarini siqmaydi (faqat katta qiymatlarni TOAST'da). Diqqat: solishtirish to'liq emas — Postgres tomonda teglar alohida jadvalda (vazifa_teglari), u bu 115 MB ga kirmagan.

9.2 Bir xil savol — GROUP BY ustuvorlik

js
// Mongo
K.aggregate([
  { $group: { _id: "$ustuvorlik", jami: { $sum: 1 },
    bajarilgan: { $sum: { $cond: ["$bajarildi", 1, 0] } } } },
  { $sort: { _id: 1 } },
]);
// Postgres
pg.query(`SELECT ustuvorlik, count(*),
  count(*) FILTER (WHERE bajarildi)
  FROM vazifalar GROUP BY 1 ORDER BY 1`);

Chiqishda avval birinchi va oxirgi qatordagi vaqtlarga, keyin o'rtadagi ikki qatorda sonlar bir xil, lekin tartib va son turi har xil ekaniga qarang:

text
GROUP BY ustuvorlik (500 000): Mongo 246.78 ms, Postgres 41.11 ms (mediana 5)
[{"_id":"orta","jami":250254,"bajarilgan":149680},{"_id":"past","jami":124368,"bajarilgan":74804},{"_id":"yuqori","jami":125378,"bajarilgan":75202}]
[{"ustuvorlik":"past","jami":"124368","bajarilgan":"74804"},{"ustuvorlik":"orta","jami":"250254","bajarilgan":"149680"},{"ustuvorlik":"yuqori","jami":"125378","bajarilgan":"75202"}]
top-3 teg: Mongo ($unwind) 745.71 ms, Postgres (JOIN) 71.76 ms
500 000 vazifa: bir xil savol, ikki baza
  • GROUP BY ustuvorlik — Mongo246,78 ms
  • GROUP BY ustuvorlik — Postgres41,11 ms
  • Top-3 teg — Mongo ($unwind)745,71 ms
  • Top-3 teg — Postgres (JOIN)71,76 ms

Manba: IlmHamroh kanon o'lchovi, 2026-10-25: MongoDB 8.3.13 va PostgreSQL 18.6 (Docker, WSL2), Node 24.21.0, i5-12500H; 5 marta, mediana

Bu o'lchovda Postgres guruhlashda 6 marta, teglar bo'yicha 10 marta tez. Kanon izohi: Postgres bu so'rovni parallel bajardi (bir nechta ishchi jarayon — Postgres arxitekturasi). Mongo'da bitta aggregation so'rovi bunday parallel ishchilarga bo'linmaydi (hujjat bo'yicha; kanon buni alohida o'lchamagan). Katta to'plamda 400 096 ta teg bog'lanishi bor — $unwind shuncha oraliq hujjat yasaydi, keyin ularni guruhlaydi. Postgres'da teglar allaqachon alohida qatorlarda.

Xulosa "Mongo sekin" emas. Mongo bitta hujjatni olish va yozishda kuchli, katta tahliliy guruhlashda esa relatsion baza odatda oldinda. Bitta mashina, bitta o'lchov — o'z ma'lumotingizda o'lchang.

9.3 Ikki nozik farq

Natija qatorlariga yana qarang:

  1. Tartib. Mongo: orta, past, yuqori — satr sifatida alifbo tartibi. Postgres: past, orta, yuqori — ustuvorlik enum turi, tartib e'lon qilingandagidek (Pul, miqdor va enum'larni modellash). Mongo'da "past < orta < yuqori" tartibi kerak bo'lsa — son maydon yoki $switch bilan tartib raqami.
  2. Son turi. Postgres javobida "jami":"124368" — satr! pg drayveri count(*) (bigint) ni JavaScript son aniqligini yo'qotmaslik uchun satr qilib qaytaradi (node-postgres asoslari). Mongo'da — oddiy son.

10. Loyiha qadami: kanon tajribasi, loyihaga kirmaydi

vazifalar-api hisobotlari Postgres'da: statistika — materialized view, teglar — GET /v1/teglar. Kanon aggregation'da:

  1. Statistika va top teglar — Postgres bilan bir xil sonlar.
  2. $facet (sahifa + jami + guruh), $bucket, $setWindowFields.
  3. Yarim hujjat oqibatlari: jami 47, _id: null guruhi va $size xatosi (Location17124) — $ifNull bilan tuzatildi.
  4. explain: optimizator $match ni ko'chirdi; 500 000 da Mongo 246.78 vs Postgres 41.11 ms; disk 18.1 vs 115 MB.

11. Ko'p uchraydigan xatolar

Xato Oqibat To'g'risi
$match oxirida Hamma hujjat quvurdan o'tadi, indeks yo'q $match va $sort boshida
Maydon nomini $ siz ({ $sum: "narx" }) Satr — har hujjatda 0 qo'shiladi "$narx"
$size maydonsiz hujjatda Location17124 — pipeline yiqiladi $ifNull: ["$izohlar", []]
$lookup + $unwind ning yo'qotishi Topilmagan guruhlar jim tushib qoladi preserveNullAndEmptyArrays: true
Teng qiymatlarda bitta kalit bo'yicha $sort Tartib har safar boshqacha Ikkinchi kalit (_id)
Enum'ni satr deb saralash orta, past, yuqori Son maydon yoki $switch
"Baza ichida — doim tez" Katta guruhlashda kutilmagan sekinlik explain va o'lchov

Yana bir chegara: har pipeline bosqichi sukut bo'yicha xotira limitiga ega (MongoDB hujjatida — 100 MB); oshsa, server vaqtinchalik fayllarga yozadi (allowDiskUse, yangi versiyalarda sukut yoqiq). Kanon bu chegaraga yetmadi.

12. Mashqlar

Mashqlar kurs/mashqlar/27/48-aggregation/ da. 2- va 3-mashq yechimlari Node 24 da tekshirilgan (biz ishga tushirdik — pipeline'ning kichik modeli, bazasiz).

1-mashq (oson): to'ldiring

  • SQL'dagi WHERE ning bosqichi —
  • Massivni har element uchun alohida hujjatga aylantiradigan bosqich —
  • { $sum: 1 } nima qiladi?
  • Maydon yo'q bo'lsa zaxira qiymat beradigan operator —
  • row_number() ning Mongo o'xshashi —
Yechim
  • $match — boshida bo'lsa indeks ishlatadi.
  • $unwind — bo'sh massivli hujjat yo'qoladi.
  • sanaydi — har hujjat uchun 1 qo'shadi.
  • $ifNull — SQL'dagi coalesce.
  • $documentNumber — $setWindowFields ichida.

2-mashq (o'rta): mini pipeline

pipeline(hujjatlar, bosqichlar) yozing. Qo'llab-quvvatlasin: $match (faqat tenglik), $unwind ("$maydon"), $group (_id: "$maydon", akkumulyator faqat { $sum: 1 }), $sort (bitta maydon, 1/-1), $limit. Kanon "top teglar" pipeline'ini kichik ma'lumotda takrorlang.

Ishora: har bosqich — massivdan massiv qaytaradigan funksiya; reduce bilan zanjir (reduce).

Yechim
js
const ol = (h, yol) => h[yol.slice(1)];   // "$teglar" -> h.teglar

const BOSQICHLAR = {
  $match: (hh, f) => hh.filter((h) =>
    Object.entries(f).every(([k, v]) => h[k] === v)),
  $unwind: (hh, yol) => hh.flatMap((h) =>
    (ol(h, yol) ?? []).map((e) => ({ ...h, [yol.slice(1)]: e }))),
  $group: (hh, { _id, ...akk }) => {
    const m = new Map();
    for (const h of hh) {
      const k = ol(h, _id);
      const g = m.get(k) ?? { _id: k };
      for (const nom of Object.keys(akk)) g[nom] = (g[nom] ?? 0) + 1;
      m.set(k, g);
    }
    return [...m.values()];
  },
  $sort: (hh, s) => {
    const [[k, yon]] = Object.entries(s);
    return [...hh].sort((a, b) =>
      (a[k] > b[k] ? 1 : a[k] < b[k] ? -1 : 0) * yon);
  },
  $limit: (hh, n) => hh.slice(0, n),
};

function pipeline(hujjatlar, bosqichlar) {
  return bosqichlar.reduce((hh, b) => {
    const [[nom, arg]] = Object.entries(b);
    return BOSQICHLAR[nom](hh, arg);
  }, hujjatlar);
}

const vazifalar = [
  { egasi: 1, teglar: ["ish", "tolov"] },
  { egasi: 1, teglar: ["ish"] },
  { egasi: 2, teglar: ["uy", "ish"] },
  { egasi: 2, teglar: [] },
  { egasi: 1, teglar: ["uy"] },
];
const top = pipeline(vazifalar, [
  { $unwind: "$teglar" },
  { $group: { _id: "$teglar", soni: { $sum: 1 } } },
  { $sort: { soni: -1 } },
  { $limit: 2 },
]);
console.log(JSON.stringify(top));
const aziz = pipeline(vazifalar, [
  { $match: { egasi: 1 } },
  { $unwind: "$teglar" },
  { $group: { _id: "$teglar", n: { $sum: 1 } } },
  { $sort: { _id: 1 } },
]);
console.log(JSON.stringify(aziz));

Konsolda:

text
[{"_id":"ish","soni":3},{"_id":"uy","soni":2}]
[{"_id":"ish","n":2},{"_id":"tolov","n":1},{"_id":"uy","n":1}]

To'rtinchi vazifa (bo'sh teglar) $unwind dan keyin yo'qoldi — Mongo'dagidek. reduce — har bosqich oldingisining natijasini oladi: pipeline'ning aynan ta'rifi. Haqiqiy $sort barqaror tartib kafolatlamaydi — shuning uchun kanon ikkinchi kalit qo'shgan.

3-mashq (qiyin): $bucket modeli

bucket(qiymatlar, chegaralar, sukut) yozing: chegaralar = [0, 1, 2, 5] — [0,1), [1,2), [2,5) guruhlari; tashqaridagilar — sukut guruhiga. Natija — { _id, soni } massivi, faqat bo'sh bo'lmagan guruhlar, chegaralar tartibida (sukut — oxirida). Kanon natijasini qayta hosil qiling (38 ta 0, 5 ta 1, 5 ta 2–4) va yana 3 ta vazifa qo'shing: 7, 9 va 4 izohli.

Ishora: findIndex bilan qaysi oraliqqa tushishini toping.

Yechim
js
function bucket(qiymatlar, chegaralar, sukut) {
  const soni = new Map();
  for (const q of qiymatlar) {
    const i = chegaralar.findIndex(
      (c, j) => j < chegaralar.length - 1
        && q >= c && q < chegaralar[j + 1]);
    const kalit = i === -1 ? sukut : chegaralar[i];
    soni.set(kalit, (soni.get(kalit) ?? 0) + 1);
  }
  const tartib = [...chegaralar.slice(0, -1), sukut];
  return tartib.filter((k) => soni.has(k))
    .map((k) => ({ _id: k, vazifalar: soni.get(k) }));
}

const izohlar = [
  ...Array(38).fill(0), ...Array(5).fill(1),
  2, 2, 3, 3, 4,
];
const chegara = [0, 1, 2, 5];
bucket(izohlar, chegara, "5+").forEach((g) =>
  console.log(JSON.stringify(g)));
console.log("---");
bucket([...izohlar, 7, 9, 4], chegara, "5+").forEach((g) =>
  console.log(JSON.stringify(g)));

Konsolda:

text
{"_id":0,"vazifalar":38}
{"_id":1,"vazifalar":5}
{"_id":2,"vazifalar":5}
---
{"_id":0,"vazifalar":38}
{"_id":1,"vazifalar":5}
{"_id":2,"vazifalar":6}
{"_id":"5+","vazifalar":2}

Birinchi uch qator — kanon chiqishi bilan bir xil. 2–4 izohlilarning aniq taqsimoti kanonda ko'rinmaydi: biz 2, 2, 3, 3, 4 deb oldik, guruh soni baribir 5. 5 chegarasi o'zi [2,5) ga kirmaydi — yuqori chegara ochiq; 7 va 9 — default guruhida. Mongo'da default berilmasa, chegaradan tashqaridagi qiymat xato beradi (hujjat bo'yicha).

13. Real ishda

  • Dashboard va hisobotlar — Mongo'li loyihalarda statistika endpoint'larining deyarli hammasi aggregation; $facet — "sahifa + jami + filtrlar soni" uchun standart.
  • Katta tahlil — ko'pincha Mongo'dan alohida tahliliy bazaga (ClickHouse, BigQuery, Postgres) ko'chiriladi; kanon o'lchovi sababini ko'rsatdi.
  • Materialized natija — $merge/$out bosqichlari pipeline natijasini kolleksiyaga yozadi (Postgres'dagi materialized view kabi; hujjat bo'yicha, kanon sinamadi).
  • Intervyu savollari: "$match qayerda bo'lishi kerak va nega?", "$lookup qanday ishlaydi?", "$unwind nima qiladi?", "aggregation'da indeks qachon ishlaydi?".

Xulosa

  • Aggregation pipeline — bosqichlar massivi; har bosqich oldingisining natijasini oladi; "$maydon" — maydon qiymati.
  • Asosiy bosqichlar: $match, $group (akkumulyatorlar $sum, $cond), $lookup, $unwind, $project, $sort, $limit; qo'shimcha: $facet, $bucket, $setWindowFields.
  • Sxemasizlik hisobotda chiqadi: yarim hujjat jami 47, _id: null va $size xatosini berdi — $ifNull bilan himoya.
  • $match/$sort boshida — indeks ishlaydi; optimizator ba'zan o'zi ko'chiradi, lekin unga tayanmang.
  • Kanon 500 000 da: Mongo 246.78 vs Postgres 41.11 ms (guruhlash), 745.71 vs 71.76 ms (teglar); disk 18.1 vs 115 MB.

Keyingi dars: MongoDB indekslari — COLLSCAN va IXSCAN, ESR qoidasi, multikey, text, TTL va partial indekslar 500 000 hujjatda.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
MongoDB aggregation pipeline: $match, $group, $lookup, $unwind, $facet va bosqichlar tartibi — IlmHamroh