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:

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 }
}
js
db.buku.aggregate([ { $unwind: "$tag" }, { $project: { _id: 0, judul: 1, tag: 1 } } ])

Output yang Dihasilkan:

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

js
db.buku.aggregate([ { $unwind: "tag" } ])

Output yang Dihasilkan:

text
Uncaught MongoServerError[Location28818]: path option to $unwind stage should be prefixed with a '$': tag

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

js
db.buku.aggregate([
  { $match: { judul: "Laut Bercerita" } },
  { $unwind: { path: "$terjual", includeArrayIndex: "urutan" } },
  { $project: { _id: 0, terjual: 1, urutan: 1 } }
])

Output yang Dihasilkan:

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

js
db.buku.aggregate([
  { $match: { judul: "Kamus Bahasa Indonesia" } },
  { $unwind: "$terjual" },
  { $project: { _id: 0, judul: 1, terjual: 1 } }
]).toArray()

Output yang Dihasilkan:

text
[]

Untuk analisis penjualan, hilangnya buku tanpa penjualan justru menyesatkan — nol penjualan adalah informasi. Opsi preserveNullAndEmptyArrays mempertahankan document semacam itu:

js
db.buku.aggregate([
  { $unwind: { path: "$terjual", preserveNullAndEmptyArrays: true } },
  { $project: { _id: 0, judul: 1, terjual: 1 } }
])

Output yang Dihasilkan:

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

js
db.buku.aggregate([
  { $unwind: "$tag" },
  { $group: { _id: "$tag", jumlahBuku: { $sum: 1 }, daftarBuku: { $push: "$judul" } } },
  { $sort: { jumlahBuku: -1, _id: 1 } }
])

Output yang Dihasilkan:

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

js
db.buku.aggregate([
  { $unwind: "$terjual" },
  { $group: { _id: "$terjual.bulan", totalTerjual: { $sum: "$terjual.jumlah" } } },
  { $sort: { _id: 1 } }
])

Output yang Dihasilkan:

text
[
  { _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.
  • includeArrayIndex menambahkan nomor posisi elemen; preserveNullAndEmptyArrays menyelamatkan document dengan array kosong, null, atau field yang hilang.
  • Pola $unwind + $group adalah pintu masuk analitik per elemen: frekuensi tag, total penjualan per bulan, dan sejenisnya.
  • $unwind menggandakan jumlah document — saring dulu, pecah kemudian.