$facet

Menghasilkan beberapa bentuk ringkasan dalam satu kali eksekusi pipeline menggunakan $facet.

Daftar isi

Tahap $facet menjalankan beberapa sub-pipeline secara bersamaan terhadap kumpulan dokumen masukan yang sama, sehingga beberapa bentuk ringkasan diperoleh dalam satu kali eksekusi pipeline. Mekanisme ini menggantikan pembuatan beberapa query terpisah ketika seluruh ringkasan dibutuhkan pada satu tampilan, misalnya kartu ringkasan pada halaman pengawasan kepegawaian. Topik ini membahas struktur spesifikasi $facet, kebebasan masing-masing sub-pipeline dalam menyaring dan membentuk data, serta larangan tahap yang berlaku di dalamnya. Data contohnya berupa koleksi pegawai pada basis data pegawai yang memuat kode departemen dan gaji delapan pegawai.

Satu pipeline untuk beberapa ringkasan#

Tahap $facet menerima satu dokumen spesifikasi yang setiap fieldnya menamai satu sub-pipeline berupa array tahap agregasi. Seluruh sub-pipeline menerima dokumen masukan yang sama persis, lalu mengolahnya secara terpisah sampai selesai. Keluaran $facet berupa satu dokumen yang setiap fieldnya memuat array hasil sub-pipeline dengan nama yang sama, sehingga jumlah field keluaran mengikuti jumlah field spesifikasinya. Urutan dokumen di dalam setiap array ditentukan oleh tahap-tahap sub-pipelinenya sendiri, misalnya $sort untuk pengurutan.

Jalankan mongosh, pilih basis data kerja, lalu isi koleksi pegawai dengan delapan dokumen berikut. Gaji dan kode departemen dipilih agar rata-rata per departemen mudah dihitung ulang secara manual.

mongosh
use pegawai
text Keluaran
switched to db pegawai
mongosh
db.pegawai.insertMany([
  { nama: "Ali Rahman", kodeDepartemen: "IT", gaji: 8000000 },
  { nama: "Budi Santoso", kodeDepartemen: "IT", gaji: 7000000 },
  { nama: "Citra Melati", kodeDepartemen: "SDM", gaji: 6500000 },
  { nama: "Dewi Lestari", kodeDepartemen: "SDM", gaji: 6000000 },
  { nama: "Eka Pratama", kodeDepartemen: "KEU", gaji: 7500000 },
  { nama: "Fitri Handayani", kodeDepartemen: "KEU", gaji: 5500000 },
  { nama: "Gilang Saputra", kodeDepartemen: "IT", gaji: 9000000 },
  { nama: "Hana Puspita", kodeDepartemen: "SDM", gaji: 5500000 }
])
text Keluaran
{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId('6ae898e1c469bb582c8de666'),
    '1': ObjectId('6ae898e1c469bb582c8de667'),
    '2': ObjectId('6ae898e1c469bb582c8de668'),
    '3': ObjectId('6ae898e1c469bb582c8de669'),
    '4': ObjectId('6ae898e1c469bb582c8de66a'),
    '5': ObjectId('6ae898e1c469bb582c8de66b'),
    '6': ObjectId('6ae898e1c469bb582c8de66c'),
    '7': ObjectId('6ae898e1c469bb582c8de66d')
  }
}

Pipeline berikut membentuk tiga ringkasan sekaligus, yaitu statistik keseluruhan, rekapitulasi per departemen, dan pegawai dengan gaji tertinggi. Ketiganya dibutuhkan bersamaan pada satu tampilan, sehingga $facet menampung ketiga sub-pipeline tersebut dalam satu tahap.

