Lewati ke konten utama
Semua artikel
MenengahData

MongoDB & NoSQL: Kapan dan Bagaimana Memakainya

Pahami perbedaan paradigma NoSQL vs SQL, desain document-oriented schema, aggregation pipeline, indexing, dan kapan memilih MongoDB dibanding PostgreSQL.

15 menit baca

Sebelum ini, pelajari dulu


"NoSQL bukan pengganti SQL. NoSQL adalah jawaban untuk pertanyaan yang berbeda: bukan 'bagaimana menyimpan data ini dengan benar', tapi 'bagaimana bentuk data ini sebenarnya'."


Tentang Artikel Ini

Kalau kamu sudah baca artikel #03 (Desain Database), kamu tahu normalisasi, foreign key, dan JOIN adalah cara PostgreSQL menjaga data tetap konsisten. MongoDB membuang sebagian besar aturan itu — dan itu bukan kekurangan, itu keputusan desain untuk bentuk masalah yang berbeda.

Artikel ini tidak akan bilang "MongoDB lebih cepat" atau "SQL sudah ketinggalan zaman" — keduanya klaim yang menyesatkan tergantung konteks. Sebaliknya, kita akan bangun pemahaman yang jujur: kapan bentuk data kamu cocok dengan model document, kapan tidak, dan bagaimana membuat keputusan itu berdasarkan bukti, bukan tren.

Setelah selesai, kamu akan bisa:

  • Memahami perbedaan fundamental antara model relasional dan document-oriented
  • Mendesain schema MongoDB — kapan embed, kapan reference
  • Melakukan CRUD dan query dengan Mongoose di Node.js
  • Menulis aggregation pipeline untuk kebutuhan reporting
  • Membuat index yang efektif di MongoDB
  • Mengambil keputusan MongoDB vs PostgreSQL berdasarkan bentuk data, bukan hype

Prasyarat: Sudah baca artikel #03 (Desain Database) — artikel ini banyak membandingkan langsung dengan konsep normalisasi dan JOIN yang dibahas di sana. Familiar dengan JavaScript async/await.


Daftar Isi

  1. Kenapa NoSQL? Paradigma yang Berbeda, Bukan yang Lebih Baik
  2. Konsep Dasar MongoDB
  3. Schema Design: Embedding vs Referencing
  4. CRUD dengan Mongoose
  5. Query dan Aggregation Pipeline
  6. Indexing di MongoDB
  7. Studi Kasus: Katalog Produk dengan Atribut Bervariasi
  8. Kapan MongoDB, Kapan PostgreSQL

Bab 1: Kenapa NoSQL? Paradigma yang Berbeda, Bukan yang Lebih Baik

Masalah yang Coba Dipecahkan

