Array adalah cara MongoDB menyimpan “banyak nilai dalam satu document”, tetapi bentuk ini menyulitkan analisis per elemen: $group melihat satu document Bumi dengan dua tag, bukan dua kejadian tag. Tahap $unwind memecah kebuntuan itu — setiap elemen array menjadi satu document utuh, dan array-nya menghilang dari bentuk. Setelah dipecah, elemen bisa dihitung, dikelompokkan, dan disaring seperti document biasa. Chapter ini membedah mekaniknya, opsi-opsinya, dan pola analitik klasik yang mengikutinya.
Memecah Array Menjadi Document per Elemen #
Bentuk paling sederhana $unwind hanya berisi path menuju array — dan path itu wajib diawali tanda $. Kita mulai dari data toko buku yang sudah dikenal:
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 }
}db.buku.aggregate([ { $unwind: "$tag" }, { $project: { _id: 0, judul: 1, tag: 1 } } ])Output yang Dihasilkan:
[
{ judul: 'Laut Bercerita', tag: 'sastra' },
{ judul: 'Laut Bercerita', tag: 'novel' },
{ judul: 'Bumi', tag: 'novel' },
{ judul: 'Bumi', tag: 'fantasi' },
{
judul: 'Supernova: Ksatria, Puteri, dan Bintang Jatuh',
tag: 'novel'
},
{
judul: 'Supernova: Ksatria, Puteri, dan Bintang Jatuh',
tag: 'sains'
},
{ judul: 'Filosofi Teras', tag: 'filsafat' },
{ judul: 'Filosofi Teras', tag: 'pengembangan diri' },
{ judul: 'Atomic Habits', tag: 'pengembangan diri' },
{ judul: 'Atomic Habits', tag: 'kebiasaan' },
{ judul: 'Kamus Bahasa Indonesia', tag: 'referensi' },
{ judul: 'Kamus Bahasa Indonesia', tag: 'bahasa' }
]Enam document berubah menjadi dua belas: setiap buku muncul sekali per tag-nya, dan field tag kini berisi satu nilai, bukan array. Seluruh field lain (judul, harga, stok) ikut terduplikasi ke setiap salinan. Lupa tanda $ adalah kesalahan paling umum pemula — dan server menolaknya dengan pesan yang cukup membantu:
db.buku.aggregate([ { $unwind: "tag" } ])Output yang Dihasilkan:
Uncaught MongoServerError[Location28818]: path option to $unwind stage should be prefixed with a '$': tagincludeArrayIndex: Nomor Urut Elemen #
$unwind menerima bentuk object untuk opsi tambahan. Opsi pertama, includeArrayIndex, menambahkan field berisi posisi elemen di array aslinya — berguna saat urutan elemen bermakna, seperti catatan penjualan bulanan:
db.buku.aggregate([
{ $match: { judul: "Laut Bercerita" } },
{ $unwind: { path: "$terjual", includeArrayIndex: "urutan" } },
{ $project: { _id: 0, terjual: 1, urutan: 1 } }
])Output yang Dihasilkan:
[
{ terjual: { bulan: 'Januari', jumlah: 14 }, urutan: Long('0') },
{ terjual: { bulan: 'Februari', jumlah: 9 }, urutan: Long('1') },
{ terjual: { bulan: 'Maret', jumlah: 11 }, urutan: Long('2') }
]Tiga elemen array terjual menjadi tiga document, dan field urutan (angka Long 64-bit) mencatat posisi aslinya: 0, 1, 2. Perhatikan bahwa $unwind bekerja pada array of documents sekuat pada array of scalars — elemen terjual yang berupa document utuh tetap utuh menjadi nilai field.
Document dengan Array Kosong #
Perilaku bawaan $unwind terhadap array kosong layak diwaspadai: document-nya hilang dari hasil. Kamus Bahasa Indonesia memiliki terjual: [], dan array kosong tidak punya elemen untuk dipecah — buktinya:
db.buku.aggregate([
{ $match: { judul: "Kamus Bahasa Indonesia" } },
{ $unwind: "$terjual" },
{ $project: { _id: 0, judul: 1, terjual: 1 } }
]).toArray()Output yang Dihasilkan:
[]Untuk analisis penjualan, hilangnya buku tanpa penjualan justru menyesatkan — nol penjualan adalah informasi. Opsi preserveNullAndEmptyArrays mempertahankan document semacam itu:
db.buku.aggregate([
{ $unwind: { path: "$terjual", preserveNullAndEmptyArrays: true } },
{ $project: { _id: 0, judul: 1, terjual: 1 } }
])Output yang Dihasilkan:
[
{ judul: 'Laut Bercerita', terjual: { bulan: 'Januari', jumlah: 14 } },
{ judul: 'Laut Bercerita', terjual: { bulan: 'Februari', jumlah: 9 } },
{ judul: 'Laut Bercerita', terjual: { bulan: 'Maret', jumlah: 11 } },
{ judul: 'Bumi', terjual: { bulan: 'Januari', jumlah: 21 } },
{ judul: 'Bumi', terjual: { bulan: 'Februari', jumlah: 17 } },
{ judul: 'Bumi', terjual: { bulan: 'Maret', jumlah: 25 } },
{
judul: 'Supernova: Ksatria, Puteri, dan Bintang Jatuh',
terjual: { bulan: 'Januari', jumlah: 7 }
},
{
judul: 'Supernova: Ksatria, Puteri, dan Bintang Jatuh',
terjual: { bulan: 'Februari', jumlah: 5 }
},
{ judul: 'Filosofi Teras', terjual: { bulan: 'Januari', jumlah: 12 } },
{ judul: 'Filosofi Teras', terjual: { bulan: 'Februari', jumlah: 15 } },
{ judul: 'Filosofi Teras', terjual: { bulan: 'Maret', jumlah: 10 } },
{ judul: 'Atomic Habits', terjual: { bulan: 'Januari', jumlah: 18 } },
{ judul: 'Atomic Habits', terjual: { bulan: 'Februari', jumlah: 22 } },
{ judul: 'Atomic Habits', terjual: { bulan: 'Maret', jumlah: 16 } },
{ judul: 'Kamus Bahasa Indonesia' }
]Empat belas document penjualan tetap utuh, dan baris terakhir kini memuat Kamus Bahasa Indonesia — tetap hadir meski tanpa field terjual, karena memang tidak ada elemen yang bisa ditempelkan. Opsi yang sama juga menyelamatkan document yang field-nya hilang atau null, bukan hanya array kosong.
Pola Klasik: $unwind + $group untuk Analitik #
Kekuatan sesungguhnya muncul saat $unwind disusul $group. Pertanyaan “tag mana yang paling banyak dipakai, dan oleh buku apa saja?” dijawab tiga tahap:
db.buku.aggregate([
{ $unwind: "$tag" },
{ $group: { _id: "$tag", jumlahBuku: { $sum: 1 }, daftarBuku: { $push: "$judul" } } },
{ $sort: { jumlahBuku: -1, _id: 1 } }
])Output yang Dihasilkan:
[
{
_id: 'novel',
jumlahBuku: 3,
daftarBuku: [
'Laut Bercerita',
'Bumi',
'Supernova: Ksatria, Puteri, dan Bintang Jatuh'
]
},
{
_id: 'pengembangan diri',
jumlahBuku: 2,
daftarBuku: [ 'Filosofi Teras', 'Atomic Habits' ]
},
{
_id: 'bahasa',
jumlahBuku: 1,
daftarBuku: [ 'Kamus Bahasa Indonesia' ]
},
{ _id: 'fantasi', jumlahBuku: 1, daftarBuku: [ 'Bumi' ] },
{ _id: 'filsafat', jumlahBuku: 1, daftarBuku: [ 'Filosofi Teras' ] },
{ _id: 'kebiasaan', jumlahBuku: 1, daftarBuku: [ 'Atomic Habits' ] },
{
_id: 'referensi',
jumlahBuku: 1,
daftarBuku: [ 'Kamus Bahasa Indonesia' ]
},
{
_id: 'sains',
jumlahBuku: 1,
daftarBuku: [ 'Supernova: Ksatria, Puteri, dan Bintang Jatuh' ]
},
{
_id: 'sastra',
jumlahBuku: 1,
daftarBuku: [ 'Laut Bercerita' ]
}
]Tag novel memimpin dengan tiga buku, disusul pengembangan diri dengan dua. Tanpa $unwind, laporan semacam ini mustahil dibuat di sisi server. Pola yang sama bekerja untuk data penjualan — total penjualan per bulan:
db.buku.aggregate([
{ $unwind: "$terjual" },
{ $group: { _id: "$terjual.bulan", totalTerjual: { $sum: "$terjual.jumlah" } } },
{ $sort: { _id: 1 } }
])Output yang Dihasilkan:
[
{ _id: 'Februari', totalTerjual: 68 },
{ _id: 'Januari', totalTerjual: 72 },
{ _id: 'Maret', totalTerjual: 62 }
]Setelah $unwind, elemen terjual menjadi nilai field biasa sehingga "$terjual.bulan" dan "$terjual.jumlah" bisa dibaca seperti field bertitik mana pun. Perhatikan satu jebakan pada hasilnya: $sort: { _id: 1 } mengurutkan nama bulan secara alfabetis, sehingga Februari mendahului Januari. Untuk urutan kronologis, simpan bulan sebagai angka (1–12) atau tanggal, bukan nama.
Rangkuman #
$unwind: "$field"memecah array menjadi satu document per elemen; path wajib berawalan$, dan seluruh field lain ikut terduplikasi.includeArrayIndexmenambahkan nomor posisi elemen;preserveNullAndEmptyArraysmenyelamatkan document dengan array kosong,null, atau field yang hilang.- Pola
$unwind+$groupadalah pintu masuk analitik per elemen: frekuensi tag, total penjualan per bulan, dan sejenisnya. $unwindmenggandakan jumlah document — saring dulu, pecah kemudian.