Antipola schema adalah desain dokumen yang terasa praktis saat ditulis tetapi merusak performa setelah datanya besar. Dua bentuk yang paling sering muncul adalah array raksasa yang terus bertambah di dalam satu dokumen dan dokumen induk yang dibiarkan tumbuh tanpa batas. Topik ini mendemonstrasikan keduanya pada basis data kampus dengan pengukuran yang bersifat struktural, bukan pengukuran waktu eksekusi. Pengukuran itu mencakup ukuran dokumen dalam byte melalui operator $bsonSize dan jumlah dokumen yang diperiksa sebuah query melalui totalDocsExamined.
Array raksasa pada dokumen induk#
Antipola pertama menumpuk ribuan elemen pada satu array di dalam dokumen induk. Kasusnya adalah kebiasaan mencatat setiap kehadiran mahasiswa sebagai elemen array pada dokumen mata kuliah. Setiap pertemuan menambah puluhan entri, setiap semester menambah belasan pertemuan, sehingga array itu berkembang menjadi ribuan elemen sementara dokumen induknya tetap satu.
use kampusswitched to db kampusdb.mata_kuliah.insertMany([
{ _id: "TSA201", nama: "Statistika Terapan", hadir: [] },
{ _id: "TSA301", nama: "Analisis Regresi", hadir: [] }
]){ acknowledged: true, insertedIds: { '0': 'TSA201', '1': 'TSA301' } }Ukuran dokumen dalam byte diukur menggunakan operator agregasi $bsonSize. Fungsi bantu berikut membungkusnya agar setiap pengukuran cukup dipanggil dengan satu kode mata kuliah.
const ukuranDokumen = (id) => db.mata_kuliah.aggregate([
{ $match: { _id: id } },
{ $project: { _id: 0, ukuran: { $bsonSize: "$$ROOT" } } }
]).toArray()[0].ukuran
ukuranDokumen("TSA201")62Selanjutnya 8000 entri kehadiran didorong ke dokumen TSA201 dalam satu $push dengan $each, sebagaimana yang terjadi secara bertahap pada koleksi yang dibiarkan tumbuh.
const logHadir = Array.from({ length: 8000 }, (_, i) => i % 80)
db.mata_kuliah.updateOne({ _id: "TSA201" }, { $push: { hadir: { $each: logHadir } } }){
acknowledged: true,
insertedId: null,
matchedCount: 1,
modifiedCount: 1,
upsertedCount: 0
}({
ukuranTSA201: ukuranDokumen("TSA201"),
ukuranTSA301: ukuranDokumen("TSA301"),
jumlahEntri: db.mata_kuliah.findOne({ _id: "TSA201" }).hadir.length
}){ ukuranTSA201: 78952, ukuranTSA301: 60, jumlahEntri: 8000 }Kedua dokumen berisi jenis data yang sama, tetapi TSA201 berukuran puluhan kilobyte karena membawa 8000 elemen, sedangkan TSA301 hanya berukuran puluhan byte. Perbedaan itu melekat pada setiap pembacaan: satu permintaan nama mata kuliah untuk kebutuhan halaman jadwal ikut memindahkan seluruh 8000 elemen array dari disk ke jaringan.
Query terhadap elemen array tanpa indeks memeriksa seluruh dokumen pada koleksi, dan totalDocsExamined menunjukkan jumlahnya.
const hasilScan = db.mata_kuliah.find({ hadir: 79 }).explain("executionStats")
hasilScan.queryPlanner.winningPlan.stage
hasilScan.executionStats.totalDocsExaminedCOLLSCAN
2Indeks multikey pada field array mengubah rencana query tersebut.
db.mata_kuliah.createIndex({ hadir: 1 })hadir_1const hasilIndeks = db.mata_kuliah.find({ hadir: 79 }).explain("executionStats")
hasilIndeks.queryPlanner.winningPlan.stage
hasilIndeks.queryPlanner.winningPlan.inputStage.stage
hasilIndeks.executionStats.totalDocsExaminedFETCH
IXSCAN
1Tanpa indeks, rencananya berupa COLLSCAN dan kedua dokumen pada koleksi diperiksa. Dengan indeks, rencananya berupa FETCH di atas IXSCAN dan hanya satu dokumen yang diperiksa. Indeks memperbaiki rencana query, tetapi tidak memperbaiki desainnya, karena satu proses FETCH tetap membaca seluruh dokumen berukuran puluhan kilobyte itu.
Pertumbuhan dokumen tanpa batas#
Antipola kedua adalah membiarkan satu dokumen tumbuh pada field yang sama sepanjang usianya. Demonstrasi berikut menambahkan elemen ke dokumen TSA301 satu per satu dengan $push, dan ukurannya diukur ulang setiap kali. Pola ini mirip pola pertama, tetapi bertumbuh dalam langkah kecil yang sulit disadari.
for (let n = 1; n <= 3; n++) {
db.mata_kuliah.updateOne({ _id: "TSA301" }, { $push: { hadir: n } })
print(`setelah push ke-${n}: ${ukuranDokumen("TSA301")} byte`)
}setelah push ke-1: 67 byte
setelah push ke-2: 74 byte
setelah push ke-3: 81 byteUkuran dokumen naik pada setiap pembaruan, dan pola ini tidak berhenti dengan sendirinya. Setiap operasi $push terlihat kecil, yaitu satu entri kehadiran atau satu baris log yang menambah puluhan byte, sehingga tidak ada pembaruan yang terasa mahal pada waktunya. Bahayanya justru akumulasi yang tidak berhenti itu, karena ukuran dokumen naik terus sedangkan pola aksesnya tidak berubah. Pembaruan yang membuat dokumen membesar juga menulis ulang dokumen secara utuh, sehingga biaya tulisnya mengikuti ukuran akhir, bukan ukuran field yang berubah.
MongoDB menetapkan batas ukuran satu dokumen sebesar 16 MB, sehingga pertumbuhan tanpa kendali berakhir dengan error pada operasi tulis ketika batas itu tercapai. Jauh sebelum batasnya tercapai, setiap pembacaan dan pembaruan dokumen itu sudah membawa biaya yang tumbuh bersama ukurannya. Indeks pun tidak menahan pertumbuhan ini, karena indeks hanya mempercepat pencarian, sedangkan ukuran dokumen yang dibaca tetap utuh.
Memisahkan data yang bertumbuh ke koleksinya sendiri#
Perbaikan untuk kedua antipola tersebut adalah memindahkan data yang bertumbuh ke koleksi tersendiri dengan dokumen yang ukurannya terkendali. Satu dokumen kehadiran menampung satu pertemuan, sehingga ukurannya tetap kecil berapa pun panjang semester. Pertumbuhan yang tidak terbatas pun berpindah menjadi jumlah dokumen, yaitu bentuk pertumbuhan yang memang dirancang untuk ditangani MongoDB.
db.kehadiran.insertMany([
{ mataKuliahId: "TSA201", pertemuan: 1, hadirId: [ 0, 3, 5 ] },
{ mataKuliahId: "TSA201", pertemuan: 2, hadirId: [ 1, 2 ] },
{ mataKuliahId: "TSA301", pertemuan: 1, hadirId: [ 4 ] }
]){
acknowledged: true,
insertedIds: {
'0': ObjectId('6ae89a081d094fb7d98de666'),
'1': ObjectId('6ae89a081d094fb7d98de667'),
'2': ObjectId('6ae89a081d094fb7d98de668')
}
}db.kehadiran.aggregate([
{ $project: { _id: 0, ukuran: { $bsonSize: "$$ROOT" } } }
])[ { ukuran: 97 }, { ukuran: 90 }, { ukuran: 83 } ]Setiap dokumen kehadiran berukuran di bawah seratus byte dan tidak pernah melebihi ukuran satu pertemuan. Query kehadiran per mata kuliah menjadi pencarian berindeks pada mataKuliahId, sedangkan laporan lintas pertemuan menjadi agregasi biasa pada koleksi kehadiran. Field rujukan mataKuliahId memainkan peran yang sama dengan rujukan pada relasi satu-ke-banyak, sehingga pola query dan indeksnya pun sama.
db.kehadiran.createIndex({ mataKuliahId: 1 })mataKuliahId_1Kesalahan umum#
Kesalahan pertama adalah menilai schema dengan pengukuran waktu pada data yang masih kecil. Ratusan atau ribuan dokumen pada sandbox hampir selalu selesai dalam hitungan milidetik, sehingga array raksasa tidak terasa merugikan sebelum koleksinya bertemu data produksi. Ukuran dokumen dan totalDocsExamined memberi peringatan jauh lebih dini karena keduanya menunjukkan biaya struktural, bukan hasil pengukuran waktu yang berubah-ubah.
Kesalahan kedua adalah menganggap 16 MB sebagai target aman. Batas itu adalah plafon kegagalan, bukan ukuran ideal, dan dokumen berukuran megabyte sudah menyulitkan setiap pembacaan jauh sebelum plafonnya tersentuh. Desain yang sehat menjaga ukuran dokumen sekecil yang dituntut oleh tugasnya.
Kesalahan ketiga adalah menambah indeks untuk menutupi antipola. Indeks multikey memperbaiki rencana query terhadap array besar, tetapi proses FETCH sesudahnya tetap membaca seluruh dokumen yang membengkak. Perbaikan yang tepat tetap berupa pemisahan data yang bertumbuh ke koleksinya sendiri, dan indeks mengikuti desain yang sudah benar. Urutan perbaikannya pun perlu benar, yaitu desain terlebih dahulu, lalu indeks, karena indeks yang dibuat di atas desain yang keliru hanya menambah biaya tulis tanpa mengurangi biaya baca.
Ringkasan#
- Array yang terus bertambah pada dokumen induk memperbesar setiap pembacaan, dan ukurannya diukur dengan operator
$bsonSize. - Pertumbuhan dokumen tanpa batas berakhir pada plafon 16 MB per dokumen dan membebani setiap pembaruan jauh sebelum plafon itu.
totalDocsExaminedmenunjukkan jumlah dokumen yang diperiksa sebuah query, dan turunnya angka tersebut setelah pembuatan indeks menandai rencana query yang membaik.- Data yang bertumbuh dipindahkan ke koleksi sendiri dengan dokumen berukuran terkendali, misalnya satu dokumen per pertemuan, lalu diindeks pada field rujukannya.
Referensi#
- MongoDB Limits and Thresholds — MongoDB Manual — Daftar batas MongoDB, termasuk batas ukuran dokumen 16 MB.
- $bsonSize — MongoDB Manual — Operator agregasi penghitung ukuran dokumen dalam byte.
- cursor.explain() — MongoDB Manual
— Dokumentasi rencana query dan statistik eksekusi seperti
totalDocsExamined. - Multikey Indexes — MongoDB Manual — Perilaku indeks pada field bertipe array.
- $push — MongoDB Manual
— Operator penambah elemen array, termasuk modifier
$each.