Lewati ke isi

03 — Database

← REST API | Index | Lanjut: Docker →


3.1 Apa Itu Database?

Tempat penyimpanan data yang terstruktur, persisten, dan bisa dicari cepat.

Kenapa tidak simpan di file Excel/JSON saja?

Kebutuhan File biasa Database
1000 user menulis bersamaan 💥 Saling timpa/korup ✅ Diatur dengan aman (locking, transaction)
Cari 1 baris di antara 10 juta Lambat (baca semua) ✅ Milidetik (index)
Jaminan data konsisten Tidak ada ✅ Constraint, transaction
Hak akses berbeda per user Manual ✅ Built-in

Analogi: lemari arsip raksasa dengan sistem indeks, petugas yang mengatur antrean, dan aturan pengarsipan — bukan tumpukan kertas acak.

3.2 Dua Keluarga Besar

SQL (Relational) NoSQL
Contoh MySQL, PostgreSQL, SQLite, SQL Server MongoDB (dokumen), Redis (key-value), DynamoDB
Struktur Tabel (baris & kolom), skema ketat Dokumen/key-value, skema fleksibel
Analogi Spreadsheet yang saling terhubung dengan aturan ketat Folder berisi file JSON bebas bentuk
Jaminan konsistensi Kuat (ACID) Bervariasi
Cocok untuk Data transaksional: order, invoice, user, stok Cache, session, log, data yang strukturnya sering berubah

📌 Untuk 90% aplikasi bisnis, mulai dari SQL (PostgreSQL/MySQL). NoSQL dipakai sebagai pelengkap untuk kebutuhan spesifik — contoh kombinasi umum: PostgreSQL untuk data utama + Redis untuk cache & session.

⚠️ Red flag: AI memilih MongoDB "biar fleksibel" untuk data yang jelas-jelas relasional (order–produk–user). Fleksibilitas skema di data transaksional biasanya berarti inkonsistensi yang ditunda.

3.3 Tabel, Relasi & ERD

Contoh mini e-commerce:

users                    orders                        products
┌────┬─────────┐         ┌────┬─────────┬─────────┐    ┌────┬──────────┬────────┬───────┐
│ id │ name    │         │ id │ user_id │ total   │    │ id │ name     │ price  │ stock │
├────┼─────────┤         ├────┼─────────┼─────────┤    ├────┼──────────┼────────┼───────┤
│ 1  │ Budi    │◄────────│ 1  │ 1       │ 150000  │    │ 1  │ Kaos     │ 75000  │ 10    │
│ 2  │ Sari    │         │ 2  │ 1       │ 120000  │    │ 2  │ Celana   │ 120000 │ 5     │
└────┴─────────┘         └────┴─────────┴─────────┘    └────┴──────────┴────────┴───────┘
      PK: id                  PK: id                        PK: id
                              FK: user_id → users.id

                         order_items  (tabel perantara / pivot)
                         ┌────┬──────────┬────────────┬─────┬───────────────┐
                         │ id │ order_id │ product_id │ qty │ price_at_time │
                         ├────┼──────────┼────────────┼─────┼───────────────┤
                         │ 1  │ 1        │ 1          │ 2   │ 75000         │
                         │ 2  │ 2        │ 2          │ 1   │ 120000        │
                         └────┴──────────┴────────────┴─────┴───────────────┘
  • Primary Key (PK): identitas unik tiap baris — biasanya kolom id.
  • Foreign Key (FK): kolom yang "menunjuk" ke baris di tabel lain. Database bisa menolak data yang menunjuk ke baris yang tidak ada (referential integrity) — order dengan user_id = 999 ditolak kalau user 999 tidak ada.
  • Perhatikan kolom price_at_time di order_items: harga disalin saat transaksi, karena harga produk bisa berubah — tapi invoice lama tidak boleh ikut berubah. Pola penting yang sering dilupakan AI.

Jenis relasi

Relasi Contoh Implementasi
One-to-Many 1 user punya banyak order FK di sisi "many" (orders.user_id)
Many-to-Many Order berisi banyak produk; produk ada di banyak order Tabel perantara (order_items)
One-to-One 1 user punya 1 profil detail FK unik, untuk memisahkan data yang jarang diakses