Model relasional (dibahas di artikel #03) unggul saat data punya struktur konsisten dan hubungan yang jelas antar entity — order punya customer, customer punya banyak order. Tapi ada kelas masalah lain: data yang bentuknya berbeda-beda antar record, berubah-ubah seiring waktu, atau secara alami berbentuk pohon/dokumen daripada tabel.

Bayangkan sistem katalog produk marketplace: laptop punya ram, cpu, storage; baju punya size, material, warna; buku punya penulis, penerbit, jumlah_halaman. Di artikel #03 kita sudah bahas ini bisa diselesaikan dengan JSONB di PostgreSQL — dan itu solusi valid. MongoDB mengambil pendekatan yang lebih jauh: seluruh database dirancang di sekitar dokumen fleksibel ini, bukan hanya satu kolom.

Empat Perbedaan Fundamental

AspekRelasional (PostgreSQL)Document (MongoDB)
Unit penyimpananBaris di tabel dengan schema tetapDokumen JSON/BSON dengan schema fleksibel
RelasiJOIN antar tabelEmbed (nested) atau reference manual
KonsistensiACID penuh, multi-tabelACID per-dokumen kuat; multi-dokumen butuh transaction eksplisit
ScalingVertikal (server lebih besar), horizontal butuh effort (sharding, read replica)Horizontal by design (sharding native)

Bukan "NoSQL Menggantikan SQL"

Klaim generasi pertama NoSQL (2010-an awal) bahwa NoSQL akan menggantikan database relasional sudah terbukti salah — mayoritas sistem production serius (termasuk yang pakai MongoDB) tetap punya PostgreSQL/MySQL untuk data transaksional inti (user, order, payment), dan MongoDB untuk bagian spesifik yang memang cocok bentuknya: katalog produk, log aktivitas, konten CMS, data analytics event.

Aturan praktis yang dipakai kebanyakan tim production: default ke PostgreSQL untuk data bisnis inti kamu (transaksi, keuangan, apa pun yang butuh integritas relasional ketat). Pertimbangkan MongoDB hanya ketika bentuk data spesifik — bukan seluruh sistem — memang document-shaped.


Bab 2: Konsep Dasar MongoDB

Terminologi: Padanan dari Dunia SQL

PostgreSQLMongoDB
DatabaseDatabase
TableCollection
RowDocument
ColumnField
Primary Key (id)_id (otomatis, tipe ObjectId)
JOIN$lookup (aggregation) atau embedding
Schema (fixed)Schema fleksibel (opsional, biasanya di-enforce di level aplikasi)

Instalasi & Koneksi

# macOS
brew tap mongodb/brew
brew install mongodb-community
 
# Jalankan sebagai service
brew services start mongodb-community
 
# Atau pakai MongoDB Atlas (cloud, free tier) — tidak perlu install apapun
npm install mongoose
// src/db/mongo.js
import mongoose from 'mongoose';
 
export async function connectMongo() {
  await mongoose.connect(process.env.MONGODB_URI);
  console.log('MongoDB connected');
}
 
mongoose.connection.on('error', (err) => {
  console.error('MongoDB connection error:', err);
});

Bentuk Dokumen

// Satu dokumen di collection "products"
{
  _id: ObjectId("65f1a2b3c4d5e6f7a8b9c0d1"),
  name: "Laptop Gaming X1",
  price: 15000000,
  category: "electronics",
  specs: {
    ram: "16GB",
    cpu: "Intel i7",
    storage: "512GB SSD",
  },
  tags: ["gaming", "laptop", "intel"],
  createdAt: ISODate("2024-01-15T10:00:00Z"),
}

Perhatikan: specs adalah object bersarang (nested), tags adalah array — keduanya representasi native, bukan trik seperti JSONB atau tabel junction.


Bab 3: Schema Design: Embedding vs Referencing

Ini keputusan desain paling penting di MongoDB — setara pentingnya dengan normalisasi di dunia SQL, tapi arahnya sering berlawanan.

Embedding: Simpan Bersarang di Dokumen yang Sama

// Order dengan item embedded
{
  _id: ObjectId("..."),
  customerName: "Budi Santoso",
  items: [
    { productName: "Kopi Arabika", quantity: 2, unitPrice: 85000 },
    { productName: "Teh Hijau", quantity: 1, unitPrice: 35000 },
  ],
  total: 205000,
  createdAt: ISODate("2024-01-15"),
}

Kapan embed:

  • Data anak selalu diakses bersama data induk (order selalu ditampilkan dengan item-nya)
  • Relasi "one-to-few" — jumlah item terbatas dan tidak akan tumbuh tak terbatas
  • Data anak tidak perlu di-query independen dari induknya

Referencing: Simpan ID, Query Terpisah

// Product sebagai collection terpisah, order hanya simpan reference
// orders collection
{
  _id: ObjectId("..."),
  customerId: ObjectId("65f1..."),   // reference ke customers
  items: [
    { productId: ObjectId("65f2..."), quantity: 2 },
  ],
}

Kapan reference:

  • Relasi "one-to-many" tak terbatas (satu customer bisa punya ribuan order — jangan embed semua order ke dalam dokumen customer)
  • Data anak perlu di-query dan diupdate independen (update harga produk tanpa menyentuh semua order lama)
  • Data anak dipakai bersama oleh banyak dokumen induk (many-to-many)

Perbandingan Langsung dengan Normalisasi SQL

Di artikel #03, kita normalisasi order_items sebagai tabel terpisah supaya tidak ada data redundant dan integritas terjaga lewat foreign key. Di MongoDB, pola yang setara untuk item transaksi justru sering di-embed, bukan direferensi — karena performanya lebih baik untuk kasus "selalu dibaca bersama induknya" dan MongoDB tidak punya JOIN semurah PostgreSQL.

SQL: normalisasi = pisahkan, JOIN saat butuh
MongoDB: denormalisasi terkontrol = gabungkan yang selalu dipakai bersama,
         reference hanya yang benar-benar butuh query independen

Jebakan paling umum untuk developer yang baru pindah dari SQL: mencoba mereplikasi skema ternormalisasi penuh di MongoDB — semua di-reference, tidak ada yang di-embed. Hasilnya adalah aplikasi yang butuh berkali-kali round-trip query untuk menampilkan satu halaman, kehilangan justru keunggulan utama MongoDB.

Snapshot Pattern: Solusi untuk Item Transaksi

Sama seperti di artikel #03, item transaksi butuh snapshot harga saat transaksi terjadi, bukan reference ke harga produk saat ini:

{
  items: [
    {
      productId: ObjectId("..."),  // reference untuk lacak balik ke produk asli
      productName: "Kopi Arabika", // snapshot — nama saat transaksi
      unitPrice: 85000,            // snapshot — harga saat transaksi
      quantity: 2,
    },
  ],
}

Pola ini identik dengan yang kita pakai di tabel transaction_items PostgreSQL — prinsipnya sama, cuma representasinya berbeda.


Bab 4: CRUD dengan Mongoose

Kenapa Mongoose, Bukan Native Driver

MongoDB native driver Node.js bisa dipakai langsung, tapi tidak punya schema enforcement — setiap dokumen bisa berbentuk apapun, termasuk yang salah karena typo. Mongoose menambahkan schema, validasi, dan model di atasnya — mirip peran Zod di artikel #02, tapi terintegrasi langsung ke layer data.

Mendefinisikan Schema

// src/models/product.model.js
import mongoose from 'mongoose';
 
const productSchema = new mongoose.Schema(
  {
    name: {
      type: String,
      required: [true, 'Nama produk wajib diisi'],
      trim: true,
      minlength: 3,
    },
    price: {
      type: Number,
      required: true,
      min: [0, 'Harga tidak boleh negatif'],
    },
    category: {
      type: String,
      required: true,
      enum: ['electronics', 'fashion', 'food', 'books'],  // sama seperti ENUM di SQL
    },
    specs: {
      type: mongoose.Schema.Types.Mixed,  // fleksibel — beda field per kategori
      default: {},
    },
    tags: [String],
    isActive: {
      type: Boolean,
      default: true,
    },
  },
  {
    timestamps: true,  // otomatis tambahkan createdAt & updatedAt
  }
);
 
export const Product = mongoose.model('Product', productSchema);

Create

const product = await Product.create({
  name: 'Laptop Gaming X1',
  price: 15000000,
  category: 'electronics',
  specs: { ram: '16GB', cpu: 'Intel i7', storage: '512GB SSD' },
  tags: ['gaming', 'laptop'],
});

Read

// Cari satu
const product = await Product.findById('65f1a2b3c4d5e6f7a8b9c0d1');
const product2 = await Product.findOne({ name: 'Laptop Gaming X1' });
 
// Cari banyak dengan filter, mirip WHERE
const laptops = await Product.find({
  category: 'electronics',
  price: { $gte: 5000000, $lte: 20000000 },  // setara WHERE price BETWEEN
  isActive: true,
});
 
// Pagination
const page = 2, limit = 10;
const products = await Product.find({ isActive: true })
  .sort({ createdAt: -1 })
  .skip((page - 1) * limit)
  .limit(limit);

Update

// Update satu field
await Product.findByIdAndUpdate(id, { price: 14500000 });
 
// Update dengan operator — increment stok tanpa race condition
await Product.findByIdAndUpdate(id, { $inc: { stock: -1 } });
 
// Update banyak dokumen sekaligus
await Product.updateMany(
  { category: 'electronics' },
  { $set: { taxRate: 0.11 } }
);

Delete

// Hard delete
await Product.findByIdAndDelete(id);
 
// Soft delete — pola yang sama seperti di artikel #03
await Product.findByIdAndUpdate(id, { isActive: false });

Validasi Otomatis dari Schema

try {
  await Product.create({ name: 'AB', price: -100, category: 'invalid' });
} catch (err) {
  // err.errors berisi detail per-field, mirip result.error dari Zod safeParse
  console.log(err.errors.name.message);      // "Nama produk wajib diisi" (kurang dari minlength)
  console.log(err.errors.price.message);     // "Harga tidak boleh negatif"
  console.log(err.errors.category.message);  // bukan salah satu ENUM
}

Bab 5: Query dan Aggregation Pipeline

Query Sederhana vs Aggregation

Untuk filter dan sort sederhana, find() sudah cukup. Untuk kebutuhan seperti JOIN, GROUP BY, atau perhitungan bertingkat — pakai aggregation pipeline, rangkaian tahap (stage) yang memproses dokumen secara berurutan.

$match, $group: Setara WHERE + GROUP BY

// Setara: SELECT category, COUNT(*), SUM(price) FROM products
//         WHERE isActive = true GROUP BY category
const summary = await Product.aggregate([
  { $match: { isActive: true } },
  {
    $group: {
      _id: '$category',
      totalProducts: { $sum: 1 },
      totalValue: { $sum: '$price' },
      avgPrice: { $avg: '$price' },
    },
  },
  { $sort: { totalValue: -1 } },
]);
 
// Hasil:
// [
//   { _id: 'electronics', totalProducts: 45, totalValue: 675000000, avgPrice: 15000000 },
//   { _id: 'fashion', totalProducts: 120, totalValue: 36000000, avgPrice: 300000 },
// ]

$lookup: Setara JOIN

// orders collection punya customerId (reference) — "join" ke customers
const ordersWithCustomer = await Order.aggregate([
  {
    $lookup: {
      from: 'customers',       // collection yang di-join
      localField: 'customerId',
      foreignField: '_id',
      as: 'customer',          // hasil disimpan sebagai array di field ini
    },
  },
  { $unwind: '$customer' },    // ubah array 1-elemen jadi object langsung
  {
    $project: {                // pilih field yang ditampilkan, setara SELECT kolom tertentu
      total: 1,
      createdAt: 1,
      'customer.name': 1,
      'customer.email': 1,
    },
  },
]);

$lookup bukan JOIN yang seefisien PostgreSQL. Tidak ada query planner sekuat PostgreSQL untuk optimasi otomatis, dan $lookup antar collection besar bisa mahal. Kalau aplikasimu butuh JOIN kompleks berkali-kali di jalur kritis, itu sinyal kuat data-mu lebih cocok di PostgreSQL — bukan alasan untuk memaksakan $lookup.

Pipeline Bertingkat: Laporan Penjualan Bulanan

const monthlySales = await Order.aggregate([
  { $match: { status: 'completed' } },
  {
    $group: {
      _id: {
        year: { $year: '$createdAt' },
        month: { $month: '$createdAt' },
      },
      totalRevenue: { $sum: '$total' },
      orderCount: { $sum: 1 },
    },
  },
  { $sort: { '_id.year': -1, '_id.month': -1 } },
  { $limit: 12 },
]);

Bab 6: Indexing di MongoDB

Prinsip yang Sama, Sintaks Berbeda

Konsep index di MongoDB paralel dengan PostgreSQL (dibahas di artikel #03 Bab 5) — mempercepat query dengan mengorbankan sedikit kecepatan write. Tanpa index, MongoDB melakukan collection scan (setara sequential scan).

// Index sederhana — setara CREATE INDEX di SQL
await Product.collection.createIndex({ category: 1 });   // 1 = ascending
await Product.collection.createIndex({ createdAt: -1 }); // -1 = descending
 
// Compound index — urutan field penting, sama seperti composite index SQL
await Product.collection.createIndex({ category: 1, price: -1 });
 
// Unique index — setara UNIQUE constraint
await Product.collection.createIndex({ sku: 1 }, { unique: true });
 
// Text index — untuk full-text search sederhana
await Product.collection.createIndex({ name: 'text', description: 'text' });
// Definisikan langsung di schema — lebih idiomatik
const productSchema = new mongoose.Schema({
  sku: { type: String, unique: true, index: true },
  category: { type: String, index: true },
});
 
productSchema.index({ category: 1, price: -1 });  // compound index

Analisa Query dengan explain()

const result = await Product.find({ category: 'electronics' }).explain('executionStats');
 
console.log(result.executionStats.executionStages.stage);
// "COLLSCAN" = collection scan (tanpa index, seperti Seq Scan di PostgreSQL)
// "IXSCAN"   = index scan (pakai index, bagus)

Bab 7: Studi Kasus: Katalog Produk dengan Atribut Bervariasi

Ini kasus yang secara eksplisit kita sebut di artikel #03 sebagai alasan valid pakai JSONB — sekarang bandingkan langsung dengan pendekatan MongoDB.

Schema

// src/models/product.model.js
const productSchema = new mongoose.Schema(
  {
    name: { type: String, required: true },
    category: {
      type: String,
      required: true,
      enum: ['laptop', 'fashion', 'book'],
    },
    price: { type: Number, required: true, min: 0 },
    stock: { type: Number, required: true, default: 0, min: 0 },
 
    // Bentuk field ini berbeda total tergantung category —
    // ini justru kasus yang paling natural untuk MongoDB
    attributes: {
      type: mongoose.Schema.Types.Mixed,
      default: {},
    },
  },
  { timestamps: true }
);
 
productSchema.index({ category: 1, price: 1 });
productSchema.index({ name: 'text' });
 
export const Product = mongoose.model('Product', productSchema);
// Insert laptop
await Product.create({
  name: 'Laptop Gaming X1',
  category: 'laptop',
  price: 15000000,
  stock: 10,
  attributes: { ram: '16GB', cpu: 'Intel i7', storage: '512GB SSD' },
});
 
// Insert baju — struktur attributes TOTAL berbeda, tidak perlu migration
await Product.create({
  name: 'Kemeja Flanel',
  category: 'fashion',
  price: 250000,
  stock: 50,
  attributes: { size: 'L', material: 'Cotton', warna: 'Navy' },
});

Query Berdasarkan Attribute Dinamis

// Cari laptop dengan RAM tertentu
const laptops = await Product.find({
  category: 'laptop',
  'attributes.ram': '16GB',
});
 
// Range harga + kategori — kombinasi filter tetap mudah
const results = await Product.find({
  category: 'fashion',
  price: { $gte: 100000, $lte: 500000 },
  'attributes.size': { $in: ['M', 'L'] },
});

MongoDB vs JSONB untuk Kasus Ini: Perbandingan Jujur

AspekPostgreSQL + JSONBMongoDB
Query field dinamisWHERE attributes->>'ram' = '16GB' + GIN indexfind({ 'attributes.ram': '16GB' }) + index biasa
Data transaksional lain (order, payment)Sama-sama di database, JOIN nativeButuh $lookup atau database terpisah
Skema tetap sebagian besar tabel lainTetap ternormalisasi seperti biasaSemua collection ikut model document
Tim sudah kenal SQLTidak ada tool/paradigma baruPerlu belajar aggregation pipeline

Kesimpulan jujur untuk kasus ini: kalau hanya satu jenis data yang butuh fleksibilitas skema (seperti katalog produk) sementara sisanya (order, user, payment) tetap solid relasional, JSONB di PostgreSQL yang sudah kamu pakai biasanya cukup — tidak perlu menambah database kedua. MongoDB baru lebih masuk akal kalau sebagian besar sistem kamu memang document-shaped, atau kamu butuh scaling horizontal yang MongoDB sediakan out-of-the-box.


Bab 8: Kapan MongoDB, Kapan PostgreSQL

Kerangka Keputusan

Apakah data punya relasi kompleks dan butuh JOIN sering?
├── Ya  → PostgreSQL (JOIN native jauh lebih efisien)
└── Tidak, lanjut ↓

Apakah butuh transaksi multi-record yang ketat (ACID penuh)?
├── Ya  → PostgreSQL (transaction adalah kekuatan utamanya)
└── Tidak, lanjut ↓

Apakah struktur data bervariasi signifikan antar record,
atau berubah bentuk sering seiring waktu?
├── Ya  → MongoDB (schema fleksibel = tidak perlu migration tiap perubahan bentuk)
└── Tidak → PostgreSQL (schema tetap = lebih aman dari typo dan inkonsistensi)

Apakah butuh scaling horizontal (data > yang muat di satu server)?
├── Ya  → MongoDB (sharding native, dirancang untuk ini)
└── Tidak → keduanya bisa, pilih berdasarkan poin di atas

Kasus Nyata yang Umum Ditemui

Jenis dataPilihan yang masuk akalAlasan
User, auth, order, paymentPostgreSQLButuh ACID ketat, relasi jelas, integritas kritikal
Katalog produk (single business)PostgreSQL + JSONBFleksibilitas terbatas ke satu kolom sudah cukup
Katalog produk (marketplace besar, ribuan kategori berbeda)MongoDBSkala fleksibilitas dan volume lebih besar
Log aktivitas, event trackingMongoDBVolume tinggi, struktur bisa berubah, jarang butuh JOIN
Konten CMS (artikel, halaman dinamis)MongoDBStruktur konten antar tipe halaman bisa sangat berbeda
Data analytics/reportingTergantung — sering keduanya (PostgreSQL untuk sumber, MongoDB/data warehouse untuk agregat)Kebutuhan query berbeda dari data operasional

Yang Sering Terlewat: Kamu Boleh Pakai Keduanya

Banyak sistem production nyata bukan "PostgreSQL vs MongoDB" tapi "PostgreSQL dan MongoDB", masing-masing untuk bagian yang cocok. toko-api yang kita bangun di artikel-artikel sebelumnya bisa saja tetap pakai PostgreSQL untuk orders, customers, payments — dan menambahkan MongoDB khusus untuk activity_logs yang volumenya tinggi dan strukturnya beragam. Tidak ada aturan yang memaksa satu database untuk seluruh sistem.

Yang harus dihindari: memilih MongoDB karena "startup besar pakai MongoDB" atau "katanya lebih scalable", padahal data kamu justru sangat relasional. Keputusan database yang baik lahir dari bentuk data kamu, bukan dari nama besar yang memakainya.


Penutup

Referensi Keputusan Cepat

SituasiPilihan
Data transaksional dengan relasi ketatPostgreSQL
Satu jenis entity butuh field fleksibel, sisanya solidPostgreSQL + JSONB
Mayoritas sistem document-shaped, skema sering berubahMongoDB
Butuh sharding/scaling horizontal nativeMongoDB
Item transaksi (order items, cart items)Embed di dokumen induk (MongoDB) — setara tabel terpisah di SQL
Relasi one-to-many tak terbatasReference, jangan embed
Field per-kategori berbeda (product attributes)attributes: Mixed + index pada field yang sering di-query
Butuh JOIN kompleks berkali-kaliSinyal kuat: pakai PostgreSQL

Langkah selanjutnya:

  • Eksplorasi Elasticsearch untuk kebutuhan full-text search yang lebih canggih dari $text MongoDB atau GIN index PostgreSQL (artikel elasticsearch-search)
  • Pelajari Redis untuk caching dan session — database NoSQL lain dengan bentuk masalah yang lagi-lagi berbeda (artikel redis-caching)

Lanjutkan ke

Elasticsearch: Full-Text Search & AnalyticsSegera
Redis: Caching, Session & Rate LimitingSegera