Kas yra agregavimo pipeline ir kam jo reikia
Jei kada nors teko dirbti su MongoDB, tikriausiai pastebėjote, kad paprastos užklausos su `find()` kartais tiesiog neužtenka. Kai reikia ne tik surasti duomenis, bet ir juos sugrupuoti, perskaičiuoti, sujungti ar transformuoti, į pagalbą ateina agregavimo pipeline. Tai vienas galingiausių MongoDB įrankių, kuris leidžia apdoroti duomenis tiesiog duomenų bazės lygmenyje, neperkraunant aplikacijos logikos.
Agregavimo pipeline veikia kaip konvejeris – duomenys pereina per kelias stadijas, kur kiekviena stadija atlieka tam tikrą transformaciją. Įsivaizduokite gamyklą: žaliava įeina vienoje pusėje, pereina per įvairias apdirbimo stotis ir išeina kaip gatavs produktas kitoje pusėje. Panašiai veikia ir agregavimas MongoDB.
Kodėl tai svarbu? Nes alternatyva būtų ištraukti visus duomenis į aplikaciją ir apdoroti juos ten. Tai ne tik lėčiau, bet ir sunaudoja daugiau atminties bei tinklo resursų. Agregavimo pipeline leidžia atlikti sudėtingus skaičiavimus ten, kur yra duomenys – duomenų bazėje.
Pagrindinės pipeline stadijos, kurias naudosite kasdien
Pradėkime nuo esminių dalykų. MongoDB agregavimo pipeline sudaro stadijos, kurias žymime dolaro ženklu. Kiekviena stadija – tai objektas, kuris nurodo, ką norime padaryti su duomenimis.
$match – tai jūsų filtras. Veikia panašiai kaip `find()`, bet agregavimo kontekste. Geriausia praktiką – naudoti `$match` kuo anksčiau pipeline, kad sumažintumėte duomenų kiekį, kurį reikės apdoroti vėlesnėse stadijose.
„`javascript
{ $match: { status: „active”, age: { $gte: 18 } } }
„`
$project – čia formuojate, kokius laukus norite matyti rezultate. Galite pasirinkti tik tam tikrus laukus, pervadinti juos arba sukurti naujus apskaičiuotus laukus.
„`javascript
{ $project: {
name: 1,
email: 1,
fullName: { $concat: [„$firstName”, ” „, „$lastName”] }
}}
„`
$group – čia prasideda tikroji magija. Grupavimas leidžia sujungti dokumentus pagal tam tikrą kriterijų ir atlikti agregavimo funkcijas kaip suma, vidurkis, maksimumas ir pan.
„`javascript
{ $group: {
_id: „$category”,
totalSales: { $sum: „$amount” },
avgPrice: { $avg: „$price” },
count: { $sum: 1 }
}}
„`
$sort – rūšiavimas. Naudojamas 1 didėjančiai tvarkai, -1 mažėjančiai.
$limit ir $skip – riboja rezultatų kiekį ir praleidžia pirmuosius N dokumentų. Puikiai tinka paginacijai.
Kaip sujungti duomenis iš skirtingų kolekcijų
Vienas dažniausių klausimų, su kuriais susiduria MongoDB pradedantieji – kaip padaryti „join” operaciją? SQL pasaulyje tai įprasta, bet NoSQL duomenų bazėse reikia kitokio požiūrio. MongoDB tam siūlo `$lookup` stadiją.
Tarkime, turite dvi kolekcijas: `users` ir `orders`. Norite gauti kiekvieno vartotojo užsakymus:
„`javascript
db.users.aggregate([
{
$lookup: {
from: „orders”,
localField: „_id”,
foreignField: „userId”,
as: „userOrders”
}
}
])
„`
Rezultate kiekvienas vartotojo dokumentas turės naują masyvą `userOrders` su visais jo užsakymais. Bet atsargiai – `$lookup` gali būti lėtas didelėse kolekcijose. Įsitikinkite, kad turite tinkamus indeksus ant laukų, kuriuos naudojate sujungimui.
Nuo MongoDB 3.6 versijos galite naudoti ir sudėtingesnę `$lookup` sintaksę su pipeline:
„`javascript
{
$lookup: {
from: „orders”,
let: { userId: „$_id” },
pipeline: [
{ $match: {
$expr: { $eq: [„$userId”, „$$userId”] },
status: „completed”
}},
{ $project: { _id: 1, total: 1, date: 1 }}
],
as: „completedOrders”
}
}
„`
Tai leidžia atlikti sudėtingesnius filtravimus ir transformacijas jau sujungimo metu.
Masyvų apdorojimas – $unwind ir draugai
Viena iš dažniausių situacijų – turite dokumentą su masyvu ir norite apdoroti kiekvieną masyvo elementą atskirai. Čia į pagalbą ateina `$unwind`.
Pavyzdžiui, turite produktų dokumentus, kur kiekvienas produktas turi spalvų masyvą:
„`javascript
{
_id: 1,
name: „Marškinėliai”,
colors: [„raudona”, „mėlyna”, „žalia”],
price: 20
}
„`
Naudojant `$unwind`:
„`javascript
db.products.aggregate([
{ $unwind: „$colors” }
])
„`
Gausite tris atskirus dokumentus:
„`javascript
{ _id: 1, name: „Marškinėliai”, colors: „raudona”, price: 20 }
{ _id: 1, name: „Marškinėliai”, colors: „mėlyna”, price: 20 }
{ _id: 1, name: „Marškinėliai”, colors: „žalia”, price: 20 }
„`
Tai ypač naudinga, kai reikia grupuoti ar filtruoti pagal masyvo elementus. Tačiau būkite atsargūs – `$unwind` gali labai padidinti dokumentų skaičių, jei masyvai dideli.
Nuo naujesnių versijų MongoDB siūlo ir kitų operatorių darbui su masyvais: `$filter`, `$map`, `$reduce`. Jie leidžia apdoroti masyvus be `$unwind`, kas dažnai būna efektyviau:
„`javascript
{
$project: {
name: 1,
expensiveItems: {
$filter: {
input: „$items”,
as: „item”,
cond: { $gte: [„$$item.price”, 100] }
}
}
}
}
„`
Sąlyginė logika ir skaičiavimai pipeline
MongoDB agregavimo framework’as turi daugybę operatorių, leidžiančių atlikti sudėtingus skaičiavimus ir sąlyginę logiką. `$cond` operatorius veikia kaip ternary operatorius programavime:
„`javascript
{
$project: {
name: 1,
priceCategory: {
$cond: {
if: { $gte: [„$price”, 100] },
then: „brangus”,
else: „pigus”
}
}
}
}
„`
Galite naudoti ir `$switch` sudėtingesnėms sąlygoms:
„`javascript
{
$project: {
name: 1,
ageGroup: {
$switch: {
branches: [
{ case: { $lt: [„$age”, 18] }, then: „vaikas” },
{ case: { $lt: [„$age”, 65] }, then: „suaugęs” },
{ case: { $gte: [„$age”, 65] }, then: „senjoras” }
],
default: „nežinoma”
}
}
}
}
„`
Matematiniai operatoriai taip pat plačiai prieinami: `$add`, `$subtract`, `$multiply`, `$divide`, `$mod` ir kiti. Galite atlikti datos operacijas su `$dateToString`, `$year`, `$month`, `$dayOfMonth`:
„`javascript
{
$project: {
orderDate: 1,
year: { $year: „$orderDate” },
month: { $month: „$orderDate” },
formattedDate: {
$dateToString: {
format: „%Y-%m-%d”,
date: „$orderDate”
}
}
}
}
„`
Optimizavimo patarimai ir indeksų naudojimas
Agregavimo pipeline gali būti labai galingas, bet ir labai lėtas, jei nenaudojamas teisingai. Štai keletas praktinių patarimų, kaip išspausti maksimalų našumą.
Naudokite $match kuo anksčiau. Kuo greičiau sumažinsite duomenų kiekį, tuo greičiau veiks visos kitos stadijos. Idealiu atveju `$match` turėtų būti pirma stadija.
Indeksai veikia tik pradžioje. MongoDB gali naudoti indeksus `$match` ir `$sort` stadijoms, bet tik jei jos yra pipeline pradžioje. Kai tik pasirodo stadija, kuri modifikuoja dokumentus (pvz., `$project`, `$unwind`), indeksai nebegali būti naudojami.
Projektuokite tik tai, ko reikia. Naudokite `$project` arba `$unset`, kad pašalintumėte nereikalingus laukus kuo anksčiau. Tai sumažina duomenų kiekį, kuris keliauja per pipeline.
Stebėkite atminties naudojimą. Pagal nutylėjimą agregavimo operacijos turi 100MB atminties limitą. Jei viršijate, operacija nepavyks. Galite naudoti `allowDiskUse: true` opciją, bet tai sulėtins procesą:
„`javascript
db.collection.aggregate(pipeline, { allowDiskUse: true })
„`
Naudokite $facet atsargiai. Nors `$facet` leidžia vykdyti kelias agregacijas vienu metu, tai dvigubina ar trigubina resursų naudojimą.
Norėdami pamatyti, kaip veikia jūsų pipeline, naudokite `explain()`:
„`javascript
db.collection.aggregate(pipeline).explain(„executionStats”)
„`
Tai parodys, kurios stadijos naudoja indeksus, kiek dokumentų apdorojama kiekvienoje stadijoje ir kiek laiko tai užtrunka.
Realūs naudojimo scenarijai ir pavyzdžiai
Teorija teorija, bet pažiūrėkime, kaip tai atrodo praktikoje. Štai keletas dažnų scenarijų, su kuriais tikriausiai susidursite.
Pardavimų ataskaita pagal mėnesį ir kategoriją:
„`javascript
db.orders.aggregate([
{
$match: {
orderDate: {
$gte: new Date(„2024-01-01”),
$lt: new Date(„2025-01-01”)
}
}
},
{
$group: {
_id: {
year: { $year: „$orderDate” },
month: { $month: „$orderDate” },
category: „$category”
},
totalRevenue: { $sum: „$total” },
orderCount: { $sum: 1 },
avgOrderValue: { $avg: „$total” }
}
},
{
$sort: { „_id.year”: 1, „_id.month”: 1 }
},
{
$project: {
_id: 0,
year: „$_id.year”,
month: „$_id.month”,
category: „$_id.category”,
totalRevenue: { $round: [„$totalRevenue”, 2] },
orderCount: 1,
avgOrderValue: { $round: [„$avgOrderValue”, 2] }
}
}
])
„`
Top 10 aktyvių vartotojų su jų užsakymų informacija:
„`javascript
db.users.aggregate([
{
$lookup: {
from: „orders”,
localField: „_id”,
foreignField: „userId”,
as: „orders”
}
},
{
$project: {
name: 1,
email: 1,
orderCount: { $size: „$orders” },
totalSpent: { $sum: „$orders.total” },
lastOrderDate: { $max: „$orders.orderDate” }
}
},
{
$match: { orderCount: { $gt: 0 } }
},
{
$sort: { totalSpent: -1 }
},
{
$limit: 10
}
])
„`
Produktų rekomendacijos pagal dažniausiai kartu perkamus produktus:
„`javascript
db.orders.aggregate([
{ $unwind: „$items” },
{ $unwind: „$items” },
{
$group: {
_id: {
product1: „$items.productId”,
product2: „$items.productId”
},
frequency: { $sum: 1 }
}
},
{
$match: {
$expr: { $ne: [„$_id.product1”, „$_id.product2”] }
}
},
{ $sort: { frequency: -1 } },
{ $limit: 20 }
])
„`
Kai pipeline tampa per sudėtingas: ką daryti
Kartais agregavimo pipeline gali tapti tikru monstru su dešimtimis stadijų. Kodas tampa sunkiai skaitomas ir prižiūrimas. Štai keletas strategijų, kaip su tuo susidoroti.
Skaidykite į funkcijas. Vietoj vieno milžiniško pipeline, sukurkite funkcijas, kurios grąžina atskiras stadijas:
„`javascript
function matchActiveUsers() {
return { $match: { status: „active” } };
}
function projectUserInfo() {
return {
$project: {
name: 1,
email: 1,
registrationYear: { $year: „$createdAt” }
}
};
}
const pipeline = [
matchActiveUsers(),
projectUserInfo(),
// kitos stadijos…
];
„`
Naudokite $merge ar $out tarpiniams rezultatams. Jei agregacija labai sudėtinga, galite išsaugoti tarpinius rezultatus į atskirą kolekciją:
„`javascript
db.orders.aggregate([
// pirmoji pipeline dalis
{ $match: { … } },
{ $group: { … } },
{ $out: „temp_aggregation_results” }
])
// Tada tęskite su antrąja dalimi
db.temp_aggregation_results.aggregate([
// antroji pipeline dalis
])
„`
Apsvarstykite materialized views. Jei ta pati agregacija vykdoma dažnai, galbūt verta sukurti view arba periodiškai atnaujinamus agregavimo rezultatus.
Testuokite po vieną stadiją. Kai kuriate sudėtingą pipeline, pridėkite stadijas po vieną ir tikrinkite rezultatus. MongoDB Compass turi puikų agregavimo pipeline builder’į, kuris leidžia matyti kiekvienos stadijos rezultatus realiu laiku.
Ir paskutinis patarimas – dokumentuokite savo pipeline. Po kelių mėnesių net jūs patys nežinosite, ką čia norėjote pasiekti. Komentarai JavaScript kode gali išgelbėti gyvybę:
„`javascript
const pipeline = [
// Filtruojame tik aktyvius vartotojus iš paskutinių 6 mėnesių
{ $match: { … } },
// Sujungiame su užsakymais
{ $lookup: { … } },
// Skaičiuojame bendras sumas pagal kategoriją
{ $group: { … } }
];
„`
Ką daryti, kai viskas veikia, bet per lėtai
Sukūrėte puikų agregavimo pipeline, jis grąžina teisingus rezultatus, bet vykdymas užtrunka amžinybę. Štai keletas debug’inimo žingsnių.
Pirmiausiai – `explain()`. Jau minėjau, bet tai tikrai svarbu:
„`javascript
db.collection.aggregate(pipeline, { explain: true })
„`
Ieškokite `COLLSCAN` – tai reiškia, kad MongoDB skenuoja visą kolekciją. Norite matyti `IXSCAN`, kas reiškia, kad naudojamas indeksas.
Patikrinkite, kiek dokumentų pereina per kiekvieną stadiją. Jei pirma stadija apdoroja milijoną dokumentų, o rezultate gaunate 10, kažkas negerai. Turbūt galite anksčiau pritaikyti griežtesnį filtrą.
Jei naudojate `$lookup`, įsitikinkite, kad foreign field’as turi indeksą. Tai vienas dažniausių našumo problemų šaltinių:
„`javascript
db.orders.createIndex({ userId: 1 })
„`
Kartais verta apsvarstyti duomenų modelio pakeitimą. Jei nuolat darote tą patį `$lookup`, galbūt verta denormalizuoti duomenis ir saugoti reikalingą informaciją tiesiogiai dokumente? NoSQL filosofija skatina dubliavimą, jei tai pagerina našumą.
Jei agregacija vis tiek per lėta, galite ją vykdyti asinchroniškai background procese ir rezultatus cache’inti. Redis puikiai tam tinka:
„`javascript
// Tikrinti cache
const cachedResult = await redis.get(‘aggregation:sales:monthly’);
if (cachedResult) {
return JSON.parse(cachedResult);
}
// Vykdyti agregaciją
const result = await db.orders.aggregate(pipeline).toArray();
// Išsaugoti cache su TTL
await redis.setex(‘aggregation:sales:monthly’, 3600, JSON.stringify(result));
return result;
„`
Ir nepamirškite – kartais paprasčiausias sprendimas yra galingesnis serveris. MongoDB našumas labai priklauso nuo RAM kiekio. Jei darbiniai duomenys netelpa į RAM, viskas lėtėja eksponentiškai.
Kai pipeline tampa jūsų geriausiu draugu
Agregavimo pipeline iš pradžių gali atrodyti bauginantis, ypač jei ateinat iš SQL pasaulio. Bet kai įvaldote pagrindines stadijas ir suprantate, kaip jos veikia kartu, atsiveria visiškai naujos galimybės.
Svarbiausias dalykas – praktika. Pradėkite nuo paprastų pipeline su viena ar dviem stadijomis. Eksperimentuokite su `$match` ir `$group`. Palaipsniui pridėkite `$lookup`, `$unwind`, sąlyginę logiką. MongoDB Compass agregavimo builder’is tikrai padės – galite vizualiai matyti, kas vyksta kiekvienoje stadijoje.
Nepamirškite, kad agregavimo pipeline nėra vienintelis sprendimas. Kartais paprastas `find()` su projekcija bus greitesnis ir aiškesnis. Naudokite tinkamą įrankį tinkamam darbui. Bet kai reikia sudėtingų transformacijų, grupavimų ar skaičiavimų – agregavimo pipeline tikrai jūsų geriausias draugas.
Ir paskutinis dalykas – MongoDB dokumentacija yra tikrai gera. Kai užstrigsite, ten rasite daugybę pavyzdžių ir paaiškinimų. Agregavimo framework’as nuolat tobulėja, kiekvienoje naujoje versijoje atsiranda naujų operatorių ir galimybių. Verta sekti naujienas ir atnaujinimus.
Sėkmės transformuojant duomenis!