Diagram relasi antar tabel disebut ERD (Entity Relationship Diagram) — mintalah AI membuatnya sebelum menulis kode:

💡 Prompt: "Sebelum menulis kode, buatkan ERD (dalam format Mermaid) untuk fitur ini: [deskripsi]. Jelaskan alasan setiap relasi dan kolom penting."

3.4 Tipe Data & Constraint

Tipe data yang umum

Kebutuhan Tipe yang tepat Catatan
Teks pendek (nama, email) VARCHAR(255) Beri batas panjang
Teks panjang (deskripsi) TEXT
Bilangan bulat INT / BIGINT BIGINT untuk ID di tabel yang akan sangat besar
Uang DECIMAL(15,2) atau integer satuan terkecil JANGAN FLOAT/DOUBLE — error pembulatan
Benar/salah BOOLEAN
Tanggal-waktu TIMESTAMP/DATETIME Simpan dalam UTC, konversi timezone di aplikasi
Pilihan terbatas (status) ENUM atau VARCHAR + validasi pending, paid, shipped, ...
Data fleksibel JSON/JSONB Untuk atribut yang benar-benar dinamis saja

Constraint — aturan yang dijaga database sendiri

CREATE TABLE users (
  id         BIGINT PRIMARY KEY AUTO_INCREMENT,
  email      VARCHAR(255) NOT NULL UNIQUE,   -- wajib diisi & tidak boleh kembar
  name       VARCHAR(100) NOT NULL,
  balance    DECIMAL(15,2) NOT NULL DEFAULT 0 CHECK (balance >= 0),
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

📌 Wajib paham — validasi berlapis: validasi di aplikasi bisa terlewat (bug, endpoint baru yang lupa validasi, akses langsung ke DB). Constraint di database adalah benteng terakhir yang tidak bisa dilewati. Keduanya perlu.

  • NULL = "tidak ada nilai / belum diketahui" — beda dengan 0 atau string kosong. NULL punya perilaku khusus: NULL = NULL hasilnya bukan true; harus pakai IS NULL. Sumber bug klasik di kode AI.

3.5 SQL Dasar (Cukup untuk Membaca Kode AI)

-- Membaca data
SELECT name, price
FROM products
WHERE price > 50000 AND stock > 0
ORDER BY price DESC
LIMIT 10;

-- Menambah data
INSERT INTO products (name, price, stock) VALUES ('Topi', 45000, 20);

-- Mengubah data
UPDATE products SET price = 80000 WHERE id = 1;

-- Menghapus data
DELETE FROM products WHERE id = 99;

Agregasi — merangkum data

-- Total penjualan per user, hanya yang belanja > 500rb, urut terbesar
SELECT user_id, COUNT(*) AS jumlah_order, SUM(total) AS total_belanja
FROM orders
GROUP BY user_id
HAVING SUM(total) > 500000
ORDER BY total_belanja DESC;

Fungsi agregat: COUNT, SUM, AVG, MIN, MAX. WHERE menyaring sebelum agregasi, HAVING menyaring hasil agregasi.

JOIN — menggabungkan tabel

SELECT orders.id, users.name, orders.total
FROM orders
JOIN users ON orders.user_id = users.id;
Jenis Perilaku
INNER JOIN (default) Hanya baris yang punya pasangan di kedua tabel
LEFT JOIN Semua baris tabel kiri; yang tak berpasangan diisi NULL

Contoh bedanya: "daftar semua user beserta jumlah ordernya, termasuk yang belum pernah order" → wajib LEFT JOIN (INNER JOIN akan menghilangkan user tanpa order).

⚠️ Red flag paling berbahaya di seluruh materi ini: UPDATE atau DELETE tanpa WHERE → mengubah/menghapus SELURUH ISI TABEL. Selalu cek ini di kode hasil AI, terutama di script "cleanup" atau "fix data".

3.6 Index — Kenapa Query Bisa Lambat

Index = daftar isi buku. Tanpa index, database membaca semua baris satu per satu (full table scan).

Tanpa index (1 juta baris):  SELECT * FROM users WHERE email = 'budi@mail.com'
                             → periksa 1.000.000 baris  → ~detik

Dengan index di email:       → lompat langsung via struktur index → ~milidetik

Panduan:

  • Index kolom yang sering dipakai di WHERE, JOIN, dan ORDER BY.
  • PK dan UNIQUE otomatis ter-index. FK sebaiknya di-index (beberapa database tidak otomatis).
  • Composite index (gabungan beberapa kolom) untuk query yang selalu memfilter kombinasi yang sama: INDEX(user_id, status).
  • Trade-off: setiap index memperlambat INSERT/UPDATE/DELETE (index ikut di-update) dan memakan storage. Jangan index semua kolom.

Cara memverifikasi: perintah EXPLAIN menunjukkan rencana eksekusi query — apakah memakai index atau full scan.

💡 Prompt: "Query ini butuh 8 detik di tabel 2 juta baris: [query]. Analisis dengan pendekatan EXPLAIN — index apa yang perlu ditambahkan, dan tulis pernyataan CREATE INDEX-nya."

3.7 Masalah N+1 — Pembunuh Performa Nomor Satu

Bug performa paling umum di kode hasil AI (dan manusia):

// ❌ N+1: 1 query ambil orders + N query ambil user per order
$orders = Order::all();                    // 1 query
foreach ($orders as $order) {
    echo $order->user->name;               // 1 query PER order! 1000 order = 1000 query
}

// ✅ Eager loading: cukup 2 query
$orders = Order::with('user')->get();      // query orders + query users sekaligus
foreach ($orders as $order) {
    echo $order->user->name;               // dari memori, tanpa query
}

Gejalanya: halaman makin lambat seiring data bertambah, padahal "kodenya tidak berubah". Di development (data 10 baris) tidak terasa; di production (10.000 baris) tumbang.

💡 Prompt: "Periksa kode ini untuk masalah N+1 query. Kalau ada, perbaiki dengan eager loading dan jelaskan berapa jumlah query sebelum vs sesudah."

3.8 Transaction — Semua atau Tidak Sama Sekali

Transaction memastikan serangkaian operasi berhasil semua atau gagal semua.

Contoh klasik — checkout order:

BEGIN TRANSACTION
  1. INSERT ke orders                         ✅
  2. INSERT ke order_items                    ✅
  3. UPDATE products SET stock = stock - 2    💥 gagal (stok tinggal 1)
ROLLBACK  → langkah 1 & 2 otomatis dibatalkan, seolah tidak pernah terjadi

Tanpa transaction: order tercatat, item tercatat, stok tidak berkurang → data korup yang baru ketahuan saat rekonsiliasi.

ACID — jaminan yang diberikan database

Sifat Arti
Atomicity Semua atau tidak sama sekali
Consistency Data selalu memenuhi aturan (constraint) sebelum & sesudah
Isolation Transaction yang berjalan bersamaan tidak saling mengganggu
Durability Sekali di-commit, data selamat walau server mati

Race condition — bahaya operasi bersamaan

Dua pembeli checkout produk berstok 1 pada detik yang sama:

Request A: baca stok → 1 ✅       Request B: baca stok → 1 ✅
Request A: stok cukup, buat order  Request B: stok cukup, buat order
Request A: stok = 0                Request B: stok = -1  💥 overselling!

Solusi umum: atomic update (UPDATE products SET stock = stock - 1 WHERE id = 1 AND stock >= 1 — lalu cek apakah ada baris yang ter-update) atau row locking (SELECT ... FOR UPDATE). Detailnya teknis, tapi yang wajib Anda tahu: cek-lalu-tulis dalam dua langkah terpisah itu rawan, dan Anda bisa memintanya secara eksplisit:

💡 Prompt: "Endpoint checkout ini akan diakses bersamaan oleh banyak user. Pastikan pengurangan stok aman dari race condition — pakai atomic update atau locking, dan jelaskan pilihanmu."

3.9 Migration & Seeding

Migration = file berisi perubahan struktur database yang di-track di Git dan dijalankan berurutan:

migrations/
  2026_01_10_create_users_table.php
  2026_02_03_create_orders_table.php
  2026_07_01_add_phone_to_users_table.php   ← perubahan baru = file baru

Aturan main:

  • Semua environment (local, staging, production) menjalankan urutan migration yang sama → struktur identik & bisa direproduksi.
  • Migration yang sudah jalan di production tidak diedit — buat migration baru untuk mengubahnya.
  • Migration yang baik punya down()/rollback untuk membatalkan.

Seeding = mengisi database dengan data awal (kategori default, akun admin) atau data dummy untuk development/testing.

⚠️ Red flag: mengubah struktur database production manual lewat GUI (phpMyAdmin, TablePlus), atau AI yang "memperbaiki" migration lama alih-alih membuat yang baru.

3.10 ORM vs Raw SQL

ORM (Object-Relational Mapping) — library yang memetakan tabel ke objek kode: Eloquent (Laravel), Prisma (Node), SQLAlchemy (Python).

// ORM (Eloquent)                          // SQL yang dihasilkan
User::where('active', true)               // SELECT * FROM users
    ->orderBy('name')                     // WHERE active = 1
    ->get();                              // ORDER BY name
ORM Raw SQL
Kecepatan menulis ✅ Cepat, aman dari injection secara default Lebih verbose
Query kompleks (report, agregasi berat) Kadang canggung/tidak efisien ✅ Kontrol penuh
Risiko Query tidak efisien yang tersembunyi (N+1!) SQL injection kalau string concat

Praktik sehat: ORM untuk CRUD sehari-hari, raw query (yang tetap parameterized) untuk report kompleks.

3.11 Pola Praktis Lainnya

  • Soft delete: alih-alih DELETE, tandai baris dengan deleted_at. Data "terhapus" bagi user, tapi bisa dipulihkan & tetap ada untuk audit. Konsekuensi: semua query harus memfilter deleted_at IS NULL (ORM biasanya otomatis).
  • Timestamps standar: kolom created_at & updated_at di hampir semua tabel — murah, dan sangat berharga saat investigasi masalah.
  • Connection pooling: membuka koneksi database itu mahal; aplikasi memakai "kolam" koneksi yang dipakai bergantian. Relevan saat melihat error too many connections.
  • Backup — aturan 3-2-1: 3 salinan data, 2 media berbeda, 1 off-site. Dan: backup yang tidak pernah dites restore = tidak punya backup. Jadwalkan tes restore berkala.

3.12 Prompting AI untuk Database

💡 Desain skema: "Rancang skema database untuk [fitur]. Sertakan: ERD Mermaid, pilihan tipe data dengan alasan (khususnya kolom uang & tanggal), constraint (NOT NULL, UNIQUE, FK), dan index awal. Gunakan soft delete untuk tabel [X]."

💡 Review query: "Review query/kode ini: cek UPDATE/DELETE tanpa WHERE, potensi N+1, operasi multi-tabel tanpa transaction, race condition pada stok/saldo, dan kolom yang butuh index."

💡 Migration: "Buat migration untuk menambah kolom phone (nullable, VARCHAR 20) ke tabel users — sebagai migration BARU, jangan mengubah migration lama. Sertakan rollback."


3.13 🧪 TUTORIAL PRAKTIK: Merancang Skema dari Requirement Nyata

Estimasi 60 menit. Latihan ini melatih skill yang paling menentukan kualitas hasil vibe coding di sisi data: menerjemahkan kebutuhan bisnis → struktur data yang benar, SEBELUM menyentuh AI.

Langkah 1 — Baca requirement seperti seorang data modeler (10 menit)

Requirement dari "klien":

"Kami butuh sistem membership. Member punya paket (Silver/Gold/Platinum) yang bisa berubah kapan saja. Setiap bulan member ditagih sesuai paket AKTIF saat tagihan dibuat. Kami perlu tahu riwayat paket member dari waktu ke waktu, dan tagihan lama tidak boleh berubah walau harga paket naik."

Garis bawahi kata kunci yang menentukan struktur:

  • "bisa berubah kapan saja" + "riwayat dari waktu ke waktu" → relasi member↔paket bukan kolom biasa, tapi tabel riwayat dengan periode berlaku
  • "tagihan lama tidak boleh berubah walau harga naik" → harga disalin ke tagihan (pola price_at_time, §3.3)

Langkah 2 — Sketsa ERD sendiri dulu (15 menit)

Sebelum bertanya ke AI, sketsakan (di kertas boleh):

plans                     members                 member_plans (riwayat)
┌────┬──────────┬───────┐ ┌────┬────────┐        ┌────┬───────────┬─────────┬────────────┬──────────┐
│ id │ name     │ price │ │ id │ name   │        │ id │ member_id │ plan_id │ started_at │ ended_at │
└────┴──────────┴───────┘ └────┴────────┘        └────┴───────────┴─────────┴────────────┴──────────┘
                                                   ended_at NULL = paket yang AKTIF sekarang
invoices
┌────┬───────────┬─────────┬─────────────────┬────────────┬─────────┐
│ id │ member_id │ plan_id │ amount          │ period     │ status  │
└────┴───────────┴─────────┴─────────────────┴────────────┴─────────┘
                              ↑ harga DISALIN saat tagihan dibuat

Langkah 3 — Minta AI menantang desain Anda (10 menit)

Ini pola prompt yang sangat berguna — AI sebagai sparring partner, bukan tukang ketik:

💡 "Ini desain skema saya untuk requirement berikut: [requirement + sketsa]. Jangan langsung setuju. Tantang desain ini: skenario apa yang akan merusaknya? Bagaimana kalau member upgrade di tengah bulan? Bagaimana mencegah dua baris member_plans aktif bersamaan untuk satu member? Constraint apa yang kurang?"

Jawaban yang bagus akan menyinggung: unique partial constraint / validasi "hanya satu ended_at NULL per member", kebijakan penagihan prorata, dan index di member_plans(member_id, ended_at).

Langkah 4 — Generate & verifikasi DDL (10 menit)

💡 "Sekarang tulis migration untuk skema final, dengan: tipe uang DECIMAL, semua FK + index-nya, constraint yang kita diskusikan, timestamps, dan rollback."

Checklist verifikasi hasil AI:

  • [ ] price/amount bertipe DECIMAL, bukan FLOAT
  • [ ] Semua FK ada dan ter-index
  • [ ] invoices.amount ada (harga disalin) — bukan cuma plan_id
  • [ ] Ada mekanisme mencegah dobel paket aktif
  • [ ] Migration punya down()

Langkah 5 — Tulis 3 query bisnis paling penting (15 menit)

Minta AI menulis, lalu baca dan pahami setiap barisnya:

  1. "Paket aktif setiap member saat ini" → harus memakai ended_at IS NULL (perilaku NULL, §3.4!)
  2. "Total pendapatan per paket per bulan" → GROUP BY + SUM(amount) dari invoices (bukan dari harga plans sekarang — kenapa? karena harga bisa sudah berubah!)
  3. "Member yang belum bayar tagihan bulan ini" → LEFT JOIN + IS NULL atau filter status

Kalau Anda bisa menjelaskan kenapa query #2 harus dari invoices, Anda sudah memahami inti pemodelan data transaksional — selamat.


Rangkuman Bab

  • SQL untuk data transaksional; NoSQL pelengkap untuk kebutuhan spesifik.
  • PK/FK + constraint = integritas yang dijaga database sendiri; validasi aplikasi saja tidak cukup.
  • Uang: DECIMAL/integer, jangan float. Tanggal: UTC + ISO 8601. Salin harga saat transaksi (price_at_time).
  • UPDATE/DELETE tanpa WHERE = bencana. Selalu cek.
  • Index mempercepat baca, memperlambat tulis — pasang dengan sengaja, verifikasi dengan EXPLAIN.
  • N+1 adalah bug performa paling umum di kode AI; solusinya eager loading.
  • Transaction untuk operasi multi-tabel; waspadai race condition pada stok/saldo.
  • Perubahan skema hanya lewat migration; backup wajib dites restore.

← REST API | Index | Lanjut: Docker →