Tahap $unwind memecah satu dokumen yang memuat array menjadi beberapa dokumen, yaitu satu dokumen untuk setiap elemen arraynya. Mekanisme ini menjadikan isi array dapat disaring, dihitung, dan dikelompokkan seperti dokumen biasa pada tahap-tahap setelahnya. Topik ini membahas bentuk dasar $unwind, penanda posisi elemen, perilaku pada array kosong, serta pola analisis $unwind yang dirangkai dengan $group. Data contohnya berupa koleksi siswa pada basis data sekolah yang memuat daftar hobi dalam bentuk array.
Memecah array menjadi satu dokumen per elemen#
Tahap $unwind menerima satu argumen berupa field path yang diawali $, misalnya "$hobi", dan mengalikan setiap dokumen masukan sebanyak jumlah elemen array yang dimilikinya. Setiap dokumen keluaran memuat salinan seluruh field asli, sedangkan field arraynya sudah digantikan oleh satu elemen. Field lain seperti nama dan kelas diduplikasi apa adanya ke setiap keluaran, karena satu baris asli kini mewakili beberapa baris hasil.
Jalankan mongosh, pilih basis data kerja, lalu isi koleksi siswa dengan sepuluh dokumen berikut. Dua dokumen terakhir sengaja dibuat khusus, yaitu siswa dengan hobi berupa array kosong dan siswa tanpa field hobi, untuk menguji perilaku $unwind.
use sekolahswitched to db sekolahdb.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"] },
{ nama: "Indra Gunawan", kelas: "8A", umur: 14, hobi: [] },
{ nama: "Joko Prasetyo", kelas: "8B", umur: 15 }
]){
acknowledged: true,
insertedIds: {
'0': ObjectId('6ae893774a8fff9c948de666'),
'1': ObjectId('6ae893774a8fff9c948de667'),
'2': ObjectId('6ae893774a8fff9c948de668'),
'3': ObjectId('6ae893774a8fff9c948de669'),
'4': ObjectId('6ae893774a8fff9c948de66a'),
'5': ObjectId('6ae893774a8fff9c948de66b'),
'6': ObjectId('6ae893774a8fff9c948de66c'),
'7': ObjectId('6ae893774a8fff9c948de66d'),
'8': ObjectId('6ae893774a8fff9c948de66e'),
'9': ObjectId('6ae893774a8fff9c948de66f')
}
}Pecahkan array hobi menjadi satu dokumen per hobi, lalu bentuk keluarannya dengan $project agar ringkas.
db.siswa.aggregate([
{ $unwind: "$hobi" },
{ $project: { _id: 0, nama: 1, hobi: 1 } }
])[
{ nama: 'Ali Rahman', hobi: 'catur' },
{ nama: 'Ali Rahman', hobi: 'futsal' },
{ nama: 'Budi Santoso', hobi: 'futsal' },
{ nama: 'Citra Melati', hobi: 'seni tari' },
{ nama: 'Citra Melati', hobi: 'catur' },
{ nama: 'Dewi Lestari', hobi: 'vokal' },
{ nama: 'Eka Pratama', hobi: 'futsal' },
{ nama: 'Eka Pratama', hobi: 'renang' },
{ nama: 'Fitri Handayani', hobi: 'seni tari' },
{ nama: 'Gilang Saputra', hobi: 'renang' },
{ nama: 'Gilang Saputra', hobi: 'catur' },
{ nama: 'Gilang Saputra', hobi: 'futsal' },
{ nama: 'Hana Puspita', hobi: 'vokal' },
{ nama: 'Hana Puspita', hobi: 'seni tari' }
]Keluarannya memuat 14 baris, sedangkan koleksi asalnya memuat 10 dokumen. Jumlah itu diperoleh dari penjumlahan panjang seluruh array hobi, yaitu dua hobi milik Ali Rahman, satu milik Budi Santoso, dan seterusnya. Nama yang sama muncul beberapa kali karena dokumen asalnya diduplikasi per elemen.
Urutan baris keluaran mengikuti urutan elemen pada array asalnya, dan baris-baris dari dokumen yang sama tetap berdampingan. Field lain seperti kelas dan umur tetap terbawa pada setiap baris hasil $unwind, sehingga baris yang sudah dipecah masih dapat disaring menurut data induknya. Bentuk satu baris per elemen inilah yang menjadikan array setara dengan tabel detail pada model relasional.
Menandai posisi elemen dengan includeArrayIndex#
Bentuk panjang $unwind berupa dokumen opsi yang memungkinkan penandaan posisi elemen pada keluaran melalui opsi includeArrayIndex. Nilainya berupa nama field baru, dan field tersebut diisi nomor posisi elemen dalam array asalnya, mulai dari 0. Pipeline berikut memecah seluruh hobi, menyaring baris renang, lalu menampilkan posisi hobi tersebut pada array asalnya.
db.siswa.aggregate([
{ $unwind: { path: "$hobi", includeArrayIndex: "posisiHobi" } },
{ $match: { hobi: "renang" } },
{ $project: { _id: 0, nama: 1, hobi: 1, posisiHobi: 1 } }
])[
{ nama: 'Eka Pratama', hobi: 'renang', posisiHobi: Long('1') },
{ nama: 'Gilang Saputra', hobi: 'renang', posisiHobi: Long('0') }
]Eka Pratama memiliki renang pada posisi kedua arraynya sehingga posisiHobi bernilai 1, sedangkan renang milik Gilang Saputra berada di awal array sehingga nilainya 0. Nomor posisi tersebut bertipe bilangan bulat 64 bit, sehingga mongosh menampilkannya sebagai Long. Informasi posisi berguna ketika urutan elemen membawa makna, misalnya peringkat pada daftar.
Posisi pada array hanya bermakna apabila urutan penulisannya terjaga, misalnya daftar peringkat atau langkah bernomor. Apabila urutan elemen tidak mengikuti pola yang bermakna, field posisi sebaiknya tidak diandalkan sebagai penanda. Kasus semacam itu lebih tepat ditangani dengan field posisi yang ditulis eksplisit pada datanya.
Menjaga dokumen tanpa elemen#
Opsi preserveNullAndEmptyArrays dengan nilai true mengubah perilaku bawaan $unwind. Dokumen dengan array kosong maupun tanpa field array tetap diteruskan ke keluaran tanpa field array, sementara dokumen berisi elemen tetap dipecah seperti biasa. Pipeline berikut menampilkan kedua siswa kasus khusus pada data contoh setelah $unwind berjalan.
db.siswa.aggregate([
{ $unwind: { path: "$hobi", preserveNullAndEmptyArrays: true } },
{ $match: { nama: { $in: ["Indra Gunawan", "Joko Prasetyo"] } } },
{ $project: { _id: 0, nama: 1, kelas: 1, hobi: 1 } }
])[
{ nama: 'Indra Gunawan', kelas: '8A' },
{ nama: 'Joko Prasetyo', kelas: '8B' }
]Keluarannya dua dokumen yang tidak memuat field hobi sama sekali, bukan dokumen dengan hobi berupa array kosong. Perbedaan itu penting bagi kode pembaca yang memeriksa keberadaan field, karena field yang tidak ada dan field yang kosong diperlakukan berbeda oleh operator query. Pilih opsi ini apabila dokumen tanpa elemen tetap harus dilaporkan, misalnya pada laporan keikutsertaan kegiatan.
Opsi ini juga mencakup dokumen yang fieldnya ada tetapi bernilai null, karena nilai null memang tidak memiliki elemen untuk dipecah. Dengan demikian, array kosong, nilai null, dan field yang tidak ada diperlakukan setara oleh preserveNullAndEmptyArrays. Ketiganya lolos sebagai satu baris tanpa dipecah, sedangkan array berisi elemen tetap dipecah sebagaimana biasa.
Menganalisis elemen array dengan $unwind dan $group#
Manfaat $unwind paling terasa ketika tahapnya dirangkai dengan $group. Array yang sudah dipecah menjadi satu baris per elemen dapat dihitung dan dikelompokkan menurut isi elemennya, sesuatu yang tidak dapat dilakukan pada array utuh. Pipeline berikut menghitung jumlah pemilik setiap hobi di seluruh koleksi.
db.siswa.aggregate([
{ $unwind: "$hobi" },
{ $group: { _id: "$hobi", jumlahSiswa: { $sum: 1 } } },
{ $sort: { jumlahSiswa: -1, _id: 1 } }
])[
{ _id: 'futsal', jumlahSiswa: 4 },
{ _id: 'catur', jumlahSiswa: 3 },
{ _id: 'seni tari', jumlahSiswa: 3 },
{ _id: 'renang', jumlahSiswa: 2 },
{ _id: 'vokal', jumlahSiswa: 2 }
]Hasilnya lima hobi berbeda beserta banyaknya pemiliknya, dan futsal menempati urutan teratas dengan empat siswa. Pola tiga tahap ini, yaitu $unwind, $group, lalu $sort, adalah bentuk analisis array yang paling umum pada MongoDB. Tanpa $unwind, satu $group hanya melihat array utuhnya dan tidak dapat menghitung setiap elemennya secara terpisah.
Letakkan $match sebelum $unwind apabila hanya sebagian dokumen yang dianalisis, karena setiap baris yang tidak perlu dipecah membebani tahap-tahap sesudahnya. Setelah $unwind, syarat pada satu elemen ditulis seperti syarat field biasa karena elemen sudah menggantikan array pada barisnya. Posisi $match di sepanjang pipeline semacam ini memengaruhi jumlah pekerjaan setiap tahap, sehingga kebiasaan menyaring sedini mungkin tetap berlaku.
Ringkasan#
$unwindmengalikan setiap dokumen sebanyak jumlah elemen arraynya, dengan satu elemen menggantikan array pada tiap keluaran.- Opsi
includeArrayIndexmenambahkan field nomor posisi elemen, dan opsipreserveNullAndEmptyArraysmenjaga dokumen tanpa elemen. - Array kosong dan field yang tidak ada dibuang secara bawaan, sehingga jumlah keluaran dapat lebih sedikit dari dugaan.
- Rangkaian
$unwind,$group, dan$sortadalah pola baku untuk menghitung sesuatu per elemen array.
Referensi#
- $unwind — MongoDB Manual — Dokumentasi tahap pemecah array beserta seluruh opsinya.
- Aggregation Pipeline — MongoDB Manual — Dokumentasi konsep pipeline agregasi beserta aliran data antartahap.
- Aggregation Pipeline Operators — MongoDB Manual — Indeks seluruh operator tahap dan ekspresi agregasi.