Batas 16 MB untuk satu document terdengar sangat longgar — sampai array rincian pesanan grosir tumbuh tanpa sadar dan insert gagal dengan galat yang tidak bisa dinegosiasi. Masalah sebenarnya bukan batasnya, melainkan biaya yang menumpuk jauh sebelum batas tercapai: document yang besar mahal ditransfer, mahal dipegang di cache, dan mahal ditulis ulang. Chapter ini mengukur distribusi ukuran document dengan $bsonSize, membuktikan dampaknya terhadap kapasitas batch cursor, lalu menutup dengan pola desain untuk menjaga document tetap ramping.

Peta materi Chapter ini:

  • Batas keras 16 MB, batas kedalaman 100 level, dan cara mengukur distribusi ukuran.
  • Dampak document besar: transfer batch, cache WiredTiger, penulisan ulang saat update.
  • GridFS untuk file yang memang besar.
  • Pola subset dan strategi menjaga ukuran tetap sehat.

Anatomi Batas 16 MB #

Satu document BSON maksimum 16.777.216 byte — sudah kita buktikan di Chapter Document Growth, ketika insert 100 byte di atas batas ditolak dengan object to insert too large. Yang terhitung adalah seluruh document hasil serialisasi BSON: nilai field, nama field, dan overhead struktur — bukan hanya isinya. Ada pula batas kedalaman bersarang 100 level yang sudah kita saksikan ditolak galat Overflow di Chapter Nested Documents; praktik sehat berhenti di dua sampai empat level.

Angka tunggal tidak cukup untuk memahami sebuah collection — yang penting adalah distribusinya. Siapkan transaksi biasa dan transaksi grosir yang menyematkan rincian item:

js
use latihan_perf
db.dropDatabase()
db.penjualan.insertMany([
  { _id: 1, waktu: ISODate("2026-03-02T09:15:00Z"), judul_buku: "Laut Bercerita", jumlah: 1, harga_total: 98000, kota: "Jakarta", channel: "toko" },
  { _id: 2, waktu: ISODate("2026-03-02T11:40:00Z"), judul_buku: "Bumi", jumlah: 2, harga_total: 178000, kota: "Bandung", channel: "online" },
  { _id: 3, waktu: ISODate("2026-03-03T10:05:00Z"), judul_buku: "Filosofi Teras", jumlah: 1, harga_total: 89000, kota: "Jakarta", channel: "online" },
  { _id: 4, waktu: ISODate("2026-03-03T14:30:00Z"), judul_buku: "Atomic Habits", jumlah: 3, harga_total: 345000, kota: "Surabaya", channel: "toko" },
  { _id: 5, waktu: ISODate("2026-03-04T08:20:00Z"), judul_buku: "Rumah Kaca", jumlah: 1, harga_total: 72000, kota: "Bandung", channel: "toko" },
  { _id: 6, waktu: ISODate("2026-03-04T13:55:00Z"), judul_buku: "Bumi", jumlah: 1, harga_total: 89000, kota: "Jakarta", channel: "toko" },
  { _id: 7, waktu: ISODate("2026-03-05T09:30:00Z"), judul_buku: "Paket Grosir A", jumlah: 3, harga_total: 264000, kota: "Surabaya", channel: "grosir",
    rincian: [
      { judul_buku: "Filosofi Teras", jumlah: 1, harga_satuan: 88000 },
      { judul_buku: "Bumi", jumlah: 1, harga_satuan: 89000 },
      { judul_buku: "Hujan Bulan Juni", jumlah: 1, harga_satuan: 87000 }
    ] },
  { _id: 8, waktu: ISODate("2026-03-06T10:45:00Z"), judul_buku: "Paket Grosir B", jumlah: 9, harga_total: 796000, kota: "Bandung", channel: "grosir",
    rincian: [
      { judul_buku: "Laut Bercerita", jumlah: 2, harga_satuan: 98000 },
      { judul_buku: "Rumah Kaca", jumlah: 3, harga_satuan: 89000 },
      { judul_buku: "Atomic Habits", jumlah: 1, harga_satuan: 115000 },
      { judul_buku: "Deep Work", jumlah: 1, harga_satuan: 110000 },
      { judul_buku: "Bumi", jumlah: 2, harga_satuan: 89000 }
    ] },
  { _id: 9, waktu: ISODate("2026-03-07T14:20:00Z"), judul_buku: "Paket Grosir C", jumlah: 24, harga_total: 2145000, kota: "Jakarta", channel: "grosir",
    rincian: Array.from({ length: 12 }, (_, i) => ({
      judul_buku: `Buku Sekolah ${i + 1}`, jumlah: 2, harga_satuan: 85000 + i * 1000
    })) }
])

