Pipeline yang benar secara hasil belum tentu benar secara biaya. Dua pipeline bisa mengembalikan angka yang sama, tetapi yang satu memindai seluruh collection sementara yang lain berhenti di index setelah beberapa document. Selisihnya pada data sejuta baris bukan hitungan persen, melainkan hitungan detik versus menit. Chapter ini merangkum tuas optimasi utama aggregation: urutan tahap, pemakaian index, pengendalian memori, dan cara membaca rencana eksekusi pipeline — ditutup daftar kebiasaan yang layak dijadikan refleks.
Peta materi Chapter ini:
- Urutan tahap: saring dan pangkas sedini mungkin.
$match+$sortsebagai pasangan penikmat index.- Batas memori 100 MB dan
allowDiskUse. - Membaca rencana eksekusi
aggregate()denganexplain().
Urutan Tahap: Saring Dulu, Kerja Kemudian #
Aturan emasnya satu: setiap document yang berhasil dibuang lebih awal tidak akan pernah diproses oleh tahap berikutnya. Karena itu $match dan $limit sebaiknya berada sedini mungkin, sementara $group dan $project yang berat diletakkan setelah data mengecil.
Siapkan data uji yang sama seperti Chapter sebelumnya:
use latihan_agregasi2
db.buku.drop()
db.buku.insertMany([
{ judul: "Laut Bercerita", penulis: "Leila S. Chudori", harga: 98000, kategori: "fiksi", terjual: 120, tag: ["fiksi", "sastra", "penghargaan"] },
{ judul: "Bumi", penulis: "Tere Liye", harga: 89000, kategori: "fiksi", terjual: 250, tag: ["fiksi", "remaja", "fantasi"] },
{ judul: "Filosofi Teras", penulis: "Henry Manampiring", harga: 89000, kategori: "nonfiksi", terjual: 180, tag: ["filsafat", "pengembangan diri"] },
{ judul: "Atomic Habits", penulis: "James Clear", harga: 115000, kategori: "nonfiksi", terjual: 310, tag: ["pengembangan diri", "terjemahan"] },
{ judul: "Rumah Kaca", penulis: "Tere Liye", harga: 72000, kategori: "fiksi", terjual: 120, tag: ["fiksi", "remaja"] },
{ judul: "Bumi Manusia", penulis: "Pramoedya A. Toer", harga: 105000, kategori: "sastra", terjual: 150, tag: ["sastra", "sejarah"] },
{ judul: "Hujan Bulan Juni", penulis: "Sapardi Djoko Damono", harga: 65000, kategori: "puisi", terjual: 75, tag: ["puisi", "sastra"] },
{ judul: "Ensiklopedia Sains Populer", penulis: "Tim Sains", harga: 250000, kategori: "referensi", terjual: 40, tag: ["referensi", "sains"] }
])Output yang Dihasilkan:
switched to db latihan_agregasi2
true
{
acknowledged: true,
insertedIds: {
'0': ObjectId('6ad3b2f6fd7f9ec03616fde8'),
'1': ObjectId('6ad3b2f6fd7f9ec03616fde9'),
'2': ObjectId('6ad3b2f6fd7f9ec03616fdea'),
'3': ObjectId('6ad3b2f6fd7f9ec03616fdeb'),
'4': ObjectId('6ad3b2f6fd7f9ec03616fdec'),
'5': ObjectId('6ad3b2f6fd7f9ec03616fded'),
'6': ObjectId('6ad3b2f6fd7f9ec03616fdee'),
'7': ObjectId('6ad3b2f6fd7f9ec03616fdef')
}
}Mari kita bandingkan dua pipeline yang hasilnya sama persis — total terjual untuk buku nonfiksi. Versi pertama menghitung dulu, menyaring kemudian:
db.buku.aggregate([
{ $group: { _id: "$kategori", total: { $sum: "$terjual" } } },
{ $match: { _id: "nonfiksi" } }
])Output yang Dihasilkan:
[ { _id: 'nonfiksi', total: 490 } ]Versi kedua menyaring lebih dulu, baru menghitung:
db.buku.aggregate([
{ $match: { kategori: "nonfiksi" } },
{ $group: { _id: "$kategori", total: { $sum: "$terjual" } } }
])Output yang Dihasilkan:
[ { _id: 'nonfiksi', total: 490 } ]Hasil akhirnya sama, tetapi biayanya tidak. Mari kita bedah:
- Versi pertama:
$groupmemproses kedelapan document — membangun grup untukfiksi,nonfiksi,sastra,puisi, danreferensi— sebelum$matchmembuang empat grup yang tidak diminta. Pada collection satu juta baris dengan ribuan kategori,$groupmengalokasikan hash table raksasa untuk kemudian dibuang. - Versi kedua:
$matchdi posisi pertama membuat$grouphanya menerima dua document. Mesin eksekusi bahkan bisa memakai indexkategoribila tersedia, dan seluruh tahap setelahnya bekerja dengan data yang sudah kecil. - Prinsip umum: taruh
$matchdan$limitdi depan,$skipdan$projectdi tengah,$groupdan$unwindsesudah penyaringan selama logika memungkinkan.
$match + $sort: Pasangan Penikmat Index #
$match di tahap pertama memakai index seperti query find() biasa — dan begitu pula $sort yang berada tepat setelahnya. Pasangan ini penting karena $sort di posisi lanjutan (misalnya setelah $group) harus mengurutkan hasil di memori, sementara $match + $sort di awal dapat memanfaatkan index yang urut, menghapus langkah pengurutan sama sekali.
Buat index lalu lihat mesin eksekusi memakainya:
db.buku.createIndex({ kategori: 1, terjual: -1 })
db.buku.aggregate([
{ $match: { kategori: "fiksi" } },
{ $sort: { terjual: -1 } },
{ $project: { _id: 0, judul: 1, terjual: 1 } }
])Output yang Dihasilkan:
kategori_1_terjual_-1
[
{ judul: 'Bumi', terjual: 250 },
{ judul: 'Laut Bercerita', terjual: 120 },
{ judul: 'Rumah Kaca', terjual: 120 }
]Index majemuk { kategori: 1, terjual: -1 } melayani keduanya sekaligus: kategori difilter lewat batas index, dan terjual sudah terurut menurun di dalam rentang tersebut. Pola ini — buat index yang meniru bentuk $match + $sort — adalah optimasi berbiaya termurah dengan dampak terbesar pada pipeline analitik.
Batas Memori 100 MB dan allowDiskUse #
Tahap yang harus menampung data di memori — $group, $sort, $bucket, $setWindowFields — dibatasi 100 MB per pipeline secara default. Melebihi itu, pipeline berhenti dengan galat; solusinya dua arah: kecilkan data lebih dulu ($match, $limit, $project), atau izinkan tumpahan ke disk:
db.buku.aggregate(
[ { $sort: { terjual: -1 } }, { $group: { _id: null, termahal: { $max: "$harga" } } } ],
{ allowDiskUse: true }
)Output yang Dihasilkan:
[ { _id: null, termahal: 250000 } ]allowDiskUse: true membuat tahap yang kehabisan memori menulis data sementara ke berkas di direktori _tmp database. Kecepatannya jauh di bawah memori, jadi anggap opsi ini sebagai jaring pengaman, bukan pemecah utama: pipeline yang sehat tetap menyaring dulu agar jarang menyentuh batas ini.
Membaca Rencana Eksekusi Pipeline #
Seperti query, pipeline juga bisa diperiksa rencana eksekusinya — cukup ganti db.buku.aggregate(...) dengan db.buku.explain().aggregate(...):
db.buku.explain().aggregate([
{ $match: { kategori: "fiksi" } },
{ $sort: { terjual: -1 } }
])Output yang Dihasilkan:
{
explainVersion: '1',
queryPlanner: {
namespace: 'latihan_agregasi2.buku',
parsedQuery: { kategori: { '$eq': 'fiksi' } },
queryHash: 'AD3D8A67',
planCacheShapeHash: 'AD3D8A67',
planCacheKey: '82549CEB',
optimizationTimeMillis: 0,
optimizedPipeline: true,
winningPlan: {
isCached: false,
stage: 'FETCH',
inputStage: {
stage: 'IXSCAN',
keyPattern: { kategori: 1, terjual: -1 },
indexName: 'kategori_1_terjual_-1',
isMultiKey: false,
direction: 'forward',
indexBounds: {
kategori: [ '["fiksi", "fiksi"]' ],
terjual: [ '[MaxKey, MinKey]' ]
}
}
},
rejectedPlans: []
},
command: {
aggregate: 'buku',
pipeline: [ { '$match': { kategori: 'fiksi' } }, { '$sort': { terjual: -1 } } ],
cursor: {},
'$db': 'latihan_agregasi2'
},
ok: 1
}Output di atas dipadatkan agar mudah dibaca — serverInfo, serverParameters, queryShapeHash, dan metadata waktu replikasi dipangkas; nilai queryHash dan planCacheKey juga akan berbeda di server Anda. Empat hal yang perlu dicari:
optimizedPipeline: true: MongoDB mengenali bahwa$match+$sortdi awal pipeline setara dengan query biasa, lalu mengeksekusinya lewat jalur kursor yang sama — inilah sebabnya pipeline seperti ini mendapat seluruh perlakuan index.winningPlanberupaFETCH→IXSCAN: pemindaian memakai indexkategori_1_terjual_-1. Perhatikan bahwa tidak ada tahapSORTsama sekali — index yang tersimpan terurut sudah memenuhi$sort, jadi langkah pengurutan di memori tidak pernah ada.indexBounds:kategoriterkunci di["fiksi", "fiksi"](tepat satu nilai) danterjualdipindai penuh[MaxKey, MinKey]— konsisten dengan filter satu kategori yang hasilnya lalu diurutkan.rejectedPlans: []: tidak ada rencana saingan; bila Anda melihatCOLLSCANdiwinningPlanpadahal index seharusnya terpakai, periksa apakah$matchbenar-benar berada di tahap pertama pipeline.
Kebiasaan yang Layak Dijadikan Refleks #
Kebiasaan yang Layak Dijadikan Refleks #
| Kebiasaan | Alasan |
|---|---|
$match selalu di tahap pertama bila memungkinkan | Memakai index dan memangkas data sebelum kerja berat |
$limit sedini mungkin setelah $sort | Menghentikan pemindaian lebih awal pada pola top-N |
$project membuang field besar sebelum $group | Menurunkan ukuran document yang mengalir |
Hindari $unwind sebelum $match pada array besar | Menggandakan jumlah document sebelum disaring |
Rancang index meniru $match + $sort | Menghapus langkah sort di memori |
allowDiskUse hanya sebagai jaring pengaman | Disk jauh lebih lambat dari memori |
Uji dengan explain() pada data representatif | Rencana pada 8 document bisa berbeda pada 8 juta |
Rangkuman #
- Saring dan pangkas sedini mungkin:
$matchpertama,$limitdini,$groupsetelah data mengecil. $match+$sortdi awal pipeline diperlakukan seperti query biasa dan bisa dilayani index majemuk sepenuhnya.- Tahap memori (
$group,$sort,$setWindowFields) dibatasi 100 MB; kecilkan datanya, dan pakaiallowDiskUsehanya sebagai jaring pengaman. db.collection.explain().aggregate(...)memperlihatkan tahap$cursortempat index dipakai atau dilewatkan.- Kebiasaan menulis pipeline yang rapi lebih menentukan daripada menyetel server: urutan tahap adalah optimasi termurah yang Anda miliki.