DBA mencegah masalah performansi sejak desain: praktik DDL/DML yang bersih, strategi normalisasi 1NF-3NF versus denormalisasi yang disengaja, pemilihan data type yang hemat storage, serta pembuatan index B-tree, unique, dan partial yang tepat sasaran lewat studi kasus schema e-commerce

Di episode 3 server kalian sudah hidup dan terkonfigurasi. Sekarang masuk ke bagian yang membedakan DBA yang hanya "jaga server" dengan DBA yang mencegah masalah: desain database dan administrasi SQL. Prinsip yang akan kita pegang: a bad schema is a lifetime of slow queries. Query yang lambat jarang lahir karena SQL-nya jelek — akarnya hampir selalu schema dan index yang salah dari awal.
Development DBA (episode 1) menghabiskan sebagian besar waktunya di sini: mereview migration, mengecek rencana index, dan menegakkan standar. Kalian akan memakai pengetahuan ini untuk mereview desain tim, bukan hanya mengeksekusi perintah.
Sebelum desain, pastikan dasar administrasi DDL/DML kalian rapi. Contoh penamaan dan struktur yang konsisten:
CREATE TABLE users (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
full_name TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'active'
CHECK (status IN ('active', 'suspended')),
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE orders (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id),
total_cents BIGINT NOT NULL CHECK (total_cents >= 0),
status TEXT NOT NULL DEFAULT 'pending',
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);Poin yang wajib dipahami sebagai DBA:
BIGINT GENERATED ALWAYS AS IDENTITY adalah standar modern (jangan SERIAL — identity lebih aman dari privilege).CHECK, NOT NULL, dan foreign key di database mencegah data korup walau aplikasi punya bug.total_cents), bukan FLOAT — menghindari error pembulatan yang menyakitkan.TIMESTAMPTZ menyimpan waktu dengan zona; TIMESTAMP tanpa zona adalah undangan untuk bug lintas waktu.Normalisasi adalah proses menghilangkan redundansi agar data tidak duplikat. Tiga tahap awal yang paling sering dipakai:
Contoh pelanggaran 3NF: tabel orders menyimpan user_name dan user_email — jika nama user berubah, data di semua order harus di-update. Solusinya: simpan user_id, dan join ke users saat butuh nama. Normalisasi menghemat storage dan mencegah anomalies (data tidak konsisten).
Normalisasi bukan harga mati. Denormalisasi adalah keputusan sadar untuk menduplikasi data demi kecepatan baca — dengan konsekuensi yang dikelola. Kapan layak?
order_count di tabel users yang di-increment tiap order — mempercepat dashboard, tapi wajib dijaga konsistensinya (ideal via trigger atau materialized view, bukan kode aplikasi).Aturan DBA: normalisasi dulu sampai 3NF, lalu denormalisasi secara sadar dan terdokumentasi — bukan karena malas membuat join. Setiap denormalisasi wajib punya alasan tertulis di review.
Pemilihan tipe data menentukan ukuran tabel dan performansi index:
| Kebutuhan | Jangan pakai | Pakai |
|---|---|---|
| Angka kecil | BIGINT | INT/SMALLINT |
| Uang | FLOAT/DOUBLE | BIGINT (cents) atau NUMERIC |
| Teks pendek | TEXT | VARCHAR(n) dengan batas |
| Tanggal tanpa jam | TIMESTAMPTZ | DATE |
| Boolean | INT 0/1 | BOOLEAN |
| Enum statis | TEXT bebas | ENUM/CHECK |
Penghematan tiap kolom tampak kecil, tetapi di tabel dengan 100 juta baris, SMALLINT vs BIGINT menghemat 600 MB plus mempercepat scan.
Index mempercepat query, tapi setiap index memperlambat INSERT/UPDATE dan memakan disk. Prinsipnya: index untuk query yang benar-benar dijalankan, bukan untuk semua kolom.
-- Query umum: WHERE status = 'pending' ORDER BY created_at
CREATE INDEX idx_orders_status_created
ON orders (status, created_at);
-- Unik: menjamin tidak ada email ganda (juga jadi index)
CREATE UNIQUE INDEX idx_users_email ON users (email);
-- Partial: hanya index baris yang relevan (hemat storage)
CREATE INDEX idx_orders_pending ON orders (created_at)
WHERE status = 'pending';Pola yang kalian akan review terus-menerus:
WHERE col = value, >, <, ORDER BY, dan join.(status, created_at) membantu query WHERE status = 'pending' ORDER BY created_at; index (created_at, status) tidak membantu karena filter status ada di posisi kedua.WHERE status = 'pending') ideal untuk data yang mayoritas sudah "selesai" — hanya baris aktif yang di-index.Tip
Cara termudah menemukan query yang butuh index: aktifkan log slow query (episode 7), ambil query teratas, lalu jalankan EXPLAIN (episode 6) untuk melihat apakah ada sequential scan pada tabel besar. Index yang bagus mengubah scan penuh menjadi index scan. Jangan membuat index sebelum melihat bukti query — ini prinsip "index on evidence, not on speculation".
Mari aplikasikan. Store orders, users, dan products. Query prioritas bisnis:
WHERE user_id = $1 AND status = 'pending' → index (user_id, status).created_at → butuh index (created_at) atau partial untuk periode berjalan.ILIKE '%term%' tidak akan memakai B-tree biasa; di episode 21 kita bahas trigram/pg_trgm.Review checklist sebelum approval migration:
created_at.SELECT * yang dikirim tanpa batas.status punya CHECK atau ENUM, bukan teks bebas.EXPLAIN).Inti yang harus dibawa pulang:
EXPLAIN), dengan partial/compound index untuk pola umum; FK wajib di-index di PostgreSQL.Di episode 5 selanjutnya kita membahas salah satu tanggung jawab terpenting DBA: backup & recovery — perbedaan logical vs physical backup, point-in-time recovery memakai WAL, sampai budaya drill restore yang menjamin backup kalian benar-benar bisa dipakai saat bencana. Ini materi yang tidak boleh kalian lewatkan. Sampai jumpa di episode 5!