$match dan $project

Menyaring dokumen dengan $match dan membentuk ulang field-nya dengan $project di dalam pipeline.

Daftar isi

Tahap $match menyaring dokumen yang mengalir di dalam pipeline, sedangkan tahap $project membentuk ulang field dokumen tersebut. Topik ini membahas kedua tahap tersebut secara berurutan, mulai dari penyaringan dengan operator query, pemilihan field, sampai penambahan field hasil perhitungan. Data contohnya berupa koleksi siswa pada basis data sekolah, dan setiap pipeline dijalankan di dalam sesi mongosh.

Menyaring dokumen dengan $match#

Tahap $match menyaring aliran dokumen dengan sintaks filter query yang sama seperti find, sehingga seluruh operator query yang sudah dikenal dapat dipakai kembali. Argumennya berupa satu dokumen filter, dan hanya dokumen yang cocok yang diteruskan ke tahap berikutnya. Dokumen yang lolos tidak diubah sama sekali, sehingga bentuk keluaran $match persis sama dengan masukannya.

Jalankan mongosh, pilih basis data kerja, lalu isi koleksi siswa dengan delapan dokumen berikut.

mongosh
use sekolah
text Keluaran
switched to db sekolah
mongosh
db.siswa.insertMany([
  { nama: "Ali Rahman", kelas: "8A", umur: 14, hobi: ["catur", "futsal"] },
  { nama: "Budi Santoso", kelas: "8A", umur: 13, hobi: ["futsal"] },
  { nama: "Citra Melati", kelas: "8A", umur: 14, hobi: ["seni tari", "catur"] },
  { nama: "Dewi Lestari", kelas: "8B", umur: 13, hobi: ["vokal"] },
  { nama: "Eka Pratama", kelas: "8B", umur: 14, hobi: ["futsal", "renang"] },
  { nama: "Fitri Handayani", kelas: "8B", umur: 15, hobi: ["seni tari"] },
  { nama: "Gilang Saputra", kelas: "8C", umur: 14, hobi: ["renang", "catur", "futsal"] },
  { nama: "Hana Puspita", kelas: "8C", umur: 13, hobi: ["vokal", "seni tari"] }
])
text Keluaran
{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId('6ae89375be4c5730e98de666'),
    '1': ObjectId('6ae89375be4c5730e98de667'),
    '2': ObjectId('6ae89375be4c5730e98de668'),
    '3': ObjectId('6ae89375be4c5730e98de669'),
    '4': ObjectId('6ae89375be4c5730e98de66a'),
    '5': ObjectId('6ae89375be4c5730e98de66b'),
    '6': ObjectId('6ae89375be4c5730e98de66c'),
    '7': ObjectId('6ae89375be4c5730e98de66d')
  }
}

Saring koleksi dengan satu syarat sederhana terlebih dahulu.

mongosh
db.siswa.aggregate([
  { $match: { kelas: "8B" } }
])
text Keluaran
[
  {
    _id: ObjectId('6ae89375be4c5730e98de669'),
    nama: 'Dewi Lestari',
    kelas: '8B',
    umur: 13,
    hobi: [ 'vokal' ]
  },
  {
    _id: ObjectId('6ae89375be4c5730e98de66a'),
    nama: 'Eka Pratama',
    kelas: '8B',
    umur: 14,
    hobi: [ 'futsal', 'renang' ]
  },
  {
    _id: ObjectId('6ae89375be4c5730e98de66b'),
    nama: 'Fitri Handayani',
    kelas: '8B',
    umur: 15,
    hobi: [ 'seni tari' ]
  }
]

Hasilnya tiga siswa kelas 8B lengkap dengan seluruh field aslinya. Syaratnya juga boleh menggabungkan operator perbandingan seperti $gte dengan pencocokan elemen array seperti pada find. Pipeline berikut mengambil siswa berumur minimal 14 tahun yang hobinya memuat catur.