db.penjualan.aggregate([
  { $group: {
    _id: null,
    jumlahDoc: { $sum: 1 },
    terkecil: { $min: { $bsonSize: "$$ROOT" } },
    rataRata: { $avg: { $bsonSize: "$$ROOT" } },
    terbesar: { $max: { $bsonSize: "$$ROOT" } },
    totalByte: { $sum: { $bsonSize: "$$ROOT" } }
  } }
])
db.penjualan.aggregate([
  { $set: { ukuran: { $bsonSize: "$$ROOT" } } },
  { $sort: { ukuran: -1 } },
  { $limit: 1 },
  { $project: { judul_buku: 1, ukuran: 1 } }
])

Output yang Dihasilkan:

text
switched to db latihan_perf
{ ok: 1, dropped: 'latihan_perf' }
{
  acknowledged: true,
  insertedIds: {
    '0': 1,
    '1': 2,
    '2': 3,
    '3': 4,
    '4': 5,
    '5': 6,
    '6': 7,
    '7': 8,
    '8': 9
  }
}

[
  {
    _id: null,
    jumlahDoc: 9,
    terkecil: 115,
    rataRata: 279,
    terbesar: 974,
    totalByte: 2511
  }
]
[ { _id: 9, judul_buku: 'Paket Grosir C', ukuran: 974 } ]

Pipeline pertama merangkum distribusi ukuran; pipeline kedua mengidentifikasi document terbesar lewat $set + $sort. Paket Grosir C dengan 12 baris rincian berukuran 974 byte — hampir sembilan kali transaksi biasa (115 byte) — dan inilah pola yang harus diawasi: satu jenis transaksi (grosir) yang strukturnya berbeda membuat rata-rata tidak lagi mewakili. Saat terbesar berkali-kali lipat terkecil, query yang menyentuh document besar menanggung biaya yang tak terlihat dari rata-rata.

Tiga Pajak Document Besar #

Pajak transfer. Cursor mengirim hasil dalam batch berplafon 16 MB — sebuah document harus muat utuh dalam satu pesan balasan. Document kecil mengisi batch pertama hingga 101; document besar mencapai plafon byte lebih dulu. Kita buktikan dengan collection berisi 150 transaksi grosir yang masing-masing menyematkan 3.000 baris rincian:

js
db.dokumen_besar.drop()
const rincianGrosir = Array.from({ length: 3000 }, (_, i) => ({ judul_buku: `Buku Grosir ${i}`, jumlah: 1, harga_satuan: 99000 }))
const batchBesar = Array.from({ length: 150 }, (_, i) => ({ _id: i + 1, waktu: ISODate("2026-03-08T10:00:00Z"), judul_buku: "Paket Grosir Tahunan", channel: "grosir", rincian: rincianGrosir }))
const hasilBesar = db.dokumen_besar.insertMany(batchBesar)
print("document tersimpan  :", db.dokumen_besar.countDocuments({}))
print("ukuran satu document:", bsonsize(db.dokumen_besar.findOne({ _id: 1 })), "byte")

const curBesar = db.dokumen_besar.find()
curBesar.hasNext()
print("isi batch pertama (document besar):", curBesar.objsLeftInBatch())
const curKecil = db.penjualan.find()
curKecil.hasNext()
print("isi batch pertama (document kecil):", curKecil.objsLeftInBatch())
db.dokumen_besar.drop()

