Halaman produk toko buku selalu menampilkan rating rata-rata. Menghitung rata-rata dari ribuan ulasan pada setiap kunjungan halaman berarti menjalankan agregasi yang sama berulang-ulang — untuk hasil yang jarang berubah. Computed Pattern membaliknya: hasil perhitungan disimpan sebagai field biasa di document, dan yang mahal hanya terjadi saat data sumbernya berubah. Pertanyaan desainnya tinggal satu: kapan hasil itu dihitung ulang.
Masalah: Perhitungan Berulang #
Kita mulai dari kondisi awal yang jujur: rating belum disimpan, dan setiap halaman menghitungnya dari array ulasan:
use latihan_modeling
db.dropDatabase()
db.buku.insertOne({
_id: 101,
judul: "Bumi",
harga: 89000,
ratingRata: 4.5,
jumlahUlasan: 2,
ulasan: [
{ pembaca: "Budi", bintang: 4 },
{ pembaca: "Siti", bintang: 5 }
]
})
db.buku.aggregate([
{ $match: { _id: 101 } },
{ $project: { judul: 1, ratingDihitung: { $round: [{ $avg: "$ulasan.bintang" }, 2] } } }
])
db.buku.findOne({ _id: 101 }, { judul: 1, ratingRata: 1, jumlahUlasan: 1 })Output yang Dihasilkan:
switched to db latihan_modeling
{ ok: 1, dropped: 'latihan_modeling' }
{ acknowledged: true, insertedId: 101 }
[ { _id: 101, judul: 'Bumi', ratingDihitung: 4.5 } ]
{ _id: 101, judul: 'Bumi', ratingRata: 4.5, jumlahUlasan: 2 }Dua jalur memberi angka yang sama: $avg menghitung ulang dari array, sedangkan findOne membaca field ratingRata yang sudah tersimpan. Jalur pertama membayar biaya agregasi setiap kali dijalankan; jalur kedua membayar hampir nol. Ketika query pertama dieksekusi ribuan kali per menit dan ulasan baru datang beberapa kali per jam, menyimpan hasilnya jelas lebih murah daripada menghitung ulang.
Patokan kasarnya bisa dihitung: kalau halaman dibaca 10.000 kali sehari dan ulasan baru masuk 20 kali sehari, agregasi berjalan 10.000 kali untuk 20 perubahan — rasio 500:1. Semakin miring rasionya, semakin jelas keuntungan menyimpan hasilnya. Kalau rasionya mendekati 1:1 (data berubah setiap kali dibaca), computed pattern justru menambah beban tulis tanpa manfaat.
Menghitung Ulang Saat Event: Cara Inkremental #
Strategi pertama: setiap event yang mengubah sumber (ulasan baru) langsung memperbarui hasil. Rumus inkrementalnya: rata-rata baru = (rata-rata lama × jumlah lama + nilai baru) ÷ (jumlah lama + 1). Update berbentuk aggregation pipeline menjadikan semuanya satu operasi atomic:
db.buku.updateOne(
{ _id: 101 },
[ { $set: {
ratingRata: { $round: [ { $divide: [
{ $add: [ { $multiply: [ "$ratingRata", "$jumlahUlasan" ] }, 3 ] },
{ $add: [ "$jumlahUlasan", 1 ] }
] }, 2 ] },
jumlahUlasan: { $add: [ "$jumlahUlasan", 1 ] },
ulasan: { $concatArrays: [ "$ulasan", [ { pembaca: "Tono", bintang: 3 } ] ] }
} } ]
)
db.buku.findOne({ _id: 101 }, { judul: 1, ratingRata: 1, jumlahUlasan: 1 })Output yang Dihasilkan:
{
acknowledged: true,
insertedId: null,
matchedCount: 1,
modifiedCount: 1,
upsertedCount: 0
}
{ _id: 101, judul: 'Bumi', ratingRata: 4, jumlahUlasan: 3 }Bedah pipeline-nya:
$multiply: ["$ratingRata", "$jumlahUlasan"]— mengembalikan total bintang kumulatif lama (4.5 × 2 = 9).$add ... 3— menambahkan bintang ulasan baru (9 + 3 = 12).$divideoleh$add: ["$jumlahUlasan", 1]— membagi dengan jumlah baru (12 ÷ 3 = 4);$roundmenjaga dua desimal.$concatArrays— ulasan baru ikut dimasukkan ke array sumber dalam operasi yang sama, sehinggaratingRatadanulasantidak pernah tercecer.
Menghitung Ulang dari Sumber: Cara Batch #
Field computed adalah duplikasi turunan, dan duplikasi bisa basang — bug aplikasi atau migrasi gagal bisa meninggalkan ratingRata yang salah. Obatnya recomputing dari sumber kebenaran:
db.buku.updateOne({ _id: 101 }, { $set: { ratingRata: 2 } })
db.buku.findOne({ _id: 101 }, { ratingRata: 1 })Output yang Dihasilkan:
{
acknowledged: true,
insertedId: null,
matchedCount: 1,
modifiedCount: 1,
upsertedCount: 0
}
{ _id: 101, ratingRata: 2 }Nilai 2 adalah kerusakan yang disengaja — seolah bug pernah menulis angka keliru. Tugas latar berkala memperbaikinya dengan menghitung ulang seluruhnya dari array:
db.buku.updateOne(
{ _id: 101 },
[ { $set: {
ratingRata: { $round: [ { $avg: "$ulasan.bintang" }, 2 ] },
jumlahUlasan: { $size: "$ulasan" }
} } ]
)
db.buku.findOne({ _id: 101 }, { judul: 1, ratingRata: 1, jumlahUlasan: 1 })Output yang Dihasilkan:
{
acknowledged: true,
insertedId: null,
matchedCount: 1,
modifiedCount: 1,
upsertedCount: 0
}
{ _id: 101, judul: 'Bumi', ratingRata: 4, jumlahUlasan: 3 }$avg pada path "ulasan.bintang" mengumpulkan seluruh nilai bintang lalu merata-ratakannya, dan $size menghitung ulang jumlahnya — angka 4 dan 3 kembali konsisten dengan sumbernya. Tugas seperti ini biasanya dijadwalkan tiap malam atau tiap jam, cukup rapat untuk menambal jendela basang namun jauh lebih murah daripada agregasi per page view.
Kedua strategi recomputing di atas juga menuntut disiplin tulis: aplikasi tidak boleh mengubah ratingRata secara langsung di jalur lain, karena nilai itu milik pipeline — bukan milik tangan. Satu jalur perubahan per field computed adalah aturan yang membuat audit dan perbaikan drift tetap sederhana.
Event-Driven atau Batch #
| Aspek | Event-driven (inkremental) | Batch (recomputing) |
|---|---|---|
| Kesegaran hasil | Selalu terkini | Tertinggal hingga tugas berjalan |
| Biaya per perubahan sumber | Satu update kecil | Nol; biaya dipusatkan per jadwal |
| Kompleksitas kode | Rumus turunan harus benar | Pipeline sederhana dari sumber |
| Ketahanan terhadap drift | Rentan akumulasi pembulatan | Selalu menyegarkan total |
Dalam praktik keduanya dipakai bersama: update inkremental menjaga kesegaran, tugas batch menjaga kebenaran. Pola ini juga kerabat dekat Approximate Quantification dari Chapter Schema Design Patterns — bedanya, computed pattern menjaga hasil tetap exact, sementara approximate sengaja menukar presisi dengan throughput untuk counter berfrekuensi sangat tinggi.
Rangkuman #
- Computed Pattern menyimpan hasil perhitungan (rata-rata, total) sebagai field biasa agar jalur baca tinggal
findOne. - Cara event-driven: update pipeline inkremental — rata-rata baru dihitung dari rata-rata lama, jumlah lama, dan nilai baru dalam satu operasi atomic.
- Cara batch: recomputing dari sumber kebenaran dengan
$avg/$size— menyembuhkan drift dan kerusakan data. - Gabungan keduanya adalah praktik umum: inkremental untuk kesegaran, batch untuk kebenaran.