mongosh
db.siswa.aggregate([
  { $match: { umur: { $gte: 14 }, hobi: "catur" } }
])
text Keluaran
[
  {
    _id: ObjectId('6ae89375be4c5730e98de666'),
    nama: 'Ali Rahman',
    kelas: '8A',
    umur: 14,
    hobi: [ 'catur', 'futsal' ]
  },
  {
    _id: ObjectId('6ae89375be4c5730e98de668'),
    nama: 'Citra Melati',
    kelas: '8A',
    umur: 14,
    hobi: [ 'seni tari', 'catur' ]
  },
  {
    _id: ObjectId('6ae89375be4c5730e98de66c'),
    nama: 'Gilang Saputra',
    kelas: '8C',
    umur: 14,
    hobi: [ 'renang', 'catur', 'futsal' ]
  }
]

Hasilnya tiga siswa, yaitu Ali Rahman, Citra Melati, dan Gilang Saputra. Kedua syarat pada filter yang sama harus terpenuhi oleh dokumen yang sama, sama seperti perilaku logika implisit pada find.

Membentuk ulang field dengan $project#

Tahap $project menentukan field mana yang dibawa ke keluaran pipeline. Pada mode penyertaan (inclusion), field yang ditulis dengan nilai 1 dinyatakan dipertahankan, sedangkan field lain dibuang. Field _id merupakan pengecualian karena ikut tersertakan secara otomatis, dan nilainya harus ditulis 0 secara eksplisit apabila tidak diinginkan.

mongosh
db.siswa.aggregate([
  { $match: { kelas: "8A" } },
  { $project: { _id: 0, nama: 1, kelas: 1 } }
])
text Keluaran
[
  { nama: 'Ali Rahman', kelas: '8A' },
  { nama: 'Budi Santoso', kelas: '8A' },
  { nama: 'Citra Melati', kelas: '8A' }
]

Mode pengecualian (exclusion) berlaku sebaliknya, yaitu field bernilai 0 dibuang dan seluruh field lain dipertahankan. Pipeline berikut menyaring siswa kelas 8B, lalu membuang field hobi saja.

mongosh
db.siswa.aggregate([
  { $match: { kelas: "8B" } },
  { $project: { hobi: 0 } }
])
text Keluaran
[
  {
    _id: ObjectId('6ae89375be4c5730e98de669'),
    nama: 'Dewi Lestari',
    kelas: '8B',
    umur: 13
  },
  {
    _id: ObjectId('6ae89375be4c5730e98de66a'),
    nama: 'Eka Pratama',
    kelas: '8B',
    umur: 14
  },
  {
    _id: ObjectId('6ae89375be4c5730e98de66b'),
    nama: 'Fitri Handayani',
    kelas: '8B',
    umur: 15
  }
]

Keluarannya masih memuat _id, nama, kelas, dan umur karena mode pengecualian tidak menghapus apa pun selain field yang disebutkan. Kedua mode tersebut tidak boleh dicampur pada satu $project, dan penjelasan lengkapnya ada pada bagian Kesalahan umum.

Apabila mode penyertaan menyebut field yang tidak dimiliki sebuah dokumen, $project tidak menimbulkan error dan field itu pun tidak muncul pada keluaran dokumen tersebut. Dokumen lain yang memuat field tersebut tetap menampilkan nilai aslinya. Sifat ini menegaskan bahwa $project bekerja per dokumen, bukan menuntut struktur koleksi yang seragam.

Menambahkan field hasil perhitungan#

Selain memilih field, $project dapat membuat field baru dari ekspresi (expression), yaitu rumus yang dihitung dari field dokumen masukan. Nilai field asal ditulis dengan awalan $, misalnya "$nama", sehingga field baru dapat menjadi salinan dengan nama lain. Pipeline berikut mengganti nama field, menghitung jumlah hobi melalui ekspresi $size, dan menyaring siswanya lebih dahulu.