Output yang Dihasilkan:

text
true

document tersimpan  : 150

ukuran satu document: 219880 byte

true
isi batch pertama (document besar): 76

true
isi batch pertama (document kecil): 9

true

Baris true pertama hasil drop(); dua berikutnya hasil hasNext() — batch pertama sudah ada di buffer klien, dan objsLeftInBatch() menghitung isinya. Batch document besar terpangkas menjadi 76, jauh di bawah 101, karena plafon 16 MB tercapai lebih dulu; sisa 74 document menunggu round-trip getMore berikutnya. Bandingkan penjualan: seluruh 9 document kecil muat dalam satu batch pertama.

Pajak cache. Data di cache WiredTiger tidak terkompresi (Chapter Working Set dan Memory), sehingga 33 MB rincian grosir tadi menempati cache jauh lebih besar daripada ukuran terkompresinya di disk. Semakin besar document yang rutin diakses, semakin cepat working set menempel batas cache dan eviksi mulai memutar halaman keluar-masuk memori.

Pajak penulisan ulang. WiredTiger tidak menimpa document di tempat: update pada satu field tetap menulis ulang document yang memuatnya — perilaku MVCC yang kita bahas di Chapter Document Growth. Menambah satu baris rincian pada document 220 KB berarti menulis ulang 220 KB, dan index yang menunjuk document itu ikut diperbarui.

GridFS untuk File yang Memang Besar #

Beberapa data memang berukuran besar secara sah: PDF katalog, sampul buku resolusi tinggi, rekaman audio. GridFS menyimpannya bukan sebagai satu document, melainkan dipecah menjadi chunk berukuran 255 KiB (default) ke collection fs.chunks, sementara metadata — nama file, panjang, chunkSize, MD5 — tersimpan di fs.files. Index majemuk unik (files_id, n) menjaga urutan chunk, driver merangkai ulang filenya saat di-query, dan range query membuka akses ke bagian tengah file tanpa memuat seluruhnya. Operasinya tersedia lewat API driver dan tool mongofiles di MongoDB Database Tools — tidak terpasang di lingkungan latihan kita; detailnya ada di dokumentasi GridFS.

GridFS bukan alat untuk “menambal” document bisnis yang membengkak. Ia untuk konten biner yang memang besar dan dibaca sebagian-sebagian; data transaksi yang membengkak harus diperbaiki dari desainnya.

Menjaga Document Tetap Ramping #

Pilihan polanya mengikuti bentuk pembengkakannya:

GejalaPola yang tepat
Detail panas dan dingin tercampur dalam satu documentSubset pattern — Chapter Subset Pattern menyisakan ringkasan dan memindahkan detail ke collection terpisah
Array tumbuh tanpa langit-langit (log, riwayat)Bucket pattern atau pindah ke reference per-item
Konten biner besar (file, media)GridFS
Satu jenis entitas selalu jauh lebih besar dari lainnyaPisahkan ke collection sendiri agar tidak menarik biaya query lain

Untuk toko buku kita: rincian transaksi grosir paling pas dipindah ke collection rincian_pesanan yang dirujuk per id_pesanan, sementara document penjualan menyimpan ringkasan — total, jumlah item, beberapa item teratas. Query daftar transaksi jadi ringan; detail lengkap diambil saat halaman detail dibuka.

Rangkuman #

  • Batas keras 16 MB per document dan 100 level kedalaman bersarang berlaku pada ukuran BSON hasil serialisasi; distribusi ukuran diukur dengan $bsonSize ($min/$avg/$max), bukan sekadar rata-rata.
  • Document besar menaruh tiga pajak: batch transfer terpangkas plafon 16 MB, jejak cache tidak terkompresi membesar, dan setiap update menulis ulang seluruh document.
  • GridFS memecah file menjadi chunk 255 KiB di fs.chunks plus metadata di fs.files — untuk konten biner yang memang besar, bukan penambal skema.
  • Pola subset, bucket, dan pemisahan collection menjaga document tetap ramping sebelum batas keras tercapai.