mongosh
db.pegawai.aggregate([
  { $facet: {
      ringkasan: [
        { $group: { _id: null, jumlahPegawai: { $sum: 1 }, totalGaji: { $sum: "$gaji" }, rataRataGaji: { $avg: "$gaji" } } },
        { $project: { _id: 0, jumlahPegawai: 1, totalGaji: 1, rataRataGaji: 1 } }
      ],
      perDepartemen: [
        { $group: { _id: "$kodeDepartemen", jumlahPegawai: { $sum: 1 }, rataRataGaji: { $avg: "$gaji" } } },
        { $sort: { _id: 1 } },
        { $project: { _id: 0, departemen: "$_id", jumlahPegawai: 1, rataRataGaji: 1 } }
      ],
      gajiTertinggi: [
        { $sort: { gaji: -1, nama: 1 } },
        { $limit: 1 },
        { $project: { _id: 0, nama: 1, gaji: 1 } }
      ]
  } }
])
text Keluaran
[
  {
    ringkasan: [
      { jumlahPegawai: 8, totalGaji: 55000000, rataRataGaji: 6875000 }
    ],
    perDepartemen: [
      { jumlahPegawai: 3, rataRataGaji: 8000000, departemen: 'IT' },
      { jumlahPegawai: 2, rataRataGaji: 6500000, departemen: 'KEU' },
      { jumlahPegawai: 3, rataRataGaji: 6000000, departemen: 'SDM' }
    ],
    gajiTertinggi: [ { nama: 'Gilang Saputra', gaji: 9000000 } ]
  }
]

Keluarannya satu dokumen yang memuat ketiga ringkasan. Field ringkasan berisi satu dokumen statistik keseluruhan dari delapan pegawai, field perDepartemen berisi tiga dokumen sesuai banyaknya kode departemen, dan field gajiTertinggi berisi satu dokumen hasil $sort beserta $limit. Tiga query terpisah menjadi tidak perlu karena semua ringkasan terbentuk dari satu kali pemindaian dokumen masukan.

Letakkan $match sebelum $facet apabila seluruh ringkasan hanya membutuhkan sebagian dokumen koleksi, karena saringan di muka menghemat pekerjaan setiap sub-pipeline. Sebaliknya, saringan yang berbeda antarringkasan ditulis di dalam sub-pipelinenya masing-masing. Kedua tempat itu tidak saling menggantikan, dan pipeline nyata sering memakai keduanya sekaligus.

Sub-pipeline yang saling bebas#

Setiap sub-pipeline boleh membentuk data dengan cara yang berbeda, termasuk menyaring dengan $match sebelum tahap-tahap lainnya. Saringan pada satu sub-pipeline tidak memengaruhi sub-pipeline lain karena keduanya menerima salinan dokumen masukan yang sama. Pipeline berikut menampilkan pegawai bergaji di atas tujuh juta pada satu field, dan jumlah seluruh pegawai pada field lainnya.

mongosh
db.pegawai.aggregate([
  { $facet: {
      gajiDiAtasTujuhJuta: [
        { $match: { gaji: { $gt: 7000000 } } },
        { $sort: { gaji: -1 } },
        { $project: { _id: 0, nama: 1, gaji: 1 } }
      ],
      jumlahSeluruhnya: [ { $count: "total" } ]
  } }
])
text Keluaran
[
  {
    gajiDiAtasTujuhJuta: [
      { nama: 'Gilang Saputra', gaji: 9000000 },
      { nama: 'Ali Rahman', gaji: 8000000 },
      { nama: 'Eka Pratama', gaji: 7500000 }
    ],
    jumlahSeluruhnya: [ { total: 8 } ]
  }
]

Field gajiDiAtasTujuhJuta memuat tiga pegawai yang gajinya melampaui batas saringan, sedangkan field jumlahSeluruhnya tetap menghitung delapan pegawai karena sub-pipelinenya tidak disaring. Perbedaan ini menunjukkan kemandirian setiap sub-pipeline secara utuh. Kemandirian ini juga berlaku ketika tidak ada kecocokan. Sub-pipeline yang saringannya tidak cocok dengan dokumen mana pun menghasilkan array kosong, seperti terlihat pada pipeline berikut dengan batas gaji sepuluh juta.

