Log aplikasi punya siklus hidup aneh: datanya sangat deras saat ditulis, menarik hanya beberapa menit terakhir, dan tidak berharga setelah sebulan. Kalau disimpan di collection biasa, kita harus rajin menghapus dokumen lama atau membiarkan disk penuh. Capped collection menawarkan jalan lain: collection berukuran tetap yang bekerja seperti buffer melingkar — penuh sudah, data terlama tertimpa otomatis oleh data baru. Chapter ini membuatnya, membuktikan perilaku timpa-menimpa, menguji batasan operasinya, dan membandingkannya dengan time series collection yang baru saja kita pelajari.
Membuat Collection Berukuran Tetap #
Capped collection harus dibuat eksplisit lewat db.createCollection() — insert pertama ke collection yang belum ada tidak akan membuatnya secara implisit. Dua parameter menentukan bentuknya: size (batas byte, wajib) dan max (batas jumlah document, opsional):
use latihan_timeseries
db.dropDatabase()
db.createCollection("log_aktivitas", { capped: true, size: 4096, max: 5 })
db.log_aktivitas.isCapped()
db.log_aktivitas.insertMany([
{ no: 1, aksi: "login", pengguna: "budi" },
{ no: 2, aksi: "lihat produk", pengguna: "budi" },
{ no: 3, aksi: "tambah keranjang", pengguna: "budi" },
{ no: 4, aksi: "checkout", pengguna: "budi" },
{ no: 5, aksi: "logout", pengguna: "budi" }
])
db.log_aktivitas.find({}, { _id: 0 })Output yang Dihasilkan:
switched to db latihan_timeseries
{ ok: 1, dropped: 'latihan_timeseries' }
{ ok: 1 }
true
{
acknowledged: true,
insertedIds: {
'0': ObjectId('6ad3e14e92532e88d9e3e18d'),
'1': ObjectId('6ad3e14e92532e88d9e3e18e'),
'2': ObjectId('6ad3e14e92532e88d9e3e18f'),
'3': ObjectId('6ad3e14e92532e88d9e3e190'),
'4': ObjectId('6ad3e14e92532e88d9e3e191')
}
}
[
{ no: 1, aksi: 'login', pengguna: 'budi' },
{ no: 2, aksi: 'lihat produk', pengguna: 'budi' },
{ no: 3, aksi: 'tambah keranjang', pengguna: 'budi' },
{ no: 4, aksi: 'checkout', pengguna: 'budi' },
{ no: 5, aksi: 'logout', pengguna: 'budi' }
]size: 4096 berarti server mengalokasikan sekitar 4 KB storage di muka (dibulatkan ke kelipatan 256 byte), dan max: 5 membatasi isi lima document — mana pun yang tercapai lebih dulu. Perhatikan juga dua hal dari keluaran: isCapped() mengonfirmasi statusnya, dan find() tanpa sort() mengembalikan data dalam urutan penyisipan — itu urutan natural, bukan kebetulan.
FIFO dalam Praktik #
Sekarang kita sisipkan document keenam melewati batas max:
db.log_aktivitas.insertOne({ no: 6, aksi: "login ulang", pengguna: "budi" })
db.log_aktivitas.find({}, { _id: 0 })
db.log_aktivitas.find({}, { _id: 0 }).sort({ $natural: -1 })Output yang Dihasilkan:
{
acknowledged: true,
insertedId: ObjectId('6ad3e14e92532e88d9e3e192')
}
[
{ no: 2, aksi: 'lihat produk', pengguna: 'budi' },
{ no: 3, aksi: 'tambah keranjang', pengguna: 'budi' },
{ no: 4, aksi: 'checkout', pengguna: 'budi' },
{ no: 5, aksi: 'logout', pengguna: 'budi' },
{ no: 6, aksi: 'login ulang', pengguna: 'budi' }
]
[
{ no: 6, aksi: 'login ulang', pengguna: 'budi' },
{ no: 5, aksi: 'logout', pengguna: 'budi' },
{ no: 4, aksi: 'checkout', pengguna: 'budi' },
{ no: 3, aksi: 'tambah keranjang', pengguna: 'budi' },
{ no: 2, aksi: 'lihat produk', pengguna: 'budi' }
]Document no: 1 hilang tanpa perintah hapus — collection penuh, jadi document tertua otomatis tertimpa oleh yang baru. Ini perilaku FIFO (first in, first out): retensi dikendalikan oleh kapasitas, bukan oleh pekerjaan pembersih. Query kedua memakai sort({ $natural: -1 }) untuk membaca dari ujung terbaru, seperti tail pada file log; $natural mengikuti urutan fisik di disk sehingga tetap efisien tanpa index.
_id, Delete, dan Dua Galat yang Sebenarnya #
Capped collection tetap punya _id dan index _id_ secara default — tidak seperti time series collection:
db.log_aktivitas.getIndexes()
db.log_aktivitas.deleteOne({ no: 2 })Output yang Dihasilkan:
[
{
v: 2,
key: { _id: 1 },
name: '_id_',
collation: { locale: 'simple' }
}
]
{ acknowledged: true, deletedCount: 1 }Delete individual berhasil di MongoDB 9.0 — banyak materi lama menyatakan bahwa capped collection menolak delete per document, dan pernyataan itu memang benar untuk versi-versi sebelumnya, tetapi bukan lagi untuk server yang kita pakai. Meski diizinkan, menghapus manual bukan pola yang cocok: tulisan diserialisasi (satu writer pada satu waktu, performa concurrency lebih rendah daripada collection biasa), dan alat retensinya adalah ketimbangan otomatis. Batasan yang masih berlaku keras ada dua, dan keduanya kita buktikan:
try {
const s = db.getMongo().startSession()
s.startTransaction()
const c = s.getDatabase("latihan_timeseries").log_aktivitas
c.insertOne({ no: 7, aksi: "transaksi" })
s.commitTransaction()
} catch (e) {
print("Galat " + e.code + ": " + e.message)
}
db.sembilan.insertOne({ a: 1 })
db.sembilan.aggregate([ { $out: "log_aktivitas" } ])Output yang Dihasilkan:
Galat 263: Collection 'latihan_timeseries.log_aktivitas' is a capped collection. Writes in transactions are not allowed on capped collections.
{
acknowledged: true,
insertedId: ObjectId('6ad3e14e92532e88d9e3e194')
}
Uncaught
MongoServerError[Location17152]: Executor error during aggregate command on namespace: latihan_timeseries.sembilan :: caused by :: namespace 'latihan_timeseries.log_aktivitas' is capped so it can't be used for $outDua galat asli tersebut menegaskan dua larangan yang tetap berlaku: capped collection tidak bisa ditulis di dalam transaksi (galat 263), dan $out tidak boleh menargetkannya. Selain itu capped collection tidak bisa di-shard dan tidak didukung Stable API V1. Update juga sebaiknya dihindari: mengubah document hingga melebihi ruang alokasinya berpotensi perilaku tak terduga.
Kapan Memakai Capped #
Tempat naturalnya capped collection adalah buffer berputar: log operasional beresolusi tinggi, antrian kerja sementara, atau cache data terakhir. Fitur pelengkapnya adalah tailable cursor — cursor yang tetap terbuka dan terus mengambil document baru yang menyusul di ekor collection, seperti tail -f. Oplog replica set sendiri adalah capped collection: ukuran tetap, entri lama tertimpa, dan itu sebabnya resume change stream bisa gagal bila token sudah melewati jendela oplog.
Bandingkan dengan dua tetangganya:
- Capped vs TTL index — keduanya menghapus data lama otomatis. TTL index bekerja di collection biasa berdasarkan umur field Date (fleksibel, bisa di-query historis, tetapi butuh background deletion); capped berdasarkan kapasitas fisik (pasti, murah, tetapi tanpa jaminan umur). Dokumentasi MongoDB sendiri menyarankan TTL index sebagai opsi pertama kecuali kebutuhannya memang buffer tetap.
- Capped vs time series — data sensor bernilai analitik (agregasi rentang waktu, retensi panjang) lebih tepat di time series collection; data yang hanya relevan beberapa saat terakhir (log debugging, event sementara) cukup di capped.
Rangkuman #
- Capped collection dibuat eksplisit dengan
capped: true,sizewajib (dibulatkan ke kelipatan 256 byte),maxopsional;isCapped()memverifikasi statusnya. - Penuh berarti tertimpa: document tertua otomatis digantikan, retensi dikendalikan kapasitas;
find()mengikuti urutan natural dan$natural: -1membaca dari terbaru. _iddan index-nya tetap ada; delete individual kini diizinkan di 9.0, tetapi transaksi write (galat 263) dan$out(galat terkait namespace capped) tetap ditolak.- Pilihan retensi: capped untuk buffer tetap, TTL index untuk penghapusan berbasis umur di collection biasa, time series untuk data sensor analitik.