Setiap pipeline serius hampir selalu dimulai dari pertanyaan yang sama: document mana yang relevan? Tahap $match menjawabnya dengan menyaring aliran document di dalam pipeline. Kekuatannya terletak pada dua hal: sintaksnya persis sama dengan query find() yang sudah kita kuasai, dan bila diletakkan di awal pipeline, tahap ini dapat memakai index persis seperti query biasa. Chapter ini membedah keduanya, lalu menutup dengan posisi kedua $match yang tidak kalah penting: setelah $group.
Sintaks yang Sama dengan Query find() #
Seluruh kosakata query operator — perbandingan, logika, array, field bersarang — berlaku apa adanya di dalam $match. Kita mulai dari data yang sama dengan Chapter sebelumnya:
use latihan_agregasi
db.dropDatabase()Output yang Dihasilkan:
switched to db latihan_agregasi
{ ok: 1, dropped: 'latihan_agregasi' }db.buku.insertMany([
{ _id: 1, judul: "Laut Bercerita", penulis: "Leila S. Chudori", harga: 98000, kategori: "Fiksi", tag: ["sastra", "novel"], stok: 12, tahun_terbit: 2017, terjual: [ { bulan: "Januari", jumlah: 14 }, { bulan: "Februari", jumlah: 9 }, { bulan: "Maret", jumlah: 11 } ] },
{ _id: 2, judul: "Bumi", penulis: "Tere Liye", harga: 89000, kategori: "Fiksi", tag: ["novel", "fantasi"], stok: 20, tahun_terbit: 2014, terjual: [ { bulan: "Januari", jumlah: 21 }, { bulan: "Februari", jumlah: 17 }, { bulan: "Maret", jumlah: 25 } ] },
{ _id: 3, judul: "Supernova: Ksatria, Puteri, dan Bintang Jatuh", penulis: "Dee Lestari", harga: 95000, kategori: "Fiksi", tag: ["novel", "sains"], stok: 6, tahun_terbit: 2001, terjual: [ { bulan: "Januari", jumlah: 7 }, { bulan: "Februari", jumlah: 5 } ] },
{ _id: 4, judul: "Filosofi Teras", penulis: "Henry Manampiring", harga: 89000, kategori: "Nonfiksi", tag: ["filsafat", "pengembangan diri"], stok: 8, tahun_terbit: 2018, terjual: [ { bulan: "Januari", jumlah: 12 }, { bulan: "Februari", jumlah: 15 }, { bulan: "Maret", jumlah: 10 } ] },
{ _id: 5, judul: "Atomic Habits", penulis: "James Clear", harga: 115000, kategori: "Nonfiksi", tag: ["pengembangan diri", "kebiasaan"], stok: 15, tahun_terbit: 2018, terjual: [ { bulan: "Januari", jumlah: 18 }, { bulan: "Februari", jumlah: 22 }, { bulan: "Maret", jumlah: 16 } ] },
{ _id: 6, judul: "Kamus Bahasa Indonesia", penulis: "Badan Bahasa", harga: 55000, kategori: "Referensi", tag: ["referensi", "bahasa"], stok: 30, tahun_terbit: 2016, terjual: [] }
])Output yang Dihasilkan:
{
acknowledged: true,
insertedIds: { '0': 1, '1': 2, '2': 3, '3': 4, '4': 5, '5': 6 }
}Sekarang bandingkan dua perintah berikut — keduanya mencari buku dengan harga minimal 90000:
db.buku.find({ harga: { $gte: 90000 } }, { _id: 0, judul: 1, harga: 1 })
db.buku.aggregate([
{ $match: { harga: { $gte: 90000 } } },
{ $project: { _id: 0, judul: 1, harga: 1 } }
])Output yang Dihasilkan:
[
{ judul: 'Laut Bercerita', harga: 98000 },
{
judul: 'Supernova: Ksatria, Puteri, dan Bintang Jatuh',
harga: 95000
},
{ judul: 'Atomic Habits', harga: 115000 }
]
[
{ judul: 'Laut Bercerita', harga: 98000 },
{
judul: 'Supernova: Ksatria, Puteri, dan Bintang Jatuh',
harga: 95000
},
{ judul: 'Atomic Habits', harga: 115000 }
]Filter { harga: { $gte: 90000 } } tidak berubah sedikit pun ketika pindah dari find() ke $match. Migrasi pengetahuan kita dari Section Operasi CRUD ke dunia aggregation praktis tanpa biaya.
Menggabungkan beberapa kondisi pun mengikuti pola yang sudah dikenal — beberapa kondisi dalam satu object berarti DAN, $or untuk ATAU:
db.buku.aggregate([
{ $match: { kategori: "Nonfiksi", harga: { $lt: 100000 } } },
{ $project: { _id: 0, judul: 1, kategori: 1, harga: 1 } }
])
db.buku.aggregate([
{ $match: { $or: [ { kategori: "Referensi" }, { tahun_terbit: { $lt: 2010 } } ] } },
{ $project: { _id: 0, judul: 1, kategori: 1, tahun_terbit: 1 } }
])Output yang Dihasilkan:
[ { judul: 'Filosofi Teras', harga: 89000, kategori: 'Nonfiksi' } ]
[
{
judul: 'Supernova: Ksatria, Puteri, dan Bintang Jatuh',
kategori: 'Fiksi',
tahun_terbit: 2001
},
{
judul: 'Kamus Bahasa Indonesia',
kategori: 'Referensi',
tahun_terbit: 2016
}
]Query pertama mencari Nonfiksi di bawah 100000 — hanya Filosofi Teras lolos. Query kedua menggabungkan dua syarat dengan $or: kategori Referensi atau terbit sebelum 2010.
$match di Awal Pipeline Agar Bisa Memakai Index #
$match paling bernilai di posisi pertama. Selama document yang mengalir masih berbentuk asli collection-nya, MongoDB mengeksekusi $match (dan $sort yang mengikutinya) melalui mesin query yang sama dengan find() — termasuk kemampuan memakai index. Kita buktikan dengan membuat index pada harga lalu memeriksa rencana eksekusinya:
db.buku.createIndex({ harga: 1 })
db.buku.getIndexes()Output yang Dihasilkan:
harga_1
[
{
v: 2,
key: { _id: 1 },
name: '_id_',
collation: { locale: 'simple' }
},
{
v: 2,
key: { harga: 1 },
name: 'harga_1',
collation: { locale: 'simple' }
}
]Index kedua bernama harga_1 kini terdaftar. .explain() memperlihatkan apa yang terjadi ketika $match dan $sort sama-sama menyentuh field yang berindex:
db.buku.explain().aggregate([
{ $match: { harga: { $gte: 90000 } } },
{ $sort: { harga: 1 } }
]).queryPlanner.winningPlanOutput yang Dihasilkan:
{
isCached: false,
stage: 'FETCH',
nss: 'latihan_agregasi.buku',
inputStage: {
stage: 'IXSCAN',
nss: 'latihan_agregasi.buku',
keyPattern: { harga: 1 },
indexName: 'harga_1',
isMultiKey: false,
multiKeyPaths: { harga: [] },
isUnique: false,
isSparse: false,
isPartial: false,
indexVersion: 2,
direction: 'forward',
indexBounds: { harga: [ '[90000, inf]' ] }
}
}Mari kita bedah temuan penting dari rencana eksekusi ini:
stage: 'IXSCAN'denganindexName: 'harga_1': penyaringan$matchdilakukan lewat index, bukan pemindaian collection.- Tidak ada tahap
SORT: karena$sortmengurutkan berdasarkanharga— field yang sama dengan index — urutan hasil sudah disediakan oleh index itu sendiri. Satu index melayani dua tahap sekaligus. indexBounds: { harga: [ '[90000, inf]' ) }: batas pencarian index persis seperti pada queryfind().
Sebaliknya, $match pada field tanpa index tetap berjalan tetapi memakai pemindaian collection penuh — dan inilah mengapa posisi tahap begitu menentukan. $match yang diletakkan setelah $project atau $group bekerja pada field hasil transformasi, sehingga index pada collection asli tidak lagi berlaku.
$match Setelah $group: Posisi “HAVING” #
Posisi kedua $match justru mengambil peran khusus: menyaring hasil perhitungan. Pertanyaan seperti “kategori mana yang total stoknya minimal 30?” tidak bisa dijawab $match di awal — total stok baru ada setelah $group menghitungnya:
db.buku.aggregate([
{ $group: { _id: "$kategori", totalStok: { $sum: "$stok" } } },
{ $match: { totalStok: { $gte: 30 } } }
])Output yang Dihasilkan:
[
{ _id: 'Fiksi', totalStok: 38 },
{ _id: 'Referensi', totalStok: 30 }
]Nonfiksi (total stok 23) tersaring setelah perhitungan selesai. Pembaca yang berasal dari dunia SQL akan mengenali pola ini: $match sebelum $group berperan seperti WHERE, sedangkan $match setelah $group berperan seperti HAVING. Perbedaannya, di MongoDB keduanya memakai nama tahap yang sama — hanya posisinya yang membedakan maknanya.
Rangkuman #
$matchmenyaring document dengan sintaks persis sama dengan queryfind(); seluruh query operator berlaku.- Di awal pipeline,
$matchdapat memakai index;$match+$sortpada field yang sama bahkan menghapus tahap SORT dari rencana eksekusi. $matchsetelah$project/$groupbekerja pada field hasil transformasi dan kehilangan akses index collection asli.$matchsetelah$groupadalah padananHAVING— menyaring hasil perhitungan agregat.