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:

js
use latihan_agregasi
db.dropDatabase()

Output yang Dihasilkan:

text
switched to db latihan_agregasi
{ ok: 1, dropped: 'latihan_agregasi' }
js
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:

text
{
  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:

js
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:

text
[
  { 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:

js
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:

text
[ { 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:

js
db.buku.createIndex({ harga: 1 })
db.buku.getIndexes()

Output yang Dihasilkan:

text
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:

js
db.buku.explain().aggregate([
  { $match: { harga: { $gte: 90000 } } },
  { $sort: { harga: 1 } }
]).queryPlanner.winningPlan

Output yang Dihasilkan:

text
{
  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:

  1. stage: 'IXSCAN' dengan indexName: 'harga_1': penyaringan $match dilakukan lewat index, bukan pemindaian collection.
  2. Tidak ada tahap SORT: karena $sort mengurutkan berdasarkan harga — field yang sama dengan index — urutan hasil sudah disediakan oleh index itu sendiri. Satu index melayani dua tahap sekaligus.
  3. indexBounds: { harga: [ '[90000, inf]' ) }: batas pencarian index persis seperti pada query find().

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:

js
db.buku.aggregate([
  { $group: { _id: "$kategori", totalStok: { $sum: "$stok" } } },
  { $match: { totalStok: { $gte: 30 } } }
])

Output yang Dihasilkan:

text
[
  { _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 #

  • $match menyaring document dengan sintaks persis sama dengan query find(); seluruh query operator berlaku.
  • Di awal pipeline, $match dapat memakai index; $match + $sort pada field yang sama bahkan menghapus tahap SORT dari rencana eksekusi.
  • $match setelah $project/$group bekerja pada field hasil transformasi dan kehilangan akses index collection asli.
  • $match setelah $group adalah padanan HAVING — menyaring hasil perhitungan agregat.