mongosh
db.siswa.aggregate([
  { $match: { kelas: "8C" } },
  { $project: { _id: 0, namaLengkap: "$nama", kelas: 1, jumlahHobi: { $size: "$hobi" } } }
])
text Keluaran
[
  { kelas: '8C', namaLengkap: 'Gilang Saputra', jumlahHobi: 3 },
  { kelas: '8C', namaLengkap: 'Hana Puspita', jumlahHobi: 2 }
]

Field baru bernama namaLengkap menyalin isi nama, sedangkan jumlahHobi menyimpan panjang array hobi yang dihitung oleh $size. Perhatikan bahwa urutan field pada keluaran tidak mengikuti urutan penulisan pada $project. Field asli seperti kelas tetap menempati posisi aslinya, sedangkan field hasil perhitungan diletakkan setelahnya.

Ekspresi aritmetika juga dapat dipakai untuk field numerik. Pipeline berikut menghitung perkiraan tahun lahir dengan $subtract yang mengurangi nilai umur dari tahun 2026.

mongosh
db.siswa.aggregate([
  { $match: { kelas: "8A" } },
  { $project: { _id: 0, nama: 1, umur: 1, tahunLahir: { $subtract: [2026, "$umur"] } } }
])
text Keluaran
[
  { nama: 'Ali Rahman', umur: 14, tahunLahir: 2012 },
  { nama: 'Budi Santoso', umur: 13, tahunLahir: 2013 },
  { nama: 'Citra Melati', umur: 14, tahunLahir: 2012 }
]

Setiap dokumen keluaran kini memuat tahunLahir yang tidak pernah tersimpan di koleksi. Nilai tersebut dihitung ulang pada setiap eksekusi pipeline, sehingga tidak ada risiko data hasil perhitungan menjadi tidak sinkron dengan data aslinya.

Apabila ekspresi merujuk field yang tidak ada pada sebuah dokumen, field hasil perhitungan pada dokumen itu ikut tidak muncul. Pipeline tetap berjalan tanpa pesan error, dan dokumen yang tidak memuat field sumbernya lolos tanpa field baru tersebut. Perilaku ini berbeda dari ekspresi $size yang justru menolak masukan bukan array, sehingga pilihan ekspresi perlu disesuaikan dengan kemungkinan field yang tidak konsisten antardokumen.

Kesalahan umum#

Kesalahan yang paling sering terjadi pada $project adalah mencampur mode penyertaan dan pengecualian dalam satu tahap. Pipeline berikut menyertakan nama sekaligus mengecualikan hobi, dan server menolaknya dengan pesan error.

mongosh
db.siswa.aggregate([
  { $project: { nama: 1, hobi: 0 } }
])
text Keluaran
Uncaught
MongoServerError[Location31254]: Invalid $project :: caused by :: Cannot do exclusion on field hobi in inclusion projection

Pesan error tersebut muncul karena $project harus memilih satu mode saja, yaitu menyertakan atau mengecualikan, supaya bentuk keluarannya tidak rancu. Perbaikannya adalah menuliskan seluruh field yang diinginkan sebagai penyertaan, atau menuliskan seluruh field yang dibuang sebagai pengecualian. Field _id adalah satu-satunya field yang boleh menyertai kedua mode, karena _id: 0 pada mode penyertaan memang diperbolehkan secara khusus.

Kesalahan lain yang lebih ringan adalah lupa menuliskan _id: 0 pada mode penyertaan. Pipeline tetap berjalan tanpa error, tetapi keluaran memuat _id yang tidak diharapkan. Periksa keluaran pipeline sebelum dirangkai ke tahap lain agar bentuk dokumennya sudah benar sejak awal.

Ringkasan#

  • $match menyaring dokumen dengan sintaks filter find dan tidak mengubah isi dokumen yang lolos.
  • $project menyertakan field bernilai 1 atau mengecualikan field bernilai 0, dan _id tersertakan otomatis pada mode penyertaan.
  • Field baru dapat dihitung dari field masukan melalui ekspresi seperti $size dan $subtract.
  • Mode penyertaan dan pengecualian tidak boleh dicampur dalam satu $project, dan pelanggarannya ditolak server dengan error.

Referensi#