mongosh
db.pegawai.aggregate([
  { $facet: {
      gajiDiAtasSepuluhJuta: [
        { $match: { gaji: { $gt: 10000000 } } },
        { $project: { _id: 0, nama: 1, gaji: 1 } }
      ],
      jumlahPegawai: [ { $count: "total" } ]
  } }
])
text Keluaran
[
  { gajiDiAtasSepuluhJuta: [], jumlahPegawai: [ { total: 8 } ] }
]

Field gajiDiAtasSepuluhJuta berupa array kosong karena tidak ada pegawai yang gajinya melebihi sepuluh juta, sedangkan field jumlahPegawai tetap berisi hasil hitungannya. Array kosong semacam ini adalah keluaran yang sah, bukan kegagalan pipeline, sehingga kode aplikasi cukup memeriksa kekosongan array tanpa menangani error.

Pola ini paling sering dipakai pada permintaan halaman aplikasi yang menampilkan daftar beserta totalnya sekaligus, misalnya daftar pegawai bergaji tertinggi di samping jumlah seluruh pegawai. Tanpa $facet, kebutuhan tersebut memerlukan dua query yang masing-masing memindai koleksi. Dengan $facet, dokumen masukan dipindai satu kali dan kedua bentuk hasilnya diperoleh dalam satu respons.

Kesalahan umum#

Kesalahan pertama adalah menaruh tahap yang menulis data ke koleksi lain di dalam sub-pipeline. Tahap $out maupun $merge tidak diizinkan pada $facet, dan server menolak pipeline tersebut saat eksekusi.

mongosh
db.pegawai.aggregate([
  { $facet: {
      salinan: [ { $out: "salinanPegawai" } ]
  } }
])
text Keluaran
Uncaught
MongoServerError[Location40600]: $out is not allowed to be used within a $facet stage

Pesan error tersebut muncul karena $facet menjamin keluarannya murni berupa satu dokumen hasil pembacaan, sedangkan $out menulis dokumen ke koleksi lain di akhir pipeline. Perbaikannya adalah memindahkan $out ke pipeline terpisah yang tidak memakai $facet, atau menyimpan hasil $facet di sisi aplikasi bila data tetap perlu diarsipkan.

Kesalahan kedua adalah menulis isi sub-pipeline tanpa tanda kurung array. Field spesifikasi $facet wajib berupa array tahap, sehingga bentuk objek langsung ditolak server sebelum eksekusi dimulai.

mongosh
db.pegawai.aggregate([
  { $facet: {
      jumlahSeluruhnya: { $count: "total" }
  } }
])
text Keluaran
Uncaught
MongoServerError[Location40170]: arguments to $facet must be arrays, jumlahSeluruhnya is type object

Pesan error tersebut menyebut nama field yang bersalah, yaitu jumlahSeluruhnya, beserta tipe yang terbaca. Perbaikannya adalah membungkus tahapnya menjadi array, misalnya jumlahSeluruhnya: [ { $count: "total" } ], sehingga bentuknya konsisten dengan seluruh sub-pipeline lain pada $facet.

Batas lain yang perlu dijaga berada pada ukuran keluarannya. Karena hasil seluruh sub-pipeline disatukan menjadi satu dokumen, gabungan itu tunduk pada batas ukuran dokumen BSON sebesar 16 MB. Ringkasan yang sangat besar sebaiknya dipecah menjadi beberapa tahap $facet terpisah, sedangkan keluaran ringkas berupa statistik dan daftar pendek biasanya berada jauh di bawah batas tersebut.

Ringkasan#

  • $facet menampung beberapa sub-pipeline yang menerima dokumen masukan sama dan diolah secara terpisah.
  • Keluarannya satu dokumen yang setiap fieldnya berupa array hasil sub-pipeline bernama sama, termasuk array kosong untuk sub-pipeline tanpa kecocokan.
  • Setiap sub-pipeline bebas menyaring dan membentuk data, sehingga beberapa ringkasan diperoleh dalam satu kali eksekusi.
  • Tahap $out, $merge, dan $facet bertingkat dilarang di dalam sub-pipeline, dan isi sub-pipeline wajib berupa array.

Referensi#