Aggregation pipeline stage-ების მასივია. თითოეული stage იღებს დოკუმენტებს, რომლებიც წინამ გასცა, ცვლის მათ და გადასცემს შემდეგს. $match ფილტრავს, $group ბევრ დოკუმენტს თითო გასაღებზე ერთად აქცევს, $lookup სხვა კოლექციას აერთებს, ხოლო თანმიმდევრობა, რომლითაც მათ დაალაგებ, ადგენს, გამოიყენებს თუ არა query ინდექსს, რამდენი მეხსიერება დასჭირდება და დასრულდება 40 მილიწამში თუ მეხსიერების შეცდომით გაითიშება. მოკლედ: ჯერ გაფილტრე, მერე დააჯგუფე, გააერთიანე ბოლოს და explain() ნახე, სანამ pipeline-ს ენდობი კოლექციაზე, რომელიც გაიზრდება.
ეს გზამკვლევი ერთ pipeline-ს ნაბიჯ-ნაბიჯ აგებს მაღაზიის პატარა მონაცემებზე, განმარტავს, რას აკეთებს თითოეული გავრცელებული stage, გვაჩვენებს, რომელს შეუძლია ინდექსის გამოყენება და რომელს არა, და განიხილავს 100 MB-ის ლიმიტს, რომელსაც ადამიანები პირველად მაშინ აწყდებიან, როცა ანგარიში რეალურ მონაცემებზე გაეშვება. ვარაუდია, რომ დაკავშირება და query-ების გაშვება უკვე იცი; თუ არა, დაიწყე MongoDB-ის connection string-ებით დამწყებებისთვის.
რა არის pipeline და რით განსხვავდება find()-ისგან#
find() აბრუნებს დოკუმენტებს ისე, როგორც ინახება, საჭიროების შემთხვევაში გაფილტრულს და ველებზე შეზღუდულს. aggregate()-ს შეუძლია დააბრუნოს ის, რაც არასდროს შენახულა: ჯამები კლიენტების მიხედვით, შეკვეთების რაოდენობა დღეების მიხედვით, შეკვეთები იმ კლიენტთან ერთად, ვინც გააკეთა. ყველაფერი, რასაც SQL-ში GROUP BY-ით, JOIN-ით ან window ფუნქციით დაწერდი, MongoDB-ში pipeline-ში გადის.
Pipeline ასე ეშვება: db.collection.aggregate([ stage1, stage2, ... ]). დოკუმენტები stage-ებში თანმიმდევრობით გადიან და თითოეული stage მხოლოდ იმას ხედავს, რაც წინამ გასცა. აქედან ორი შედეგი გამომდინარეობს და pipeline-ის პრობლემების უმეტესობა ერთ-ერთს უბრუნდება:
- Stage-ს შეუძლია გამოიყენოს მხოლოდ ის ველები, რომლებიც ამ მომენტისთვის ჯერ კიდევ არსებობს. თუ ადრინდელმა
$project-მა ან$group-მა ველი გააქრო, უფრო გვიანდელი$matchმასზე ვერაფერს იპოვის და შეცდომა ამას არ გეტყვის. - Stage-ები, რომლებსაც რაიმეს გაცემამდე მთელი შეყვანა სჭირდებათ (
$group, ინდექსის გარეშე$sort,$bucket), დოკუმენტებს მეხსიერებაში ინახავენ. Stage-ები, რომლებიც დოკუმენტებს ერთ-ერთად ატარებენ ($match,$project,$addFields,$limit), თითქმის არაფერი უჯდებათ.
ქვემოთ მაგალითებში ორი კოლექციაა. orders-ში ერთი დოკუმენტია თითო შეკვეთაზე, customers-ში ერთი თითო ანგარიშზე:
db.orders.insertMany([ { customerId: 1, status: "paid", total: 42.5, createdAt: ISODate("2026-10-01T09:12:00Z"), items: [{ sku: "A-100", qty: 2, price: 10 }, { sku: "B-200", qty: 1, price: 22.5 }] }, { customerId: 2, status: "paid", total: 15, createdAt: ISODate("2026-10-02T14:40:00Z"), items: [{ sku: "A-100", qty: 1, price: 10 }, { sku: "C-300", qty: 1, price: 5 }] }, { customerId: 1, status: "refunded", total: 10, createdAt: ISODate("2026-10-03T08:05:00Z"), items: [{ sku: "A-100", qty: 1, price: 10 }] }])db.customers.insertMany([ { _id: 1, name: "Nino", plan: "pro" }, { _id: 2, name: "Dato", plan: "free" }])გაუშვი ისინი mongosh-ში. იგივე stage-ის დოკუმენტები უცვლელად მუშაობს driver-იდანაც; Node.js-ში, Python-სა და Go-ში pipeline უბრალოდ ობიექტების მასივია.
$match: პირველად გაფილტრე და გამოიყენე ინდექსი#
$match იღებს იმავე query სინტაქსს, რასაც find(). ეს pipeline-ის ყველაზე მნიშვნელოვანი stage-ია, რადგან მხოლოდ ის შეუძლია ინდექსის გამოყენება და მხოლოდ ის აიაფებს ყველაფერს, რაც მის შემდეგ მოდის.
db.orders.aggregate([ { $match: { status: "paid", createdAt: { $gte: ISODate("2026-10-01") } } }])$match pipeline-ის სულ დასაწყისში იგეგმება ისევე, როგორც find()-ის ფილტრი: თუ ინდექსი status-სა და createdAt-ს ფარავს, MongoDB მთელი კოლექციის ნაცვლად ინდექსის დიაპაზონს ათვალიერებს. $match pipeline-ში უფრო შორს წინა stage-ების გამოსავალზე მუშაობს, ამიტომ კოლექციის ინდექსს ვერ გამოიყენებს. აქედან წესი: "match რაც შეიძლება ადრე".
ერთ შემთხვევაში optimiser გეხმარება. თუ $match-ს $project-ის ან $addFields-ის შემდეგ დაწერ და პირობა მხოლოდ იმ ველებს ეხება, რომლებიც მათ არ შეუცვლიათ, MongoDB თვითონ გადაიტანს $match-ს წინ. თუ პირობა წინა stage-ის გამოთვლილ ველზეა დამოკიდებული, გადატანა შეუძლებელია და ამაზე დაყრდნობა არ ღირს: pipeline იმ თანმიმდევრობით დაწერე, როგორითაც გინდა, რომ შესრულდეს.
ორი დეტალი, რომელიც გვატკენს:
- ჩვეულებრივი
$matchველს მნიშვნელობასთან ადარებს და არა სხვა ველთან. ველის მისამართები, როგორიცაა"$total", მხოლოდ გამოსახულებებში იკითხება. გამოიყენე$expr, როცა ერთი დოკუმენტის ორი ველის შედარება გჭირდება:{ $match: { $expr: { $gt: ["$total", "$limit"] } } }.$match$expr-ით ზოგადად ვერ იყენებს ინდექსს ასეთი შედარებისთვის, ამიტომ გამოიყენე შეგნებულად. - თარიღები თარიღებად მხოლოდ მაშინ ედარება, როცა თარიღებად არის შენახული. თუ
createdAtISO სტრიქონად ჩაიწერა,$gte: ISODate(...)ვერაფერს იპოვის. გაუშვიdb.orders.findOne()და ტიპი ნახე, სანამ დიაპაზონს დაწერ.
$group: ერთი დოკუმენტი თითო გასაღებზე#
$group ყველა შემომავალ დოკუმენტს, რომელსაც ერთი და იგივე _id გამოსახულება აქვს, ერთ გამომავალ დოკუმენტად აქცევს და ჯგუფზე accumulator-ებს ითვლის. შემოსავალი კლიენტების მიხედვით გადახდილი შეკვეთებიდან:
db.orders.aggregate([ { $match: { status: "paid" } }, { $group: { _id: "$customerId", orders: { $sum: 1 }, revenue: { $sum: "$total" }, average: { $avg: "$total" }, lastOrder: { $max: "$createdAt" } } }, { $sort: { revenue: -1 } }])ჯგუფის _id არის ის, რითაც აჯგუფებ. ველის მისამართი ("$customerId"), მთელი ჩადგმული დოკუმენტი შედგენილი გასაღებისთვის ({ customer: "$customerId", day: { $dateToString: { format: "%Y-%m-%d", date: "$createdAt" } } }) ან null, რომლითაც მთელ შეყვანას ერთ დოკუმენტად აქცევ, ასე ითვლება საერთო ჯამი.
აი accumulator-ები, რომლებსაც ყველაზე ხშირად გამოიყენებ:
| Accumulator | შედეგი |
|---|---|
$sum | ველის ჯამი, ან 1 თითო დოკუმენტზე დასათვლელად |
$avg | ველის საშუალო, ნაკლული მნიშვნელობები იგნორირდება |
$min, $max | ჯგუფის უმცირესი ან უდიდესი მნიშვნელობა |
$first, $last | პირველი ან ბოლო დოკუმენტის მნიშვნელობა (აზრი აქვს მხოლოდ $sort-ის შემდეგ) |
$push | ყველა მნიშვნელობის მასივი, დუბლიკატები რჩება |
$addToSet | უნიკალური მნიშვნელობების მასივი |
$push და $addToSet სახიფათოა. ისინი ყველა ჯგუფის ყველა მნიშვნელობას მეხსიერებაში ინახავენ, მზა ჯგუფი კი ერთი დოკუმენტია 16 MB-იანი ზომის ლიმიტით. $group _id: "$customerId"-ით და $push: "$$ROOT"-ით დიდ კოლექციაზე ორივე ლიმიტს ერთდროულად დაჯახების სტანდარტული გზაა. თუ თითო კლიენტის მხოლოდ ბოლო სამი შეკვეთა გჭირდება, გამოიყენე top-N accumulator-ები ($topN, $firstN) იმ ვერსიაში, სადაც ისინი არის, ნაცვლად იმისა, რომ ყველაფერი მასივში ჩაყარო და მერე მოჭრა.
თარიღის მიხედვით ანგარიში იგივე stage-ია სხვა გასაღებით. შეკვეთები დღეების მიხედვით:
db.orders.aggregate([ { $match: { createdAt: { $gte: ISODate("2026-10-01"), $lt: ISODate("2026-11-01") } } }, { $group: { _id: { $dateToString: { format: "%Y-%m-%d", date: "$createdAt" } }, orders: { $sum: 1 } } }, { $sort: { _id: 1 } }])გასაღები UTC-ის სტრიქონია, თუ timezone არ გადაეცი. თუ ანგარიშები ერთ დროის სარტყელში მყოფ ადამიანებისთვისაა, გადაეცი (timezone: "Asia/Tbilisi"), სხვაგვარად ადგილობრივი დროით 01:00-ზე გაკეთებული შეკვეთა წინა დღეში მოხვდება.
$project, $addFields, $unwind და $count#
ოთხი პატარა stage $match-სა და $group-ის გარშემო გარდაქმნის უმეტეს ნაწილს აკეთებს.
$project ველებს ტოვებს, აქრობს ან ითვლის. { $project: { _id: 0, customerId: 1, total: 1 } } მხოლოდ ამ ორ ველს დააბრუნებს. $addFields (და მისი ალიასი $set) გამოთვლილ ველებს ამატებს და დანარჩენს ინარჩუნებს, რაც pipeline-ის შუაში ჩვეულებრივ სწორედ ის არის, რაც გჭირდება.
$unwind მასივის ველს აქცევს თითო ელემენტზე თითო დოკუმენტად. ასე აგრეგირებ items-ზე:
db.orders.aggregate([ { $match: { status: "paid" } }, { $unwind: "$items" }, { $group: { _id: "$items.sku", units: { $sum: "$items.qty" }, revenue: { $sum: { $multiply: ["$items.qty", "$items.price"] } } } }, { $sort: { revenue: -1 } }, { $limit: 5 }])$unwind დოკუმენტებს ამრავლებს, ამიტომ $match-ის შემდეგ დადე და გაზრდას მიაქციე ყურადღება: ერთი მილიონი შეკვეთის კოლექცია, თითოში ათი პოზიციით, ათ მილიონ დოკუმენტად იქცევა, რომლებიც შემდეგ stage-ში გადიან. დოკუმენტები, რომლებშიც მასივი არ არის ან ცარიელია, იკარგება, თუ არ დაწერე { $unwind: { path: "$items", preserveNullAndEmptyArrays: true } }.
$count stage-ია, რომელიც ერთ დოკუმენტს გასცემს შემავალი დოკუმენტების რაოდენობით: { $count: "paidOrders" }. $sort, $limit და $skip ისევე მუშაობს, როგორც find()-ში, ერთი განსხვავებით, რომელიც მოგვიანებით მნიშვნელოვანია: $sort, რომელსაც პირდაპირ $limit მოსდევს, top-k დალაგებად ოპტიმიზდება და მეხსიერებაში მხოლოდ k დოკუმენტს ინახავს.
$lookup: კოლექციების გაერთიანება#
$lookup MongoDB-ის left outer join-ია. თითოეული შემომავალი დოკუმენტისთვის სხვა კოლექციიდან შესაბამის დოკუმენტებს იღებს და მასივად აბამს:
db.orders.aggregate([ { $match: { status: "paid" } }, { $lookup: { from: "customers", localField: "customerId", foreignField: "_id", as: "customer" } }, { $unwind: "$customer" }, { $project: { total: 1, "customer.name": 1, "customer.plan": 1 } }])as ყოველთვის მასივია, მაშინაც კი, როცა ზუსტად ერთი დოკუმენტი ემთხვევა, ამიტომ მოსდევს $unwind. თუ გამოტოვებ, მიიღებ customer: [ { ... } ]-ს, რაც თითქმის ყველას ერთხელ აკვირვებს. თუ არცერთი დოკუმენტი არ ემთხვევა, მასივი ცარიელია და ზემოთა $unwind ასეთ შეკვეთას გააქრობს. დაამატე preserveNullAndEmptyArrays: true, თუ გინდა, რომ შეკვეთა ნაკლული კლიენტის შემთხვევაშიც გადარჩეს.
სამი წესი $lookup-ს სწრაფად ინახავს:
- დააინდექსე გარე ველი. თითოეული შემომავალი დოკუმენტისთვის
$lookupმეორე კოლექციაზე query-ს ასრულებს._id-ზე ინდექსი უკვე არის.orders-შიcustomerId-ზე (როცა საპირისპირო მიმართულებით აერთებ) არ არის და ინდექსის გარეშე თითოეული ძიება მთელ კოლექციას ათვალიერებს: ათასი შემომავალი დოკუმენტი ასი ათასი გარე დოკუმენტის წინააღმდეგ ასი მილიონი შედარებაა. - ჯერ შეამცირე შეყვანა. ყოველი დოკუმენტი, რომელიც
$lookup-მდე აღწევს, ერთ query-ად ჯდება.$matchდა$limitმის წინ დადე და არასდროს მის შემდეგ. - გააერთიანე პატარა მხრიდან. დაიწყო რამდენიმე დოკუმენტით, რომლებიც ფილტრმა გადარჩინა, და დიდ კოლექციაში ინდექსით ეძებო, იაფია. პირიქით - არა.
როცა შეერთებული დოკუმენტების გაფილტვრა ან გარდაქმნა გჭირდება, გამოიყენე pipeline-ის ფორმა და ლოკალური ველები let-ით გადაეცი:
db.customers.aggregate([ { $match: { plan: "pro" } }, { $lookup: { from: "orders", let: { cid: "$_id" }, pipeline: [ { $match: { $expr: { $eq: ["$customerId", "$$cid"] }, status: "paid" } }, { $project: { _id: 0, total: 1, createdAt: 1 } }, { $sort: { createdAt: -1 } }, { $limit: 3 } ], as: "recentOrders" } }])ინდექსის გამოყენება pipeline-ფორმიან $lookup-ში უფრო შეზღუდულია, ვიდრე მარტივ ფორმაში. ველზე $expr-ით ტოლობას შეუძლია ინდექსის გამოყენება, მაგრამ explain()-ით გადაამოწმე და ნუ დაუშვებ; ამაზე ქვემოთ. შეერთებისთვის, რომელსაც მუდმივად ასრულებ, პასუხი ხშირად უფრო სწრაფი $lookup კი არა, განსხვავებული სქემაა: ჩააშენე ის, რასაც ყოველთვის ერთად კითხულობ, როგორც აღწერილია სტატიაში MongoDB-ის სქემა და ინდექსები. $lookup კარგი ინსტრუმენტია ანგარიშებისთვის და ძვირი - ვებ-მოთხოვნის ცხელი გზისთვის.
შეერთებული მასივი ასევე ერთი დოკუმენტის შიგნით ცხოვრობს, ამიტომ მასზე 16 MB-იანი დოკუმენტის ლიმიტი მოქმედებს. თუ კლიენტს მისი ყველა შეკვეთა $limit-ის გარეშე დაუკავშირე, ადრე თუ გვიან query იმ კლიენტზე ჩავარდება, ვისაც ყველაზე მეტი შეკვეთა აქვს.
რომელი stage იყენებს ინდექსს#
ინდექსს შეხება მხოლოდ pipeline-ის დასაწყისის stage-ებს შეუძლიათ, და არა ყველას:
| Stage | ინდექსის გამოყენება |
|---|---|
$match დასაწყისში | დიახ, როგორც find()-ში |
$sort დასაწყისში ან ინდექსიან $match-ის უშუალოდ შემდეგ | დიახ, თუ დალაგებას ინდექსი იძლევა |
$group | არა, მაგრამ $sort $first-თან ან $last-თან დალაგების გასაღების ინდექსს გამოიყენებს |
$lookup | გარე კოლექციაში ძიებები მის ინდექსებს იყენებს |
$project, $addFields, $unwind | არა - ისინი დოკუმენტებს გარდაქმნიან |
ნებისმიერი stage $group-ის ან $project-ის შემდეგ | არა - ეს უკვე კოლექციის დოკუმენტები აღარ არის |
მიზეზი მარტივია: ინდექსი შენახულ დოკუმენტებზე აგებული სტრუქტურაა. როგორც კი stage ნაკადის ფორმას ცვლის, ინდექსს მისათითებელი აღარაფერი რჩება. ამიტომ "match, sort, group, lookup" ნაგულისხმევი თანმიმდევრობაა, ხოლო pipeline, რომელიც $project-ით ან $addFields-ით იწყება და მხოლოდ მერე ფილტრავს, გამაფრთხილებელი ნიშანია.
pipeline-ისთვის, რომელიც ტოლობით ფილტრავს და ალაგებს, იგივე compound ინდექსის წესები მოქმედებს, რაც find()-ისთვის: ჯერ ტოლობის ველები, მერე დალაგება, მერე დიაპაზონი. ველები როგორ დაალაგო, ESR წესის განმარტებაშია. ზემოთა გადახდილი შეკვეთების თარიღით ანგარიშისთვის:
db.orders.createIndex({ status: 1, createdAt: -1 })ემსახურება როგორც status-ზე $match-ს, ისე createdAt-ზე დიაპაზონს და დოკუმენტებს უკვე თარიღით დალაგებულს აბრუნებს, თუ pipeline ამას ითხოვს.
როგორ წავიკითხოთ explain() pipeline-ისთვის
ნუ გამოიცნობ, გამოიყენა თუ არა ინდექსი. ჰკითხე:
db.orders.explain("executionStats").aggregate([ { $match: { status: "paid", createdAt: { $gte: ISODate("2026-10-01") } } }, { $group: { _id: "$customerId", revenue: { $sum: "$total" } } }])გამოსავალში იპოვე შემავალი stage. IXSCAN ინდექსის სახელით ნიშნავს, რომ ინდექსი გამოიყენა. COLLSCAN ნიშნავს, რომ ყველა დოკუმენტი წაიკითხა. შეადარე totalDocsExamined და nReturned პირველ stage-ზე: 500 000 დოკუმენტის დათვალიერება 800-ის დასაბრუნებლად ნიშნავს, რომ ფილტრი შენი ინდექსისთვის არასელექტიურია ან ინდექსი საერთოდ არ არის. $lookup-იან pipeline-ში explain-ის გამოსავალი გარე კოლექციაზე წვდომას ცალკე აჩვენებს და იქ COLLSCAN არის foreignField-ზე ნაკლული ინდექსი.
თუ პირველი stage $cursor-ად იკითხება queryPlanner ბლოკით, ეს ის ნაწილია, რომელიც query ძრავას გადაეცა და მხოლოდ მას შეუძლია ინდექსით მომსახურება. ყველაფერი, რაც მის შემდეგაა ჩამოთვლილი, უკვე აგრეგაციის ფენაში შესრულდა.
მარტივი explain() (დეტალურობის დონის გარეშე) მხოლოდ გეგმავს და pipeline-ს არასდროს ასრულებს, ეს უსაფრთხო არჩევანია production ბაზის წინააღმდეგ. executionStats pipeline-ს ასრულებს, ამიტომ დიდ კოლექციაზე გაუშვი ასლზე ან $limit-ით, რომელიც სხვა დროს არ დაგჭირდებოდა.
100 MB-ის ლიმიტი და allowDiskUse#
pipeline-ის თითოეულ ბლოკირებად stage-ს შეუძლია 100 მეგაბაიტამდე RAM გამოიყენოს. ბლოკირებადია stage, რომელსაც შედეგის გაცემამდე მთელი შეყვანის ნახვა სჭირდება: $group, $sort (როცა დალაგებას ინდექსი არ იძლევა), $bucket, $bucketAuto, $setWindowFields დიდ partition-ებზე და რამდენიმე სხვა. Stage-ები, რომლებიც დოკუმენტებს ნაკადად ატარებენ, როგორიცაა $match, $project და $addFields, ლიმიტში არ ითვლება. ლიმიტი თითო stage-ზეა და არა მთელ pipeline-ზე, ამიტომ pipeline-მა $group-ით და მის შემდეგ $sort-ით თითოეულზე 100 MB შეიძლება გამოიყენოს.
როცა stage მას გადააჭარბებს, ქცევა სერვერის ვერსიასა და allowDiskUse პარამეტრზეა დამოკიდებული:
- MongoDB 6.0-სა და უფრო ახალში სერვერის პარამეტრი
allowDiskUseByDefaultარისtrue, თუ განთავსებამ არ გამორთო, ამიტომ stage, რომელიც 100 MB-ს ცდება, დროებით ფაილებს დისკზე წერს და აგრძელებს, უბრალოდ უფრო ნელა. - უფრო ძველ ვერსიებში ან იქ, სადაც
allowDiskUseByDefaultარისfalse, stage ჩავარდება შეცდომით, როგორიცააSort exceeded memory limit of 104857600 bytes, but did not opt in to external sortingანExceeded memory limit for $group, but didn't allow external sort. გადაეციallowDiskUse: trueპარამეტრად, რომ დისკზე გადაყრის უფლება მისცე.
db.orders.aggregate( [ { $group: { _id: "$customerId", orders: { $push: "$_id" } } }, { $sort: { _id: 1 } } ], { allowDiskUse: true })driver-ში იგივე პარამეტრი aggregate()-ის options არგუმენტში მიდის. ზუსტი დაწერილობა შენი driver-ის დოკუმენტაციაში ნახე, რადგან ის ენის კონვენციებს მისდევს.
allowDiskUse სამაშველო ბადეა და არა გამოსწორება. ის მკაცრ წარუმატებლობას ნელ query-ად აქცევს: stage დალაგებულ მონაკვეთებს ბაზის გზის ქვეშ _tmp დირექტორიაში წერს და შემდეგ აერთიანებს, რაც დისკის I/O-ს ჯდება და პატარა გეგმაზე კონკურენციას უწევს ყველაფერს, რასაც სერვერი სხვას აკეთებს. მას ასევე მონაცემთა დირექტორიაში თავისუფალი ადგილი სჭირდება. pipeline, რომელიც რეგულარულად ყრის დისკზე, რაღაცას გეუბნება და ჩვეულებრივი წამალი, თანმიმდევრობით, ასეთია:
- გადაიტანე
$matchუფრო ადრე ან დაამატე მისთვის საჭირო ინდექსი, რომ ბლოკირებად stage-მდე ნაკლები დოკუმენტი მივიდეს. $group-ის ან$sort-ის წინ$project-ით მოაშორე ველები, რომლებიც არ გჭირდება. Stage მთელ დოკუმენტებს ინახავს და 4 KB-იანი დოკუმენტი 40-ბაიტიანი საჭირო ველით ასჯერ უფრო ძვირი ჯდება, ვიდრე უნდა.$sort-ის შემდეგ პირდაპირ დადე$limit, რომ მხოლოდ პირველი რამდენიმე შენარჩუნდეს.$push: "$$ROOT"და მსგავსი ჩაანაცვლე პატარა accumulator-ით, რომელიც ერთ რიცხვს ინახავს.- ანგარიშისთვის, რომელსაც ყოველდღე მილიონობით დოკუმენტზე უშვებ, წინასწარ გამოთვალე ის შემაჯამებელ კოლექციაში
$merge-ით (ან$out-ით) გრაფიკით და შეჯამება წაიკითხე.
ორი ლიმიტი 100 MB-ის რიცხვისგან განცალკევებულია და allowDiskUse მათზე არაფერს აკეთებს. ერთი დოკუმენტი 16 MB-ს ვერ გადააჭარბებს, იქნება ეს შენახული დოკუმენტი, ზედმეტად გაზრდილი ჯგუფი თუ $lookup-ის შედეგი. ხოლო $facet, რომელიც ერთსა და იმავე შეყვანაზე რამდენიმე ქვე-pipeline-ს უშვებს, გამოსავალს ერთ დოკუმენტში ინახავს, ამიტომ მისი მთელი შედეგი 16 MB-ში უნდა ჩაეტიოს.
100 MB-ის ლიმიტი ასევე არ ნიშნავს, რომ სერვერს თითო stage-ზე 100 MB თავისუფალი მეხსიერება სჭირდება: ეს ჭერია და პატარა $group რამდენიმე კილობაიტს იყენებს. მაგრამ 1 GB მეხსიერების გეგმაზე რამდენიმე მომხმარებელს, რომელიც ერთდროულად დიდ ანგარიშებს უშვებს, შეუძლია მეტი დაიკავოს, ვიდრე WiredTiger-ის cache-სა და ოპერაციულ სისტემას აქვს. ზომების განყოფილება სტატიაში MongoDB-ის სქემა და ინდექსები განმარტავს, როგორ იყოფენ მეხსიერებას cache და პროცესის დანარჩენი ნაწილი.
pipeline-ის ანალიზი: ნელიდან სწრაფამდე#
აიღე თვიური შემოსავლის ანგარიში კლიენტის სახელითა და გეგმით, დაწერილი ისე, როგორც ყველაზე ადვილად იწერება:
db.orders.aggregate([ { $lookup: { from: "customers", localField: "customerId", foreignField: "_id", as: "customer" } }, { $unwind: "$customer" }, { $match: { status: "paid", createdAt: { $gte: ISODate("2026-10-01") } } }, { $group: { _id: "$customer.plan", revenue: { $sum: "$total" }, orders: { $sum: 1 } } }, { $sort: { revenue: -1 } }])ის სწორ პასუხს აბრუნებს და სამი თვალსაზრისით არასწორია. $match $lookup-ის შემდეგ დგას, ამიტომ შეერთება ყველა შეკვეთაზე სრულდება, რაც ოდესმე გაკეთებულა. $match-ის ველები შეკვეთებს ეკუთვნის, ამიტომ optimiser ხშირად შეძლებს მის წინ გადატანას, მაგრამ ეს გადაწერა თვითონ უნდა გააკეთო. და $lookup კლიენტის მთელ დოკუმენტს აკოპირებს ყველა შეკვეთაზე $group-მდე, რომელსაც მხოლოდ plan სჭირდება.
გადაწერილი:
db.orders.aggregate([ { $match: { status: "paid", createdAt: { $gte: ISODate("2026-10-01") } } }, { $group: { _id: "$customerId", revenue: { $sum: "$total" }, orders: { $sum: 1 } } }, { $lookup: { from: "customers", localField: "_id", foreignField: "_id", as: "customer" } }, { $unwind: "$customer" }, { $group: { _id: "$customer.plan", revenue: { $sum: "$revenue" }, orders: { $sum: "$orders" } } }, { $sort: { revenue: -1 } }])ფილტრი { status: 1, createdAt: -1 } ინდექსს იყენებს. პირველი $group ნაკადს შეერთებამდე ერთ დოკუმენტამდე ამცირებს თითო გადამხდელ კლიენტზე, ამიტომ $lookup ერთხელ სრულდება კლიენტზე და არა შეკვეთაზე. მეორე $group მათ გეგმების მიხედვით აჯამებს. გაუშვი explain("executionStats") ორივეზე და განსხვავება totalDocsExamined-სა და თითო stage-ის დროში გამოჩნდება.
აგრეგაციების უსაფრთხოდ გაშვება ცოცხალ ბაზაზე#
რამდენიმე ჩვევა ანგარიშს არ აძლევს საშუალებას, საიტი თან წაიყოლოს.
- ჯერ შეზღუდულ შეყვანაზე სცადე. დამუშავებისას
{ $limit: 1000 }პირდაპირ$match-ის შემდეგ დადე და მოხსენი, როცა შედეგი სწორია. - დროის ლიმიტი დააყენე.
maxTimeMSბრძანებაზე გაქცეულ pipeline-ს შეაჩერებს:db.orders.aggregate([...], { maxTimeMS: 20000 }). ანგარიში, რომელიც ოც წამში ვერ სრულდება, ვებ-მოთხოვნა არ უნდა იყოს. - იპოვე და გააჩერე, რაც მუშაობს.
db.currentOp()ოპერაციებს ჩამოთვლის,db.killOp(opid)ერთ-ერთს აჩერებს, როგორც აღწერილია profiler-ის განყოფილებაში სტატიაში MongoDB-ის სქემა და ინდექსები. - ანგარიშებისთვის ცალკე მომხმარებელი გამოიყენე. მიეცი მას
readროლი ერთ ბაზაზე, რომ pipeline-ში შეცდომამ$out-ს ან$merge-ს ცოცხალ კოლექციაში ვერ მიაღწიოს. რომელი როლი აირჩიო, აღწერს სტატია MongoDB-ის მომხმარებლები და როლები. - ნებისმიერ `$out`-ამდე ან `$merge`-მდე backup გააკეთე.
$outსამიზნე კოლექციას ცვლის, როცა pipeline სრულდება. თუ სამიზნე შენთვის ძვირფასია, ჯერ გააკეთე mongodump.
RE:NODE-ზე MongoDB-ის გეგმას თან მოჰყვება შენთვის გენერირებული მონაცემები შესასვლელად და backup-ის სლოტები, რომლებსაც ხელით ან გრაფიკით Schedules ჩანართიდან უშვებ, და ეს ჩაწერის pipeline-მდე backup-ის გასაკეთებლად გონივრული გზაა. გეგმის მეხსიერება მკაცრი ლიმიტია: სერვერი, რომელსაც ის ამოეწურა, swap-ში გადასვლის ნაცვლად ჩერდება და სუფთად გადაიტვირთება, ამიტომ პატარა გეგმაზე ძალიან დიდი მეხსიერებაში $group მიზეზია, ჯერ pipeline გადახედო და მერე გეგმა გაზარდო. გეგმები MongoDB-ის ბაზების ჰოსტინგის გვერდზეა ჩამოთვლილი.
პრობლემების გადაჭრა#
`Exceeded memory limit for $group, but didn't allow external sort.` $group 100 MB-ს გასცდა. გადაეცი { allowDiskUse: true }, შემდეგ კი რეალური გამოსწორება გააკეთე: უფრო ადრე გაფილტრე, გამოუყენებელი ველები project-ით მოაშორე, $push-ით მთელი დოკუმენტები ნუ ჩაყრი.
`Sort exceeded memory limit of 104857600 bytes`. იგივე ლიმიტი $sort-ზე, რომლის უკან ინდექსი არ დგას. დაამატე დალაგებასთან შესაბამისი ინდექსი ან მის შემდეგ დადე $limit.
`BSONObj size: ... is invalid` ან "document exceeds maximum size 16777216". ჯგუფი ან $lookup-ის მასივი 16 MB-ზე დიდი გახდა. შეზღუდე, რასაც აგროვებ, უფრო ვიწრო pipeline-ით გააერთიანე ან მონაცემები გადააწყვე.
Pipeline არაფერს აბრუნებს და შეცდომას არ აჩვენებს. ჩვეულებრივ ველი, რომელიც ამ stage-ზე უკვე არ არსებობს, $unwind ნაკლულ ველზე ან სტრიქონად შენახული თარიღი. შეამოკლე pipeline პირველ stage-მდე, გამოსავალი შეამოწმე და stage-ები ერთ-ერთად დაამატე.
`$lookup` ძალიან ნელია. გაუშვი explain("executionStats") და გარე კოლექციაზე COLLSCAN მოძებნე. დააინდექსე foreignField და შემომავალი დოკუმენტების რაოდენობა შეამცირე.
`$match` ნელია ინდექსის მიუხედავად. ალბათ ის პირველი stage არ არის, ან ფილტრში ტიპები არ ემთხვევა (რიცხვი სტრიქონის წინააღმდეგ), ან ის უარყოფით ან წინა wildcard-იან რეგულარულ გამოსახულებას იწყება. explain()-ში IXSCAN-ის საზღვრები ნახე.
შედეგები გაშვებებს შორის განსხვავდება. $group ჯგუფებს რაიმე კონკრეტული თანმიმდევრობით არ აბრუნებს, $first და $last კი შეყვანის თანმიმდევრობაზეა დამოკიდებული. თუ შედეგი თანმიმდევრობაზეა დამოკიდებული, წინ $sort დადე.
FAQ#
aggregation pipeline უფრო სწრაფია, ვიდრე find()?
თავისთავად არა. მარტივი ფილტრისთვის find() და pipeline მხოლოდ $match-ით ერთსა და იმავე სამუშაოს აკეთებენ. Pipeline უფრო სწრაფია, ვიდრე იგივე ამოცანა აპლიკაციის კოდში, რადგან დაჯგუფება და გაერთიანება სერვერის შიგნით ხდება და ქსელში მხოლოდ შედეგი მიდის.
იყენებს თუ არა $match ინდექსს?
დიახ, როცა ის პირველი stage-ია ან მის წინ მხოლოდ ისეთი stage-ები დგას, რომელთა გავლითაც optimiser მას წინ გადაიტანს. $match $group-ის, ველების გადარქმევიანი $project-ის ან $unwind-ის შემდეგ გარდაქმნილ დოკუმენტებზე მუშაობს და კოლექციის ინდექსს ვერ გამოიყენებს.
რას აკეთებს allowDiskUse?
ბლოკირებად stage-ს საშუალებას აძლევს, 100 MB-ის მეხსიერების ლიმიტის გადაჭარბებისას დროებითი ფაილები დისკზე ჩაწეროს და არ ჩავარდეს. query სრულდება, მაგრამ უფრო ნელა. MongoDB 6.0-სა და უფრო ახალში სერვერის ნაგულისხმევი უკვე ამის უფლებას იძლევა, თუ ადმინისტრატორმა allowDiskUseByDefault არ შეცვალა.
შეიძლება ორზე მეტი კოლექციის გაერთიანება?
დიახ, $lookup stage-ების ჯაჭვით, მაგრამ თითოეული თითო შემომავალ დოკუმენტზე query-ს ამატებს, ამიტომ ფასი მრავლდება. თუ ცხელ query-ზე ორზე ან სამზე მეტს აჯაჭვავ, მონაცემები, სავარაუდოდ, ერთ დოკუმენტში უნდა იყოს.
რით განსხვავდება $lookup SQL JOIN-ისგან?
ეს left outer join-ია, რომელიც დამთხვევებს აბრუნებს მასივად თითოეული დოკუმენტის შიგნით და არა დამატებით სტრიქონებად. ის თითო შემომავალ დოკუმენტზე ერთ ძიებას ასრულებს და ორი ძალიან დიდი კოლექციის სრული გაერთიანებისთვის კარგად არ ვარგა. SQL ბაზები სწორედ ამისთვისაა აგებული; თუ შენი მონაცემები ძირითადად რელაციურია, ჯერ წაიკითხე PostgreSQL თუ MongoDB.
როდისაა წინასწარ გამოთვლა აგრეგაციაზე უკეთესი?
როცა ერთი და იგივე ძვირი ანგარიში მონაცემების ცვლილებაზე გაცილებით ხშირად იკითხება. გაუშვი pipeline გრაფიკით $merge-ით შემაჯამებელ კოლექციაში და აპლიკაციამ ის წაიკითხოს. დაშბორდი, რომელიც გვერდის ყოველ ჩატვირთვაზე მილიონ დოკუმენტს თავიდან აგრეგირებს, ჩვეულებრივი შეცდომაა.


