Database & Pemodelan Data (ERD)
Fondasi Setiap Aplikasi · Galih Prasetyo · 2026
“Data adalah salah satu aset terpenting perusahaan mana pun, karena aset itu unik untuk perusahaan tersebut. Berbeda dengan kode sumber yang bisa ditulis ulang, data yang hilang tidak bisa dipulihkan.”
— Martin Kleppmann, Designing Data-Intensive Applications (2017)
Setiap aplikasi yang pernah Anda pakai — toko daring, aplikasi perbankan, media sosial, dasbor SaaS, sampai catatan sederhana di ponsel — pada dasarnya adalah antarmuka indah di atas satu hal: data yang tersimpan dan berelasi. Kode datang dan pergi, kerangka kerja berganti setiap beberapa tahun, tetapi model data cenderung hidup jauh lebih lama daripada kode yang menulisnya. Salah memodelkan data pada awal proyek adalah salah satu kesalahan yang paling mahal dan paling menyakitkan untuk diperbaiki di kemudian hari.
Buku ini adalah panduan menyeluruh, dari nol hingga produksi, tentang cara berpikir tentang data: bagaimana menemukan entitas, menggambar hubungan lewat Entity-Relationship Diagram (ERD), menormalkan tabel agar konsisten, lalu mewujudkannya di PostgreSQL modern lewat Neon dan Prisma — dengan indexing yang tepat, transaksi yang aman, dan pola-pola yang terbukti di lapangan.
Ditujukan untuk semua level: pemula yang belum pernah menulis satu baris SQL pun, pengembang menengah yang ingin memantapkan fondasi, hingga arsitek yang mencari checklist dan pola siap pakai. Buku ini juga sengaja ditulis agar mudah dibaca oleh AI/LLM — lihat Bab 15.
Kata Pengantar
Sepanjang lebih dari satu dekade membangun perangkat lunak, satu pola berulang selalu saya temui: proyek yang gagal jarang gagal karena pilihan bahasa pemrograman atau kerangka kerja front-end. Mereka gagal karena model datanya keliru. Tabel yang menampung dua makna sekaligus, relasi yang tidak pernah dipikirkan, kolom teks yang menampung angka, uang yang disimpan sebagai float, atau tidak adanya satu pun indeks pada kolom yang di-query jutaan kali sehari.
Kabar baiknya: pemodelan data yang baik bukan bakat bawaan, melainkan keterampilan yang bisa dipelajari. Ada sekumpulan prinsip yang berumur puluhan tahun — model relasional, teori normalisasi, properti ACID — yang tetap relevan justru karena mereka menggambarkan kebenaran matematis tentang bagaimana fakta saling berhubungan. Di atas prinsip abadi itu, ada lapisan alat modern yang membuat penerapannya jauh lebih menyenangkan daripada satu dekade lalu: Postgres yang makin kuat, Neon yang menjadikannya serverless dengan branching ala Git, dan Prisma yang menjadikan skema sebagai satu sumber kebenaran yang aman-tipe.
Buku ini menautkan keduanya. Kita mulai dari teori yang tidak akan usang, lalu turun ke praktik konkret dengan kode yang bisa Anda salin dan jalankan. Di antara keduanya, Anda akan menemukan sebuah ERD Builder interaktif — tempel deskripsi skema dalam JSON, dan lihat diagramnya tergambar langsung di halaman. Belajar memodelkan data paling efektif saat Anda bisa melihat bentuknya.
“Saya, tentu saja, menyadari bahwa masih ada resistensi yang cukup besar terhadap model relasional. Namun, saya percaya bahwa model relasional itu sederhana, dan kesederhanaan itu pada akhirnya akan menang.”
— Edgar F. Codd, penemu model relasional (IBM, 1970)
Selamat membaca, dan selamat memodelkan. Semoga fondasi yang Anda bangun setelah ini cukup kokoh untuk menahan aplikasi apa pun yang Anda dirikan di atasnya.
— Galih Prasetyo, 2026
Cara Membaca Buku Ini
Buku ini dirancang untuk dibaca secara berurutan pada pembacaan pertama, karena setiap bab membangun kosakata bab berikutnya. Namun setelah itu, ia juga berfungsi sebagai referensi — lompat ke bab mana pun lewat daftar isi di kiri.
Peta jalan singkat
| Jika Anda… | Mulai dari |
|---|---|
| Belum pernah menyentuh database | Bab 1 → 2 → 3, lalu main-main dengan ERD Builder |
| Bisa SQL dasar, ingin mendalami desain | Bab 3 (ERD) → 4 (Normalisasi) → 14 (Pola) |
| Sedang memilih tumpukan teknologi | Bab 5 (Postgres) → 6 (Neon) → 7 (Prisma) |
| Aplikasi lambat / ingin optimasi | Bab 8 (Indexing) → 9 (JOIN) → 12 (Caching) |
| Membangun SaaS multi-penyewa | Bab 10 (Transaksi) → 11 (Multi-Tenant) |
| Menggunakan AI untuk menulis skema | Bab 15 (Cara Dibaca AI) |
Konvensi
- SQL dan Prisma menandai blok kode yang bisa langsung disalin. Semua contoh diuji secara konseptual terhadap PostgreSQL 15+ dan Prisma 5+.
- Kotak Catatan berwarna teal berisi tip praktik terbaik; kotak kuning berisi peringatan jebakan umum; kotak biru ditujukan khusus untuk konteks AI/LLM.
- Istilah teknis dicetak tebal saat pertama kali muncul dan dirangkum di Glosarium.
- Diagram memakai notasi crow's foot yang disederhanakan; kardinalitas ditandai
1,N, atauM.
Pendahuluan: Data adalah Fondasi
Bayangkan sebuah toko daring sederhana. Ada orang yang mendaftar, mereka menaruh barang di keranjang, lalu membuat pesanan yang berisi beberapa produk, dan membayar. Terdengar mudah. Tetapi di balik empat kalimat itu tersembunyi keputusan-keputusan yang akan menentukan apakah aplikasi Anda bertahan saat pengguna bertambah dari sepuluh menjadi sepuluh juta.
Di mana alamat pengguna disimpan — satu kolom teks, atau tabel tersendiri? Apakah satu pesanan boleh berisi produk yang sama dua kali? Apa yang terjadi pada riwayat pesanan ketika harga produk berubah bulan depan? Bagaimana Anda memastikan stok tidak terjual dua kali saat dua orang membeli barang terakhir pada saat yang sama? Semua pertanyaan ini adalah pertanyaan pemodelan data, dan jawabannya menentukan bentuk database Anda.
Apa itu database, sebenarnya?
Sebuah database adalah kumpulan data yang terorganisasi sehingga bisa disimpan, dicari, diubah, dan dijaga konsistensinya secara efisien. Database Management System (DBMS) adalah perangkat lunak yang mengelolanya — PostgreSQL, MySQL, SQLite, MongoDB adalah contoh DBMS. Buku ini berfokus pada DBMS relasional, khususnya PostgreSQL, karena ia adalah pilihan default yang tepat untuk mayoritas aplikasi.
Mengapa relasional menjadi default? Karena sebagian besar data dunia nyata memang berelasi: pengguna punya pesanan, pesanan punya item, item merujuk produk. Model relasional memberi Anda tiga jaminan yang sulit didapat di tempat lain: integritas (data tidak bisa menunjuk ke sesuatu yang tidak ada), konsistensi (aturan ditegakkan oleh database, bukan berharap kode selalu benar), dan fleksibilitas kueri (Anda bisa menanyakan hal yang tidak Anda antisipasi saat mendesain).
Tiga tingkat pemikiran tentang data
Praktisi database membedakan tiga tingkat abstraksi. Memahami perbedaannya mencegah kebingungan besar:
| Tingkat | Menjawab pertanyaan | Artefak |
|---|---|---|
| Konseptual | Apa entitas bisnis dan bagaimana mereka berelasi? | ERD tingkat tinggi (Pengguna, Pesanan, Produk) |
| Logis | Bagaimana strukturnya sebagai tabel, kolom, kunci? | Skema ternormalisasi, tipe atribut |
| Fisik | Bagaimana disimpan dan dioptimasi di DBMS nyata? | DDL SQL, indeks, partisi, tipe khusus Postgres |
Buku ini bergerak dari atas ke bawah: Bab 2–4 membangun tingkat konseptual dan logis; Bab 5 dan seterusnya turun ke fisik. Kesalahan umum pemula adalah melompat langsung ke tingkat fisik (“ayo bikin tabel!”) tanpa memikirkan konseptualnya lebih dulu.
Peta perjalanan buku ini
Berikut alur besar yang akan kita tempuh, dari ide bisnis sampai basis data produksi:
Setiap tahap punya bab tersendiri. Yang membedakan buku ini dari tutorial biasa: kita tidak akan berhenti di “cara membuat tabel”. Kita akan membahas mengapa setiap keputusan diambil, jebakan yang menanti, dan bagaimana keputusan itu bermain saat skala membesar.
Fundamental Relasional
Model relasional diperkenalkan oleh Edgar F. Codd pada tahun 1970 dalam makalah yang kini legendaris, “A Relational Model of Data for Large Shared Data Banks”. Idenya sederhana namun radikal untuk zamannya: simpan data sebagai kumpulan relasi (yang kita sebut tabel), dan biarkan pengguna menanyakannya dengan bahasa deklaratif tanpa perlu tahu bagaimana data disimpan secara fisik.
Tabel, baris, dan kolom
Anatomi dasar sebuah tabel relasional:
- Tabel (relasi) — sekumpulan data tentang satu jenis hal. Contoh: tabel
users. - Baris (tuple/record) — satu instance dari hal itu. Satu baris = satu pengguna.
- Kolom (atribut) — satu properti dari hal itu, dengan tipe data tetap. Kolom
emailbertipe teks. - Nilai — sel di persimpangan baris dan kolom. Idealnya atomik (tak terbagi lagi).
Sebuah prinsip penting: urutan baris tidak bermakna. Tabel adalah himpunan, bukan daftar terurut. Jika Anda butuh urutan, Anda harus menyatakannya lewat kolom (misalnya ORDER BY created_at). Mengandalkan “urutan penyimpanan” adalah bug yang menunggu terjadi.
Kunci: identitas dan relasi
Kunci adalah konsep paling penting di seluruh buku ini. Ada beberapa jenis yang wajib Anda kuasai.
Primary Key (PK)
Kolom (atau gabungan kolom) yang mengidentifikasi setiap baris secara unik. Setiap tabel sebaiknya punya tepat satu primary key. PK tidak boleh NULL dan tidak boleh duplikat. Ada dua mazhab:
- Natural key — memakai data bisnis yang sudah unik (misalnya NIK, ISBN). Berisiko: data bisnis bisa berubah atau ternyata tidak seunik yang dikira.
- Surrogate key — kunci buatan tanpa makna bisnis:
BIGINTauto-increment atauUUID. Hampir selalu pilihan yang lebih aman dan direkomendasikan.
id BIGINT GENERATED ALWAYS AS IDENTITY atau UUID), dan tegakkan keunikan data bisnis dengan UNIQUE constraint terpisah. Anda mendapat stabilitas identitas dan integritas bisnis sekaligus.Foreign Key (FK)
Kolom yang menunjuk ke primary key tabel lain, menciptakan relasi. Inilah “relasional” dalam basis data relasional. FK menegakkan integritas referensial: Anda tidak bisa membuat pesanan untuk pengguna yang tidak ada, dan — tergantung aturan — tidak bisa menghapus pengguna yang masih punya pesanan.
Candidate & Composite Key
Sebuah tabel bisa punya beberapa candidate key (kolom yang bisa jadi PK). Yang dipilih menjadi PK; sisanya menjadi UNIQUE. Composite key adalah kunci yang terdiri dari lebih dari satu kolom — umum pada tabel penghubung many-to-many, misalnya (order_id, product_id).
CREATE TABLE users (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, -- surrogate PK
email TEXT NOT NULL UNIQUE, -- candidate key bisnis
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) ON DELETE RESTRICT, -- FK
total NUMERIC(12,2) NOT NULL DEFAULT 0,
status TEXT NOT NULL DEFAULT 'pending'
);
-- tabel penghubung dengan composite PK
CREATE TABLE order_items (
order_id BIGINT NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
product_id BIGINT NOT NULL REFERENCES products(id),
quantity INT NOT NULL CHECK (quantity > 0),
unit_price NUMERIC(12,2) NOT NULL,
PRIMARY KEY (order_id, product_id) -- composite key
);
NULL: nilai yang bukan nilai
NULL berarti “tidak diketahui” atau “tidak berlaku” — bukan nol, bukan string kosong. Ini menyebabkan logika tiga nilai: sebuah perbandingan bisa bernilai true, false, atau unknown. NULL = NULL menghasilkan NULL, bukan true. Karena itu Anda memakai IS NULL, bukan = NULL. Batasi penggunaan NULL: bila sebuah kolom seharusnya selalu ada, tegakkan dengan NOT NULL.
SQL dalam satu tarikan napas
SQL terbagi menjadi beberapa sub-bahasa. Mengenali kategorinya membantu Anda menavigasi dokumentasi:
| Kategori | Kepanjangan | Perintah utama |
|---|---|---|
| DDL | Data Definition Language | CREATE, ALTER, DROP |
| DML | Data Manipulation Language | INSERT, UPDATE, DELETE |
| DQL | Data Query Language | SELECT |
| DCL | Data Control Language | GRANT, REVOKE |
| TCL | Transaction Control Language | BEGIN, COMMIT, ROLLBACK |
“SQL adalah bahasa yang mudah dipelajari tetapi sulit dikuasai. Kekuatannya justru terletak pada sifat deklaratifnya: Anda menyatakan apa yang diinginkan, bukan bagaimana mendapatkannya.”
— C. J. Date, SQL and Relational Theory (2011)
Desain ERD: Menggambar Data
Sebuah Entity-Relationship Diagram (ERD) adalah peta dari data Anda. Diperkenalkan oleh Peter Chen pada 1976, ERD memberi Anda cara visual untuk menangkap entitas (hal yang ingin Anda simpan), atribut (properti setiap hal), dan relasi (bagaimana hal-hal itu terhubung) — jauh sebelum satu baris SQL pun ditulis. ERD adalah bahasa bersama antara pengembang, analis bisnis, dan pemangku kepentingan.
“Model entity-relationship mengadopsi pandangan dunia nyata yang lebih alami yang terdiri dari entitas dan relasi. Ia menggabungkan sebagian informasi semantik penting tentang dunia nyata.”
— Peter Chen, The Entity-Relationship Model (ACM TODS, 1976)
Tiga komponen ERD
1. Entitas
Sebuah entitas adalah objek atau konsep yang dapat dibedakan dan ingin Anda simpan datanya: Pengguna, Pesanan, Produk, Kategori. Aturan praktis: entitas biasanya adalah kata benda dalam deskripsi bisnis Anda, dan Anda punya banyak instance darinya. Setiap entitas biasanya menjadi satu tabel.
Ada perbedaan halus antara entitas kuat (strong entity) yang bisa berdiri sendiri (Pengguna) dan entitas lemah (weak entity) yang keberadaannya bergantung pada entitas lain dan tidak punya kunci sendiri yang bermakna. Contoh klasik: OrderItem tidak punya makna tanpa Order induknya; identitasnya bergantung pada order tersebut. Weak entity biasanya digambar dengan bingkai ganda dan memakai composite key yang menyertakan kunci induknya.
2. Atribut
Properti dari sebuah entitas. Pengguna punya atribut email, name, created_at. Beberapa jenis atribut:
- Key attribute — mengidentifikasi entitas secara unik (primary key), biasa digarisbawahi.
- Simple vs composite —
ageitu sederhana;full_nameyang terdiri dari depan+belakang itu komposit. - Single vs multi-valued — nomor telepon bisa banyak; multi-valued jadi sinyal Anda butuh tabel terpisah.
- Derived — nilai yang bisa dihitung dari yang lain (mis.
umurdaritanggal_lahir); sering tidak disimpan.
3. Relasi
Hubungan antar entitas, biasanya sebuah kata kerja: Pengguna membuat Pesanan; Pesanan berisi Produk. Sifat terpenting sebuah relasi adalah kardinalitasnya.
Kardinalitas: 1-1, 1-N, N-N
Kardinalitas menjawab: “Berapa banyak instance entitas B yang bisa terhubung ke satu instance entitas A?” Ada tiga pola dasar, dan cara implementasinya berbeda-beda.
| Tipe | Contoh | Implementasi |
|---|---|---|
| One-to-One (1:1) | Pengguna ↔ Profil | FK + UNIQUE di salah satu tabel (atau gabungkan) |
| One-to-Many (1:N) | Pengguna → Pesanan | FK di sisi “many” (kolom user_id di orders) |
| Many-to-Many (N:M) | Pesanan ↔ Produk | Tabel penghubung (junction table) dengan dua FK |
Aturan emas yang wajib dihafal:
-- 1:1 Pengguna punya tepat satu Profil
CREATE TABLE profiles (
user_id BIGINT PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
bio TEXT,
avatar TEXT
); -- PK == FK memaksa 1:1
-- 1:N Satu Pengguna, banyak Pesanan (FK di sisi many)
-- (lihat tabel orders di Bab 2: kolom user_id)
-- N:M Pesanan <-> Produk lewat junction table
CREATE TABLE order_items (
order_id BIGINT REFERENCES orders(id),
product_id BIGINT REFERENCES products(id),
quantity INT NOT NULL,
PRIMARY KEY (order_id, product_id)
);
Notasi crow's foot
Notasi paling populer untuk menggambar kardinalitas adalah crow's foot (kaki gagak), dinamai dari simbol bercabang tiga di ujung garis yang berarti “banyak”. Berikut ringkasan simbolnya:
Untuk kesederhanaan, buku ini dan ERD Builder di bawah memakai label ringkas: 1, N, dan M. Yang penting bukan gaya gambarnya, melainkan bahwa Anda dan tim sepakat membaca arah dan jumlah relasi dengan benar.
"3,7,12"). Ini menghancurkan integritas referensial, membunuh performa kueri, dan membuat JOIN mustahil. Jangan pernah menyimpan daftar relasi di dalam satu kolom.◆ ERD Builder Interaktif
Teori paling melekat saat Anda melihatnya. Alat di bawah ini mengubah deskripsi skema dalam JSON menjadi diagram ERD yang tergambar langsung. Ubah JSON-nya, klik Render ERD, dan amati bagaimana entitas serta relasi tersusun. Format masukan:
{
"entities": [
{ "name": "users",
"fields": [
{ "name": "id", "type": "BIGINT", "pk": true },
{ "name": "email", "type": "TEXT" }
] }
],
"relations": [ ["users", "orders", "1-N"] ]
}
Field boleh menandai "pk": true (primary key, disorot kuning) dan "fk": true (foreign key, disorot teal). Relasi adalah triplet [dari, ke, tipe] dengan tipe "1-1", "1-N", atau "N-N".
JSON.parse, lalu diagram dirender sebagai SVG. Anda (LLM) dapat menghasilkan JSON berformat sama untuk menggambarkan skema apa pun yang diminta pengguna, lalu menempelkannya di sini untuk verifikasi visual instan.Normalisasi & Denormalisasi
Normalisasi adalah proses menata kolom dan tabel agar setiap fakta disimpan tepat satu kali. Tujuannya menghapus redundansi (data yang berulang) dan anomali (kejanggalan saat menyisipkan, mengubah, atau menghapus data). Codd merumuskan serangkaian “bentuk normal” bertingkat; untuk mayoritas aplikasi, mencapai 3NF sudah lebih dari cukup.
“Normalkan sampai sakit, denormalkan sampai berfungsi.” Sebuah pepatah lama di kalangan DBA yang menangkap keseimbangan antara kebersihan teoretis dan kebutuhan performa nyata.
— adagium umum praktisi basis data
Mengapa redundansi berbahaya
Perhatikan tabel “buruk” ini yang menyimpan pesanan dan menyalin data pelanggan di setiap baris:
| order_id | customer_name | customer_email | product | category |
|---|---|---|---|---|
| 1 | Andi | andi@mail.id | Keyboard; Mouse | Aksesori |
| 2 | Andi | andi@mail.id | Monitor | Layar |
Masalahnya berlapis: (1) Update anomaly — jika Andi ganti email, Anda harus mengubah banyak baris; lupa satu, data jadi tak konsisten. (2) Insertion anomaly — Anda tak bisa mendaftarkan pelanggan baru tanpa pesanan. (3) Deletion anomaly — menghapus pesanan terakhir Andi menghapus jejak Andi sama sekali. (4) Kolom product berisi banyak nilai dalam satu sel — melanggar 1NF.
First Normal Form (1NF)
Aturan: setiap sel berisi satu nilai atomik; tidak ada kelompok berulang atau daftar dalam satu kolom. Perbaikannya: pecah "Keyboard; Mouse" menjadi baris-baris terpisah di tabel item. Setiap perpotongan baris-kolom hanya menyimpan satu fakta.
JSONB Postgres untuk data yang memang tak terstruktur adalah cerita berbeda — lihat Bab 5. Yang dilarang 1NF adalah menyembunyikan relasi di dalam teks.)Second Normal Form (2NF)
Aturan: sudah 1NF, dan setiap atribut non-kunci bergantung pada seluruh primary key, bukan sebagian. Ini hanya relevan saat PK Anda komposit. Contoh: pada order_items(order_id, product_id, quantity, product_name), kolom product_name hanya bergantung pada product_id — sebagian dari kunci. Ini pelanggaran 2NF; pindahkan product_name ke tabel products.
Third Normal Form (3NF)
Aturan: sudah 2NF, dan tidak ada ketergantungan transitif — atribut non-kunci tidak boleh bergantung pada atribut non-kunci lain. Contoh: jika orders punya (id, user_id, user_city), maka user_city bergantung pada user_id (non-kunci), bukan langsung pada id. Solusi: user_city milik tabel users, bukan orders.
Panduan mnemonik yang terkenal: setiap atribut non-kunci harus bergantung pada “the key, the whole key, and nothing but the key” — “kunci (1NF), seluruh kunci (2NF), dan hanya kunci itu (3NF), sungguh, demi Codd.”
Ringkasan bentuk normal
| Bentuk | Menghapus | Aturan inti |
|---|---|---|
| 1NF | Nilai non-atomik, grup berulang | Satu nilai per sel |
| 2NF | Ketergantungan parsial | Non-kunci bergantung pada seluruh PK |
| 3NF | Ketergantungan transitif | Non-kunci hanya bergantung pada PK |
| BCNF | Anomali sisa pada candidate key | Setiap determinan adalah candidate key |
BCNF (Boyce-Codd Normal Form) adalah versi 3NF yang lebih ketat, jarang menjadi masalah kecuali tabel Anda punya beberapa candidate key yang saling tumpang tindih. Untuk 95% aplikasi, 3NF adalah target yang sehat.
Denormalisasi yang terukur
Normalisasi mengutamakan integritas; kadang kita mengorbankan sedikit demi performa. Denormalisasi adalah keputusan sadar untuk menyimpan data redundan agar kueri lebih cepat — misalnya menyimpan comment_count di tabel posts agar tidak perlu COUNT setiap kali. Ini sah, tetapi hanya jika:
- Anda punya bukti (profiling) bahwa kueri ternormalisasi memang jadi bottleneck — bukan tebakan.
- Anda punya mekanisme menjaga sinkronisasi: trigger, materialized view, atau pembaruan dalam transaksi yang sama.
- Anda mendokumentasikan bahwa kolom itu turunan, agar tim tahu ia bisa “basi”.
ALTER TABLE posts ADD COLUMN comment_count INT NOT NULL DEFAULT 0;
CREATE FUNCTION bump_comment_count() RETURNS TRIGGER AS $$
BEGIN
IF TG_OP = 'INSERT' THEN
UPDATE posts SET comment_count = comment_count + 1 WHERE id = NEW.post_id;
ELSIF TG_OP = 'DELETE' THEN
UPDATE posts SET comment_count = comment_count - 1 WHERE id = OLD.post_id;
END IF;
RETURN NULL;
END; $$ LANGUAGE plpgsql;
CREATE TRIGGER trg_comment_count
AFTER INSERT OR DELETE ON comments
FOR EACH ROW EXECUTE FUNCTION bump_comment_count();
PostgreSQL Mendalam
PostgreSQL — sering disingkat Postgres — adalah sistem basis data relasional sumber terbuka yang telah dikembangkan selama lebih dari tiga dekade. Ia terkenal karena kepatuhannya pada standar SQL, keandalannya, dan kekayaan fiturnya: dari tipe data JSON dan array hingga full-text search, ekstensi geospasial (PostGIS), dan bahkan penyimpanan vektor untuk AI (pgvector). Bila ragu memilih database, jawaban default yang hampir selalu benar adalah: gunakan Postgres.
“PostgreSQL adalah database relasional paling canggih di dunia yang bersifat open-source. Ia telah membuktikan arsitektur yang teruji, keandalan, integritas data, dan kebenaran yang kuat.”
— The PostgreSQL Global Development Group, dokumentasi resmi
Tipe data yang tepat
Memilih tipe kolom yang benar adalah keputusan integritas, bukan sekadar teknis. Tipe yang tepat menolak data buruk di gerbang masuk. Panduan singkat untuk kasus umum:
| Kebutuhan | Tipe Postgres | Catatan |
|---|---|---|
| Kunci integer | BIGINT + IDENTITY | Pakai BIGINT, bukan INT; hemat penyesalan saat tumbuh |
| Kunci acak/publik | UUID | Aman dibagikan, tidak bocorkan jumlah baris |
| Uang | NUMERIC(12,2) | Jangan pernah pakai FLOAT/REAL untuk uang |
| Teks | TEXT | Postgres: TEXT = VARCHAR tanpa batas, sama cepat |
| Waktu | TIMESTAMPTZ | Selalu simpan zona waktu; hindari TIMESTAMP polos |
| Benar/salah | BOOLEAN | Bukan INT 0/1 |
| Pilihan tetap | ENUM atau tabel referensi | Lihat bawah |
| Data semi-terstruktur | JSONB | Terindeks, bisa dikueri; bukan alasan malas normalisasi |
0.1 + 0.2 tidak sama dengan 0.3 dalam aritmetika titik-mengambang. Untuk uang, satu sen yang hilang bisa berarti audit gagal. Gunakan NUMERIC (desimal presisi-arbitrer), atau simpan dalam satuan terkecil (rupiah/sen) sebagai BIGINT.Constraint: aturan yang ditegakkan database
Constraint memindahkan aturan bisnis dari kode aplikasi (yang bisa lupa dijalankan) ke database (yang tidak pernah lupa). Inilah pertahanan terakhir integritas Anda.
NOT NULL— kolom wajib terisi.UNIQUE— tidak ada duplikat (bisa multi-kolom).CHECK— ekspresi boolean yang harus benar (mis.price >= 0).FOREIGN KEY— integritas referensial dengan aksiON DELETE/ON UPDATE.PRIMARY KEY— gabunganUNIQUE+NOT NULL.EXCLUSION— canggih: cegah tumpang tindih rentang (mis. jadwal ruang).
CREATE TABLE products (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
sku TEXT NOT NULL UNIQUE,
title TEXT NOT NULL CHECK (length(title) BETWEEN 1 AND 200),
price NUMERIC(12,2) NOT NULL CHECK (price >= 0),
stock INT NOT NULL DEFAULT 0 CHECK (stock >= 0),
currency CHAR(3) NOT NULL DEFAULT 'IDR',
UNIQUE (sku, currency) -- constraint multi-kolom
);
Enum: pilihan tetap
Untuk kolom dengan sekumpulan nilai terbatas (status pesanan, peran pengguna), Postgres menawarkan tipe ENUM. Alternatifnya adalah tabel referensi + FK. Keduanya sah:
| Pendekatan | Kelebihan | Kekurangan |
|---|---|---|
ENUM native | Ringkas, cepat, hemat ruang | Menambah nilai butuh ALTER TYPE; sulit dihapus |
| Tabel referensi + FK | Fleksibel, bisa punya metadata (label, urutan) | Butuh JOIN |
TEXT + CHECK IN | Paling sederhana diubah | Tanpa jaminan pusat, mudah salah ketik |
CREATE TYPE order_status AS ENUM ('pending', 'paid', 'shipped', 'cancelled');
ALTER TABLE orders ALTER COLUMN status TYPE order_status USING status::order_status;
-- menambah nilai baru (Postgres 10+), aman tanpa lock berat:
ALTER TYPE order_status ADD VALUE 'refunded' AFTER 'cancelled';
JSON dan JSONB: fleksibilitas terukur
Postgres bisa menyimpan dokumen JSON di dalam kolom. Gunakan JSONB (biner, terindeks) hampir selalu ketimbang JSON (teks mentah). Ini ideal untuk data yang benar-benar tak berstruktur atau bervariasi: preferensi pengguna, metadata event, payload webhook. Tapi hati-hati — JSONB bukan izin untuk menghindari pemodelan. Jika Anda sering mengueri atau mem-JOIN sebuah field JSON, ia seharusnya jadi kolom sungguhan.
ALTER TABLE users ADD COLUMN prefs JSONB NOT NULL DEFAULT '{}';
-- operator: -> ambil objek, ->> ambil teks, @> berisi
SELECT id, prefs ->> 'theme' AS theme
FROM users
WHERE prefs @> '{"newsletter": true}';
-- indeks GIN mempercepat pencarian di dalam JSONB
CREATE INDEX idx_users_prefs ON users USING GIN (prefs);
WHERE sering, (b) JOIN, (c) beri constraint, atau (d) agregasi. Simpan di JSONB apa yang benar-benar bebas bentuk dan hanya dibaca utuh.Neon: Serverless Postgres
Neon adalah PostgreSQL yang dikemas ulang untuk era serverless. Ia menawarkan Postgres asli — bukan tiruan — tetapi dengan arsitektur yang memisahkan komputasi dari penyimpanan. Artinya database bisa “tidur” saat tak terpakai (scale-to-zero) sehingga Anda tidak membayar server menganggur, lalu bangun dalam ratusan milidetik saat ada permintaan. Fitur andalannya yang mengubah cara kerja tim adalah branching ala Git.
Mengapa arsitektur pisah komputasi-penyimpanan penting
Pada Postgres tradisional, komputasi (CPU yang menjalankan kueri) dan penyimpanan (disk berisi data) terikat pada satu mesin. Neon memisahkannya: penyimpanan hidup di lapisan tersendiri yang tahan lama dan berbagi-salin, sementara komputasi adalah proses yang bisa dimatikan, dihidupkan, atau digandakan sesuka hati. Konsekuensinya:
- Scale-to-zero — database dev/preview yang jarang dipakai tak membebani tagihan.
- Autoscaling — komputasi naik-turun mengikuti beban.
- Branching instan — menyalin seluruh database secara logis nyaris tanpa biaya, karena hanya menyalin metadata (copy-on-write).
Branching: database seperti kode
Inilah ide yang paling mengubah alur kerja. Sama seperti Anda membuat branch Git untuk mengembangkan fitur tanpa mengganggu main, Neon membiarkan Anda membuat branch database — salinan penuh data produksi pada satu titik waktu — dalam hitungan detik. Skenario nyata:
| Skenario | Manfaat branching |
|---|---|
| Preview deployment (Vercel PR) | Setiap PR dapat database sendiri, data realistis, terisolasi |
| Menguji migrasi berisiko | Jalankan di branch; jika rusak, buang branch, produksi aman |
| Debugging bug produksi | Branch dari titik waktu tertentu untuk mereproduksi kondisi |
| Analitik berat | Branch read-only agar kueri berat tak mengganggu produksi |
# buat branch dari produksi untuk sebuah fitur
neonctl branches create --name feat/checkout-redesign
# ambil connection string branch tersebut
neonctl connection-string feat/checkout-redesign
# jalankan migrasi & uji di branch, lalu hapus bila selesai
neonctl branches delete feat/checkout-redesign
Menyambung dari aplikasi
Neon memberi Anda connection string Postgres standar, jadi alat apa pun yang bicara Postgres akan bekerja. Untuk lingkungan serverless (edge functions), Neon menyediakan driver berbasis HTTP/WebSocket yang menghindari batasan koneksi TCP.
DATABASE_URL="postgresql://user:pass@ep-cool-name-123.ap-southeast-1.aws.neon.tech/neondb?sslmode=require"
# untuk migrasi Prisma, sering dipakai koneksi non-pooled terpisah:
DIRECT_URL="postgresql://user:pass@ep-cool-name-123.ap-southeast-1.aws.neon.tech/neondb?sslmode=require"
DATABASE_URL (pooled) dan directUrl (direct) persis untuk ini.sslmode=require wajib, (2) rahasia masuk .env yang di-.gitignore, (3) migrasi lewat directUrl, bukan pooled. Alternatif serupa: Supabase, Railway, dan Postgres terkelola lain memakai pola yang sama.Prisma: ORM Modern
Prisma adalah lapisan akses data (ORM) untuk Node.js dan TypeScript yang menjadikan sebuah berkas schema.prisma sebagai satu sumber kebenaran untuk seluruh model data Anda. Dari skema itu, Prisma menghasilkan klien yang sepenuhnya aman-tipe: editor Anda tahu setiap tabel, kolom, dan relasi, dan menolak kueri yang salah bahkan sebelum dijalankan. Ini menutup celah antara ERD di kepala Anda dan kode yang benar-benar berjalan.
Tiga bagian Prisma
- Prisma Schema — berkas deklaratif yang mendefinisikan model, field, dan relasi.
- Prisma Migrate — alat yang membandingkan skema dengan database dan menghasilkan migrasi SQL.
- Prisma Client — pustaka aman-tipe yang di-generate untuk mengueri database.
Anatomi schema.prisma
Perhatikan bagaimana ERD e-commerce kita dari Bab 3 diterjemahkan hampir satu-lawan-satu menjadi model Prisma. Relasi ditulis dua arah dan sangat mudah dibaca:
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL") // pooled (aplikasi)
directUrl = env("DIRECT_URL") // direct (migrasi)
}
model User {
id BigInt @id @default(autoincrement())
email String @unique
name String?
createdAt DateTime @default(now()) @map("created_at")
orders Order[] // sisi "one" dari 1:N
@@map("users")
}
model Order {
id BigInt @id @default(autoincrement())
user User @relation(fields: [userId], references: [id])
userId BigInt @map("user_id")
total Decimal @db.Decimal(12, 2) @default(0)
status OrderStatus @default(pending)
items OrderItem[]
@@index([userId])
@@map("orders")
}
model Product {
id BigInt @id @default(autoincrement())
sku String @unique
price Decimal @db.Decimal(12, 2)
stock Int @default(0)
items OrderItem[]
@@map("products")
}
// tabel penghubung N:M (Order <-> Product) dengan atribut tambahan
model OrderItem {
order Order @relation(fields: [orderId], references: [id], onDelete: Cascade)
orderId BigInt @map("order_id")
product Product @relation(fields: [productId], references: [id])
productId BigInt @map("product_id")
quantity Int
unitPrice Decimal @db.Decimal(12, 2) @map("unit_price")
@@id([orderId, productId]) // composite PK
@@map("order_items")
}
enum OrderStatus {
pending
paid
shipped
cancelled
}
Migrasi: dari skema ke database
Setelah menulis skema, Prisma Migrate menerjemahkan perubahan menjadi berkas SQL bernomor yang tersimpan di repo — sehingga riwayat evolusi skema Anda ter-versikan seperti kode.
# saat pengembangan: buat & terapkan migrasi, regen client
npx prisma migrate dev --name init
# di produksi/CI: hanya terapkan migrasi yang sudah ada
npx prisma migrate deploy
# regenerasi Prisma Client setelah ubah skema
npx prisma generate
# jelajahi data lewat GUI
npx prisma studio
Prisma Client: kueri aman-tipe
Inilah bagian yang membuat pengembang jatuh cinta. Setiap kueri diketik penuh; salah nama kolom = error kompilasi, bukan bug produksi.
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
// CREATE dengan relasi bersarang (nested write)
const order = await prisma.order.create({
data: {
user: { connect: { id: 1n } },
status: 'pending',
items: { create: [{ productId: 7n, quantity: 2, unitPrice: '99.90' }] },
},
include: { items: true },
});
// READ dengan filter, relasi, paginasi, urutan
const recent = await prisma.order.findMany({
where: { status: 'paid', total: { gte: '100' } },
include: { user: true, items: { include: { product: true } } },
orderBy: { createdAt: 'desc' },
take: 20,
});
// UPDATE & DELETE
await prisma.product.update({ where: { id: 7n }, data: { stock: { decrement: 1 } } });
await prisma.order.delete({ where: { id: 1n } });
Transaksi di Prisma
Untuk operasi yang harus “semua atau tidak sama sekali”, bungkus dalam transaksi. Prisma menawarkan API array (batch) dan API interaktif (callback):
await prisma.$transaction(async (tx) => {
const p = await tx.product.findUniqueOrThrow({ where: { id: 7n } });
if (p.stock < 1) throw new Error('stok habis');
await tx.product.update({ where: { id: 7n }, data: { stock: { decrement: 1 } } });
await tx.order.create({ data: { userId: 1n, status: 'paid' } });
}); // jika salah satu gagal, seluruhnya di-rollback
prisma.$queryRaw (tetap ter-parameterisasi, aman dari injection) atau tulis view di SQL. ORM dan SQL bukan musuh; mereka rekan kerja.“Abstraksi yang baik bukan yang menyembunyikan database, melainkan yang membuat hal yang benar menjadi mudah dan hal yang berbahaya menjadi sulit.”
— prinsip desain yang sering dikutip komunitas Prisma
Indexing & Performa
Sebuah indeks adalah struktur data terpisah yang memungkinkan database menemukan baris tanpa memindai seluruh tabel. Bayangkan indeks di belakang buku: alih-alih membaca setiap halaman untuk menemukan sebuah istilah, Anda melompat langsung ke nomor halaman yang tepat. Tanpa indeks, kueri pada tabel besar melakukan sequential scan — membaca setiap baris. Dengan indeks yang tepat, ia melakukan lompatan logaritmik. Selisihnya bisa antara 5 milidetik dan 5 detik.
B-tree: indeks default
Sebagian besar indeks Postgres adalah B-tree (balanced tree), struktur terurut yang efisien untuk pencarian kesetaraan (=), rentang (<, >, BETWEEN), dan pengurutan (ORDER BY). Postgres membuat B-tree otomatis untuk setiap PRIMARY KEY dan UNIQUE. Yang perlu Anda tambahkan sendiri adalah indeks untuk kolom yang sering muncul di WHERE, JOIN, dan ORDER BY — terutama foreign key, yang tidak diindeks otomatis oleh Postgres.
orders(user_id) tanpa indeks membuat setiap “ambil pesanan pengguna X” dan setiap penghapusan induk menjadi sequential scan. Hampir selalu, tambahkan indeks pada kolom FK Anda.-- indeks tunggal pada FK (wajib untuk 1:N)
CREATE INDEX idx_orders_user ON orders (user_id);
-- composite index: urutan kolom PENTING (leftmost prefix rule)
CREATE INDEX idx_orders_user_status ON orders (user_id, status, created_at);
-- partial index: hanya baris yang relevan, lebih kecil & cepat
CREATE INDEX idx_orders_open ON orders (created_at)
WHERE status IN ('pending', 'paid');
-- unique index memaksa keunikan sekaligus mempercepat lookup
CREATE UNIQUE INDEX idx_users_email ON users (lower(email));
Composite index dan aturan prefix kiri
Indeks gabungan (user_id, status, created_at) bisa melayani kueri yang memfilter berdasarkan user_id; user_id + status; atau ketiganya — tetapi tidak kueri yang hanya memfilter status saja. Ini leftmost prefix rule: indeks komposit hanya berguna dari kolom paling kiri berturut-turut. Urutkan kolom dari yang paling selektif / paling sering difilter di depan.
Membaca EXPLAIN
Cara pasti mengetahui apakah indeks Anda dipakai: tanyakan langsung ke Postgres lewat EXPLAIN. Tambahkan ANALYZE untuk menjalankan kueri sungguhan dan melihat waktu nyata.
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM orders WHERE user_id = 42 AND status = 'paid';
-- yang Anda cari di output:
-- "Index Scan using idx_orders_user_status" <- BAGUS
-- "Seq Scan on orders" <- WASPADA di tabel besar
-- "rows=..." estimasi vs aktual jauh berbeda <- statistik basi, jalankan ANALYZE
| Node di EXPLAIN | Arti | Sikap |
|---|---|---|
| Seq Scan | Baca seluruh tabel | OK untuk tabel kecil; buruk untuk besar |
| Index Scan | Pakai indeks lalu ambil baris | Umumnya diinginkan |
| Index Only Scan | Jawaban ada di indeks saja | Terbaik — tak menyentuh tabel |
| Bitmap Heap Scan | Gabung banyak indeks | Wajar untuk filter majemuk |
| Nested Loop / Hash Join | Strategi JOIN | Perhatikan jika baris membengkak |
INSERT/UPDATE memperbarui indeks) dan memakan ruang disk. Indeks yang tak pernah dipakai adalah beban murni. Pantau pg_stat_user_indexes untuk menemukan indeks nganggur dan hapus yang mubazir.“Optimasi prematur adalah akar dari segala kejahatan. Namun mengabaikan indeks pada foreign key bukan optimasi prematur — itu kelalaian dasar.”
— parafrase Donald Knuth, dibumbui kearifan DBA
Relasi & JOIN
Kekuatan sejati database relasional muncul saat Anda menggabungkan tabel. Sebuah JOIN menyatukan baris dari dua tabel atau lebih berdasarkan kolom yang berelasi — biasanya pasangan primary key dan foreign key. Menguasai JOIN berarti bisa menjawab pertanyaan bisnis apa pun yang datanya tersebar di banyak tabel.
Empat jenis JOIN utama
| JOIN | Mengembalikan |
|---|---|
| INNER JOIN | Hanya baris yang cocok di kedua tabel |
| LEFT JOIN | Semua baris tabel kiri; NULL bila kanan tak cocok |
| RIGHT JOIN | Semua baris tabel kanan; NULL bila kiri tak cocok |
| FULL OUTER JOIN | Semua baris kedua tabel; NULL di sisi yang tak cocok |
-- INNER: pesanan beserta nama pemesannya (hanya yang punya user valid)
SELECT o.id, u.name, o.total
FROM orders o
JOIN users u ON u.id = o.user_id;
-- LEFT: semua pengguna, dengan jumlah pesanan (0 bila belum pernah)
SELECT u.name, COUNT(o.id) AS order_count
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
GROUP BY u.id
ORDER BY order_count DESC;
-- JOIN berlapis: pesanan -> item -> produk (menembus junction N:M)
SELECT o.id, p.title, oi.quantity, oi.unit_price
FROM orders o
JOIN order_items oi ON oi.order_id = o.id
JOIN products p ON p.id = oi.product_id
WHERE o.status = 'paid';
Agregasi dan GROUP BY
JOIN sering berpasangan dengan fungsi agregat (COUNT, SUM, AVG, MIN, MAX) dan GROUP BY untuk menghasilkan ringkasan. Gunakan HAVING untuk memfilter setelah pengelompokan (berbeda dari WHERE yang memfilter sebelum).
SELECT p.title, SUM(oi.quantity * oi.unit_price) AS revenue
FROM order_items oi
JOIN products p ON p.id = oi.product_id
GROUP BY p.id
HAVING SUM(oi.quantity * oi.unit_price) > 1000000
ORDER BY revenue DESC;
Masalah N+1: pembunuh performa senyap
Jebakan paling umum di lapisan ORM: memuat daftar (1 kueri), lalu mengambil relasi tiap item satu per satu (N kueri). Untuk 100 pesanan, itu 101 kueri — padahal seharusnya cukup 1–2. Di Prisma, cegah dengan include/select (yang menghasilkan JOIN atau kueri batch), bukan loop yang memanggil database di setiap iterasi.
log: ['query']) saat pengembangan untuk memergokinya.Transaksi & Konkurensi
Saat dua orang membeli barang terakhir pada saat bersamaan, siapa yang menang — dan bagaimana memastikan hanya satu yang mendapatkannya? Ini pertanyaan konkurensi, dan jawaban database relasional adalah transaksi: sekumpulan operasi yang diperlakukan sebagai satu unit tak terpisahkan. Transaksi adalah alasan utama mengapa sistem finansial, tiket, dan inventaris memercayakan datanya pada database relasional.
ACID: empat jaminan
| Properti | Jaminan |
|---|---|
| Atomicity | Semua operasi berhasil, atau tidak satu pun (all-or-nothing). Gagal di tengah? ROLLBACK total. |
| Consistency | Transaksi membawa database dari satu keadaan valid ke keadaan valid lain; semua constraint tetap terpenuhi. |
| Isolation | Transaksi yang berjalan bersamaan tidak saling mengganggu (seolah berurutan). |
| Durability | Setelah COMMIT, data bertahan meski server mati mendadak. |
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
-- jika koneksi putus di sini, Postgres otomatis ROLLBACK; tak ada uang hilang
COMMIT;
Isolation level: menyeimbangkan aman vs cepat
Isolasi total (transaksi benar-benar berurutan) akan aman tetapi lambat. Standar SQL mendefinisikan empat tingkat, masing-masing membolehkan anomali tertentu demi konkurensi lebih tinggi. Postgres default-nya Read Committed.
| Level | Dirty read | Non-repeatable read | Phantom read |
|---|---|---|---|
| Read Uncommitted | mungkin* | mungkin | mungkin |
| Read Committed (default PG) | tidak | mungkin | mungkin |
| Repeatable Read | tidak | tidak | tidak (di PG) |
| Serializable | tidak | tidak | tidak |
*Di Postgres, Read Uncommitted berperilaku seperti Read Committed — dirty read tidak pernah terjadi.
- Dirty read — membaca data yang belum di-commit transaksi lain.
- Non-repeatable read — membaca baris yang sama dua kali, hasil berbeda karena transaksi lain meng-commit di antaranya.
- Phantom read — kueri rentang dijalankan dua kali, muncul baris baru.
Locking dan mencegah lost update
Skenario “barang terakhir”: dua transaksi membaca stock = 1, keduanya mengurangi, hasilnya terjual dua kali. Dua strategi pencegahan:
BEGIN;
-- kunci baris; transaksi lain menunggu sampai kita COMMIT
SELECT stock FROM products WHERE id = 7 FOR UPDATE;
-- aplikasi cek stock >= 1, lalu:
UPDATE products SET stock = stock - 1 WHERE id = 7;
COMMIT;
Optimistic locking adalah alternatif tanpa kunci: tambahkan kolom version, dan saat update sertakan WHERE version = <yang dibaca>. Jika 0 baris terpengaruh, berarti orang lain menang — ulangi. Cocok untuk konflik jarang; hemat karena tak menahan kunci.
stock = stock - 1 dengan CHECK (stock >= 0)), FOR UPDATE, atau versioning. Serahkan penegakan pada database.“Konkurensi adalah salah satu topik tersulit dalam ilmu komputer. Untungnya, database relasional telah menyelesaikan sebagian besarnya untuk Anda — asalkan Anda memakai transaksi dengan benar.”
— parafrase dari Martin Kleppmann, Designing Data-Intensive Applications
Skema Multi-Tenant
Aplikasi SaaS melayani banyak pelanggan (penyewa/tenant) dari satu basis kode. Pertanyaan arsitektur pertama: bagaimana memisahkan data antar-tenant sehingga perusahaan A tidak pernah bisa melihat data perusahaan B? Ada tiga pola utama, masing-masing dengan trade-off isolasi vs biaya operasional.
| Pola | Isolasi | Biaya & kompleksitas |
|---|---|---|
| Shared DB, shared schema (kolom tenant_id) | Logis (lewat kueri) | Termurah, paling mudah skala; risiko kebocoran bila lupa filter |
| Shared DB, schema per tenant | Menengah | Isolasi lebih baik; migrasi jadi berlipat |
| Database per tenant | Terkuat | Termahal; ideal untuk enterprise/regulasi ketat |
Pola paling umum: kolom tenant_id
Mayoritas SaaS mulai dari sini: setiap tabel bertenant membawa kolom tenant_id, dan setiap kueri wajib memfilternya. Sederhana dan skalabel — asalkan filter tidak pernah lupa. Di situlah letak bahayanya.
CREATE TABLE projects (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
tenant_id BIGINT NOT NULL REFERENCES tenants(id),
name TEXT NOT NULL
);
CREATE INDEX idx_projects_tenant ON projects (tenant_id);
-- Row-Level Security: database menegakkan isolasi, bukan cuma aplikasi
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON projects
USING (tenant_id = current_setting('app.tenant_id')::BIGINT);
-- aplikasi menyetel konteks di awal tiap koneksi/transaksi:
-- SET app.tenant_id = '42';
WHERE tenant_id = ? yang terlupa, membocorkan data antar-pelanggan. Pertahanan berlapis: (1) Row-Level Security di database sebagai jaring pengaman, (2) helper kueri terpusat yang selalu menyuntikkan tenant, (3) pengujian yang secara eksplisit mencoba mengakses lintas-tenant dan memastikan gagal.tenant_id. Selalu sarankan RLS sebagai jaring pengaman dan ingatkan bahwa setiap indeks komposit sebaiknya diawali tenant_id (mis. (tenant_id, created_at)) agar kueri per-tenant efisien.Caching: Redis & Upstash
Kueri database, betapapun cepat, tetap ada biayanya. Ketika data yang sama diminta ribuan kali per detik dan jarang berubah, mengulang kueri itu adalah pemborosan. Caching menyimpan hasil di lapisan super-cepat (biasanya memori) agar permintaan berikutnya dilayani tanpa menyentuh database. Redis adalah penyimpanan key-value in-memory paling populer untuk ini; Upstash menyediakannya secara serverless dengan penagihan per-permintaan.
Kapan caching layak — dan kapan tidak
| Cocok di-cache | Hati-hati / hindari |
|---|---|
| Data sering dibaca, jarang berubah (katalog, konfigurasi) | Data yang harus selalu mutakhir (saldo, stok real-time) |
| Hasil komputasi mahal (agregasi, laporan) | Data unik per permintaan (hit-rate rendah) |
| Session, rate-limit counter, antrian | Saat konsistensi kuat lebih penting dari kecepatan |
import { Redis } from '@upstash/redis';
const redis = Redis.fromEnv();
async function getProduct(id: string) {
const key = `product:${id}`;
const cached = await redis.get(key); // 1) coba cache
if (cached) return cached;
const product = await prisma.product.findUnique({ where: { id: BigInt(id) } });
await redis.set(key, product, { ex: 300 }); // 2) simpan, TTL 5 menit
return product;
}
Tantangan terbesar caching bukan menyimpan, melainkan invalidasi — kapan menghapus data basi. Strategi umum: TTL (kedaluwarsa otomatis, sederhana tetapi bisa basi sesaat) dan write-through/eviction (hapus/perbarui cache saat data berubah). Ada lelucon terkenal:
“Hanya ada dua hal yang sulit dalam ilmu komputer: invalidasi cache dan penamaan.”
— Phil Karlton (sering dikutip)
NoSQL: Kapan Dipakai
Buku ini memihak Postgres, dan itu bukan kebetulan: untuk mayoritas aplikasi, database relasional adalah pilihan default yang benar. Tetapi “default” bukan “selalu”. NoSQL — payung untuk database non-relasional — memecahkan kelas masalah tertentu dengan lebih baik. Insinyur yang matang tahu kapan meninggalkan zona nyaman relasionalnya, dan yang lebih penting, kapan tidak.
Empat keluarga NoSQL
| Tipe | Contoh | Ideal untuk |
|---|---|---|
| Document | MongoDB, Firestore | Dokumen bersarang, skema berevolusi cepat, prototipe |
| Key-Value | Redis, DynamoDB | Cache, session, lookup ultra-cepat |
| Wide-Column | Cassandra, ScyllaDB | Tulis masif, time-series, skala horizontal ekstrem |
| Graph | Neo4j | Relasi banyak-lompatan (jejaring sosial, rekomendasi) |
Kerangka keputusan yang jujur
Pertanyaan yang benar bukan “SQL atau NoSQL?” melainkan “seperti apa bentuk dan pola akses data saya?” Pertimbangkan NoSQL saat:
- Data Anda benar-benar tak berstruktur atau strukturnya sangat berubah-ubah, dan Anda hampir selalu mengambil satu dokumen utuh berdasarkan kunci.
- Anda butuh skala tulis horizontal yang ekstrem melampaui satu node (jutaan tulis/detik, IoT/telemetri).
- Kueri utama Anda adalah traversal graf multi-lompatan yang menyakitkan di SQL (“teman dari teman dari teman”).
- Anda butuh key-value latensi mikrodetik untuk cache/session (justru pelengkap Postgres, bukan pengganti).
Tetaplah relasional saat data Anda punya relasi jelas, Anda butuh transaksi ACID lintas entitas, atau Anda akan menanyakan data dengan cara yang belum terbayangkan hari ini (kekuatan SQL ad-hoc).
JSONB dengan indeks GIN), berfungsi sebagai key-value, melakukan full-text search, menyimpan vektor untuk AI (pgvector), dan mengurus data geospasial (PostGIS). Sering kali Anda tidak butuh database kedua — Postgres sudah cukup jauh.“Pilih teknologi yang membosankan. Inovasi punya anggaran terbatas; belanjakan pada masalah bisnis unik Anda, bukan pada memilih ulang database.”
— Dan McKinley, “Choose Boring Technology” (2015)
Pola Pemodelan Produksi
Ada sekumpulan pola yang muncul di hampir setiap aplikasi serius. Menguasainya berarti tidak menemukan ulang roda — dan tidak mengulangi kesalahan yang sudah dipetakan orang lain. Bab ini adalah kotak peralatan Anda: audit, soft delete, versioning, dan event log, ditutup dengan skema e-commerce/SaaS yang lengkap.
Pola 1: Kolom audit (timestamps)
Setiap tabel sebaiknya tahu kapan barisnya dibuat dan terakhir diubah. Sederhana, murah, dan tak ternilai saat debugging.
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
-- trigger agar updated_at selalu segar
CREATE FUNCTION touch_updated_at() RETURNS TRIGGER AS $$
BEGIN NEW.updated_at = now(); RETURN NEW; END; $$ LANGUAGE plpgsql;
CREATE TRIGGER trg_touch BEFORE UPDATE ON orders
FOR EACH ROW EXECUTE FUNCTION touch_updated_at();
Pola 2: Soft delete
Alih-alih menghapus baris (dan kehilangan jejak selamanya), tandai sebagai terhapus dengan deleted_at. Data tetap ada untuk audit/pemulihan, tetapi kueri normal menyembunyikannya. Perhatikan: ini menambah kewajiban selalu memfilter deleted_at IS NULL.
deleted_at TIMESTAMPTZ -- NULL = aktif; berisi waktu = terhapus
-- keunikan email hanya berlaku untuk baris yang belum dihapus
CREATE UNIQUE INDEX uniq_active_email ON users (email)
WHERE deleted_at IS NULL;
-- kueri normal:
SELECT * FROM users WHERE deleted_at IS NULL;
Pola 3: Versioning / riwayat
Saat Anda perlu tahu “seperti apa data ini bulan lalu”, simpan riwayat. Pola umum: tabel utama berisi versi terkini, plus tabel _history yang menampung salinan setiap perubahan.
CREATE TABLE document_versions (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
document_id BIGINT NOT NULL REFERENCES documents(id),
version INT NOT NULL,
content TEXT NOT NULL,
edited_by BIGINT REFERENCES users(id),
edited_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (document_id, version)
);
Pola 4: Event log (append-only)
Alih-alih hanya menyimpan keadaan terkini, catat setiap perubahan sebagai event yang tak pernah diubah (append-only). Ini fondasi event sourcing dan audit trail: keadaan saat ini adalah hasil pemutaran ulang semua event. Berguna untuk finansial, kepatuhan, dan analitik perilaku.
CREATE TABLE events (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
aggregate TEXT NOT NULL, -- 'order', 'user', ...
aggregate_id BIGINT NOT NULL,
event_type TEXT NOT NULL, -- 'OrderPaid', 'ItemAdded'
payload JSONB NOT NULL,
occurred_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_events_agg ON events (aggregate, aggregate_id, occurred_at);
Studi kasus: skema SaaS e-commerce lengkap
Sekarang kita satukan semuanya menjadi satu skema yang bisa dijadikan titik awal proyek nyata: multi-tenant, dengan audit, soft delete, dan relasi lengkap. Perhatikan bagaimana setiap pola dari bab ini muncul.
model Tenant {
id BigInt @id @default(autoincrement())
name String
plan Plan @default(free)
users User[]
products Product[]
orders Order[]
createdAt DateTime @default(now()) @map("created_at")
@@map("tenants")
}
model User {
id BigInt @id @default(autoincrement())
tenantId BigInt @map("tenant_id")
tenant Tenant @relation(fields: [tenantId], references: [id])
email String
role Role @default(member)
orders Order[]
deletedAt DateTime? @map("deleted_at") // soft delete
createdAt DateTime @default(now()) @map("created_at")
@@unique([tenantId, email]) // email unik PER tenant
@@index([tenantId])
@@map("users")
}
model Product {
id BigInt @id @default(autoincrement())
tenantId BigInt @map("tenant_id")
tenant Tenant @relation(fields: [tenantId], references: [id])
sku String
title String
price Decimal @db.Decimal(12, 2)
stock Int @default(0)
items OrderItem[]
@@unique([tenantId, sku])
@@map("products")
}
model Order {
id BigInt @id @default(autoincrement())
tenantId BigInt @map("tenant_id")
tenant Tenant @relation(fields: [tenantId], references: [id])
userId BigInt @map("user_id")
user User @relation(fields: [userId], references: [id])
status OrderStatus @default(pending)
total Decimal @db.Decimal(12, 2) @default(0)
items OrderItem[]
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@index([tenantId, createdAt]) // tenant-first index
@@map("orders")
}
model OrderItem {
orderId BigInt @map("order_id")
order Order @relation(fields: [orderId], references: [id], onDelete: Cascade)
productId BigInt @map("product_id")
product Product @relation(fields: [productId], references: [id])
quantity Int
unitPrice Decimal @db.Decimal(12, 2) @map("unit_price")
@@id([orderId, productId])
@@map("order_items")
}
enum Plan { free pro enterprise }
enum Role { owner admin member }
enum OrderStatus { pending paid shipped cancelled refunded }
Untuk melihat skema ini sebagai diagram, salin JSON di bawah dan tempel ke ERD Builder lalu klik Render ERD:
{ "entities": [
{ "name": "tenants", "fields": [ {"name":"id","type":"BIGINT","pk":true}, {"name":"name","type":"TEXT"}, {"name":"plan","type":"ENUM"} ] },
{ "name": "users", "fields": [ {"name":"id","type":"BIGINT","pk":true}, {"name":"tenant_id","type":"BIGINT","fk":true}, {"name":"email","type":"TEXT"}, {"name":"role","type":"ENUM"} ] },
{ "name": "products", "fields": [ {"name":"id","type":"BIGINT","pk":true}, {"name":"tenant_id","type":"BIGINT","fk":true}, {"name":"sku","type":"TEXT"}, {"name":"price","type":"NUMERIC"} ] },
{ "name": "orders", "fields": [ {"name":"id","type":"BIGINT","pk":true}, {"name":"tenant_id","type":"BIGINT","fk":true}, {"name":"user_id","type":"BIGINT","fk":true}, {"name":"status","type":"ENUM"} ] },
{ "name": "order_items", "fields": [ {"name":"order_id","type":"BIGINT","pk":true,"fk":true}, {"name":"product_id","type":"BIGINT","pk":true,"fk":true}, {"name":"quantity","type":"INT"} ] }
], "relations": [
["tenants","users","1-N"], ["tenants","products","1-N"], ["tenants","orders","1-N"],
["users","orders","1-N"], ["orders","order_items","1-N"], ["products","order_items","1-N"]
] }
Cara Dibaca AI
Buku ini ditulis pada era ketika sebagian besar skema database pertama kali dirancang bukan hanya oleh manusia, tetapi bersama asisten AI. Bab ini punya dua sisi: bagaimana membuat skema Anda mudah dipahami AI, dan bagaimana meminta AI menghasilkan skema yang benar. Keduanya bermuara pada satu hal — kejelasan yang eksplisit.
Mengapa AI dan database saling cocok
Model bahasa unggul pada tugas terstruktur dan berpola, dan skema database adalah keduanya. Sebuah LLM dapat: menerjemahkan deskripsi bisnis menjadi ERD, menghasilkan DDL SQL atau skema Prisma, menyarankan indeks, menulis kueri kompleks, dan meninjau skema untuk pelanggaran normalisasi. Tetapi AI juga bisa keliru dengan percaya diri — menyarankan FLOAT untuk uang, melupakan indeks FK, atau membuat relasi N:M tanpa junction table. Anda tetap pilot; AI adalah kopilot.
Membuat konten mudah dibaca AI
- Penamaan konsisten —
snake_caseuntuk kolom, jamak untuk tabel (users),<entitas>_iduntuk FK. Pola yang dapat diprediksi membantu AI (dan manusia) menyimpulkan relasi. - Skema deklaratif sebagai sumber kebenaran — satu berkas
schema.prismaatau DDL SQL memberi AI konteks lengkap dalam satu tempat, jauh lebih baik daripada memburu definisi tersebar. - Komentar yang menjelaskan “mengapa” — catat aturan bisnis yang tidak terlihat dari struktur (“status hanya boleh maju, tak bisa mundur”).
- Format terstruktur — tabel, daftar, dan blok kode berlabel lebih mudah diurai daripada prosa panjang. Itu sebabnya buku ini penuh dengannya.
- Contoh konkret — satu baris data contoh sering menjelaskan lebih dari satu paragraf deskripsi.
Meminta AI menghasilkan skema yang baik
Kualitas keluaran AI berbanding lurus dengan kejelasan permintaan. Prompt yang buruk: “bikinkan database toko”. Prompt yang baik menyatakan entitas, relasi, kardinalitas, aturan, dan target teknologi:
Rancang skema PostgreSQL (via Prisma) untuk marketplace multi-tenant.
Entitas: Tenant, User, Store, Product, Order, OrderItem, Review.
Aturan:
- Setiap Store milik satu Tenant; satu Tenant punya banyak Store (1:N).
- Order berisi banyak Product lewat OrderItem (N:M dengan quantity & harga).
- Uang pakai Decimal(12,2), waktu pakai TIMESTAMPTZ.
- Sertakan kolom audit (created_at, updated_at) dan soft delete di User.
- Beri indeks pada semua foreign key dan kolom yang difilter.
- Tambahkan enum untuk status Order.
Keluarkan: schema.prisma + catatan indeks + potensi masalah normalisasi.
BIGINT/UUID; (2) NUMERIC/Decimal untuk uang — jangan pernah FLOAT; (3) TIMESTAMPTZ untuk waktu; (4) indeks pada setiap FK; (5) junction table untuk setiap N:M; (6) NOT NULL + constraint CHECK untuk aturan bisnis; (7) tenant_id + RLS untuk multi-tenant. Selalu jelaskan trade-off, jangan sekadar memuntahkan kode.Verifikasi keluaran AI
Perlakukan skema hasil AI seperti pull request dari kolega junior yang cepat tapi kadang ceroboh. Checklist tinjauan:
| Periksa | Pertanyaan |
|---|---|
| Tipe uang | Apakah NUMERIC/Decimal, bukan float? |
| Indeks FK | Setiap kolom FK punya indeks? |
| N:M | Ada junction table, bukan array/CSV? |
| Normalisasi | Ada data berulang yang melanggar 3NF? |
| Constraint | Aturan bisnis ditegakkan (NOT NULL, CHECK, UNIQUE)? |
| Isolasi tenant | Multi-tenant punya tenant_id + RLS? |
Alat seperti ERD Builder di Bab 3 sangat cocok di sini: minta AI mengeluarkan skemanya dalam format JSON kita, tempel, dan verifikasi bentuk relasinya secara visual dalam sekejap.
“AI tidak menggantikan pemahaman; ia melipatgandakannya. Insinyur yang memahami fundamental data akan mengarahkan AI dengan tepat; yang tidak, akan memperbanyak kesalahannya dengan kecepatan tinggi.”
— Galih Prasetyo
Merangkai Semuanya
Kita mulai dari sebuah klaim: data hidup lebih lama daripada kode. Setelah lima belas bab, semoga klaim itu terasa bukan sebagai slogan, melainkan sebagai prinsip kerja. Skema yang Anda rancang hari ini akan menampung data selama bertahun-tahun, melintasi berkali-kali penulisan ulang kode di atasnya. Waktu yang Anda investasikan untuk memikirkannya baik-baik adalah investasi dengan bunga majemuk.
Mari rekap busur besar buku ini. Kita mulai dengan berpikir tentang data — menemukan entitas, menggambar relasi lewat ERD, dan menormalkan hingga setiap fakta punya satu rumah. Kita lalu turun ke tanah: PostgreSQL sebagai fondasi kokoh, Neon yang menjadikannya lincah dan bercabang seperti Git, dan Prisma yang menjembatani skema dengan kode aman-tipe. Kita mempertajamnya dengan indeks, mengamankannya dengan transaksi, menskalakannya lewat pola multi-tenant dan caching, dan belajar kapan — secara jujur — keluar dari dunia relasional. Terakhir, kita menaruh semuanya dalam kerangka era AI.
Jika hanya ada tujuh hal yang boleh Anda bawa pulang, biarlah ini:
- Mulai dari entitas dan relasi, bukan tabel. Kata benda jadi entitas, kata kerja jadi relasi.
- Normalkan ke 3NF lebih dulu; denormalkan hanya dengan bukti dan penjaga sinkronisasi.
- Pilih tipe yang tepat:
NUMERICuntuk uang,TIMESTAMPTZuntuk waktu, surrogate key untuk identitas. - Setiap foreign key butuh indeks; setiap N:M butuh junction table.
- Bungkus operasi kritis dalam transaksi; serahkan konkurensi pada database.
- Biarkan database menegakkan aturan lewat constraint — kode bisa lupa, constraint tidak.
- Ukur sebelum mengoptimasi. Indeks dan cache mengikuti bukti, bukan firasat.
Buka kembali ERD Builder kapan saja Anda merancang sesuatu yang baru. Menggambar adalah berpikir. Selamat membangun fondasi yang kokoh.
“Tunjukkan padaku flowchart-mu tetapi sembunyikan tabel-tabelmu, maka aku akan tetap bingung. Tunjukkan tabel-tabelmu, dan aku biasanya tak butuh flowchart-mu; semuanya akan jelas.”
— Fred Brooks, The Mythical Man-Month (1975)
Tanya-Jawab (FAQ)
Haruskah saya selalu memakai UUID sebagai primary key?
Tidak selalu. UUID bagus untuk kunci yang diekspos publik atau di-generate terdistribusi (tanpa koordinasi). Tetapi UUID acak (v4) bisa memperbesar indeks dan memperlambat penyisipan karena tidak berurutan. Untuk banyak kasus, BIGINT identity lebih ramping. Jika butuh UUID yang ramah indeks, pertimbangkan UUIDv7 (berbasis waktu). Kompromi umum: BIGINT internal + kolom UUID publik.
Kapan saya sebaiknya memakai SQL mentah alih-alih Prisma?
Saat kueri Anda melampaui apa yang diekspresikan ORM dengan bersih: agregasi analitik kompleks, window function, CTE rekursif, atau fitur Postgres spesifik. Prisma menyediakan $queryRaw yang tetap ter-parameterisasi. Aturannya: ORM untuk CRUD sehari-hari, SQL untuk kueri istimewa.
Apakah normalisasi memperlambat aplikasi saya?
Jarang menjadi masalah pada skala menengah dengan indeks yang benar. JOIN pada kolom terindeks sangat cepat. Denormalisasi baru relevan saat profiling menunjukkan JOIN spesifik sebagai bottleneck nyata — dan bahkan itu sering bisa diatasi dengan indeks atau materialized view lebih dulu.
Postgres atau MySQL?
Keduanya matang dan andal. Postgres unggul dalam kepatuhan standar, tipe kaya (JSONB, array, range), ekstensibilitas (PostGIS, pgvector), dan constraint canggih. Untuk proyek baru tanpa alasan khusus, Postgres adalah rekomendasi buku ini. MySQL tetap pilihan sah, terutama di ekosistem yang sudah membangunnya.
Bagaimana menangani migrasi skema di produksi tanpa downtime?
Gunakan perubahan aditif dan bertahap (expand-then-contract): tambah kolom/tabel baru dulu (nullable), deploy kode yang menulis ke keduanya, isi-mundur (backfill) data, alihkan pembacaan, baru hapus yang lama. Hindari ALTER yang mengunci tabel besar; Postgres modern membuat banyak operasi non-blocking, tetapi selalu uji di branch Neon terlebih dahulu.
Apakah saya butuh Redis sejak awal?
Hampir pasti tidak. Mulai tanpa cache. Postgres dengan indeks yang baik melayani ribuan permintaan per detik. Tambahkan Redis saat Anda mengukur tekanan baca nyata atau butuh primitif spesifik (rate-limit, session terdistribusi, antrian).
Bagaimana cara menyimpan enum yang mungkin bertambah?
Jika daftar nilainya cukup stabil (status pesanan), ENUM native baik-baik saja — menambah nilai cukup dengan ALTER TYPE ... ADD VALUE. Jika daftarnya sering berubah, butuh metadata (label tampilan, urutan, ikon), atau dikelola pengguna, gunakan tabel referensi + foreign key. Pertimbangkan pula: apakah nilai perlu dilokalkan (multi-bahasa)? Jika ya, tabel referensi hampir selalu menang.
Apakah saya harus memakai cascade delete?
ON DELETE CASCADE praktis (menghapus induk ikut menghapus anak) tetapi berbahaya bila tak sengaja terpicu pada data penting. Gunakan CASCADE untuk relasi kepemilikan sejati (OrderItem milik Order). Gunakan RESTRICT (default aman) untuk relasi yang penghapusannya harus disadari (jangan hapus User yang masih punya Order). SET NULL cocok saat relasi opsional. Putuskan per relasi, jangan seragam.
Kapan partisi tabel (table partitioning) diperlukan?
Umumnya hanya saat sebuah tabel tunggal tumbuh sangat besar (puluhan hingga ratusan juta baris) dan datanya punya sumbu alami untuk dipecah — paling sering waktu (partisi per bulan) atau tenant. Partisi bukan optimasi pertama; indeks dan kueri yang baik lebih dulu. Untuk mayoritas aplikasi, Anda tidak akan menyentuhnya bertahun-tahun.
Glosarium
| Istilah | Definisi |
|---|---|
| ACID | Atomicity, Consistency, Isolation, Durability — empat jaminan transaksi database. |
| Atribut | Properti dari sebuah entitas; menjadi kolom di tabel. |
| B-tree | Struktur indeks terurut default Postgres; efisien untuk kesetaraan, rentang, dan pengurutan. |
| BCNF | Boyce-Codd Normal Form; versi lebih ketat dari 3NF. |
| Cardinality | Jumlah relasi antar entitas: 1:1, 1:N, atau N:M. |
| Composite key | Primary key yang terdiri dari lebih dari satu kolom. |
| Constraint | Aturan yang ditegakkan database: NOT NULL, UNIQUE, CHECK, FOREIGN KEY. |
| DDL / DML / DQL | Sub-bahasa SQL untuk mendefinisikan / memanipulasi / menanyakan data. |
| Denormalisasi | Menyimpan data redundan secara sengaja demi performa baca. |
| Entity (entitas) | Objek/konsep yang datanya disimpan; biasanya menjadi satu tabel. |
| ERD | Entity-Relationship Diagram; peta visual entitas, atribut, dan relasi. |
| Foreign key (FK) | Kolom yang menunjuk ke primary key tabel lain; menciptakan relasi. |
| Index | Struktur bantu yang mempercepat pencarian baris. |
| Isolation level | Tingkat pemisahan antar-transaksi konkuren (Read Committed, Serializable, dll). |
| JSONB | Tipe Postgres untuk menyimpan JSON biner yang bisa diindeks dan dikueri. |
| Junction table | Tabel penghubung untuk merepresentasikan relasi N:M. |
| Migration | Perubahan skema yang ter-versikan dan diterapkan bertahap. |
| Normalisasi | Menata data agar setiap fakta disimpan satu kali; menghapus redundansi. |
| N+1 | Anti-pola: 1 kueri daftar + N kueri relasi per item; sangat lambat. |
| ORM | Object-Relational Mapping; pustaka yang memetakan tabel ke objek kode (mis. Prisma). |
| Primary key (PK) | Kolom yang mengidentifikasi setiap baris secara unik. |
| RLS | Row-Level Security; kebijakan Postgres yang memfilter baris per konteks (mis. tenant). |
| Serverless Postgres | Postgres yang memisahkan komputasi dari penyimpanan (mis. Neon), bisa scale-to-zero. |
| Soft delete | Menandai baris terhapus (deleted_at) alih-alih menghapusnya secara fisik. |
| Surrogate key | Kunci buatan tanpa makna bisnis (BIGINT/UUID) sebagai PK. |
| Transaction | Sekelompok operasi yang diperlakukan sebagai satu unit all-or-nothing. |
| Weak entity | Entitas yang keberadaannya bergantung pada entitas induk (mis. OrderItem). |
Checklist Desain Skema
Cetak halaman ini atau tinjau daftar berikut sebelum menganggap sebuah skema “siap”. Setiap butir mewakili pelajaran dari sebuah bab.
Struktur & relasi
- ☐ Setiap entitas ditemukan dari kata benda domain; tiap tabel mewakili satu jenis hal.
- ☐ Setiap tabel punya primary key (surrogate: BIGINT identity atau UUID).
- ☐ Keunikan bisnis ditegakkan lewat UNIQUE terpisah, bukan hanya PK.
- ☐ Setiap relasi 1:N punya FK di sisi “many”.
- ☐ Setiap relasi N:M punya junction table (bukan array/CSV).
- ☐ Aksi ON DELETE/ON UPDATE dipikirkan (CASCADE, RESTRICT, SET NULL).
Integritas & tipe
- ☐ Uang memakai NUMERIC/Decimal — tidak ada FLOAT untuk nilai finansial.
- ☐ Waktu memakai TIMESTAMPTZ, bukan TIMESTAMP polos.
- ☐ Kolom wajib bertanda NOT NULL; NULL hanya di tempat yang benar-benar opsional.
- ☐ Aturan bisnis ditegakkan constraint CHECK (mis. quantity > 0, price >= 0).
- ☐ Pilihan tetap memakai ENUM atau tabel referensi + FK.
Normalisasi & performa
- ☐ Skema mencapai 3NF; tidak ada data berulang tanpa alasan.
- ☐ Denormalisasi (bila ada) terdokumentasi dan punya penjaga sinkronisasi.
- ☐ Setiap foreign key punya indeks.
- ☐ Kolom yang sering difilter/diurutkan punya indeks (komposit bila perlu).
- ☐ Kueri panas telah diperiksa dengan EXPLAIN ANALYZE.
Skala & operasi
- ☐ Multi-tenant: kolom tenant_id + RLS; indeks komposit diawali tenant_id.
- ☐ Kolom audit created_at/updated_at ada di tabel yang relevan.
- ☐ Operasi kritis (uang, stok) dibungkus transaksi dengan proteksi konkurensi.
- ☐ Migrasi diuji di branch (Neon) sebelum menyentuh produksi.
- ☐ Rahasia koneksi di .env yang di-gitignore; sslmode=require aktif.
Lampiran A — Referensi Cepat SQL
Kartu contek satu halaman untuk operasi yang paling sering Anda butuhkan. Semua contoh memakai sintaks PostgreSQL.
-- MEMBUAT & MENGUBAH
CREATE TABLE t (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name TEXT);
ALTER TABLE t ADD COLUMN email TEXT NOT NULL DEFAULT '';
ALTER TABLE t RENAME COLUMN name TO full_name;
DROP TABLE IF EXISTS t CASCADE;
-- MENULIS
INSERT INTO t (full_name, email) VALUES ('Andi', 'andi@mail.id') RETURNING id;
UPDATE t SET email = 'baru@mail.id' WHERE id = 1;
DELETE FROM t WHERE id = 1;
-- UPSERT (insert atau update bila bentrok)
INSERT INTO t (id, email) VALUES (1, 'x@mail.id')
ON CONFLICT (id) DO UPDATE SET email = EXCLUDED.email;
-- MEMBACA
SELECT col1, col2 FROM t
WHERE email LIKE '%@mail.id' AND id > 10
ORDER BY id DESC LIMIT 20 OFFSET 40;
-- CTE & window function
WITH ranked AS (
SELECT *, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn
FROM orders
)
SELECT * FROM ranked WHERE rn = 1; -- pesanan terbaru tiap user
Lampiran B — Peta Tipe: Postgres ↔ Prisma
| PostgreSQL | Prisma | Pakai untuk |
|---|---|---|
BIGINT | BigInt | Kunci, hitungan besar |
INTEGER | Int | Angka biasa (< 2 miliar) |
NUMERIC(p,s) | Decimal @db.Decimal | Uang, presisi tetap |
TEXT | String | Teks apa pun |
BOOLEAN | Boolean | Benar/salah |
TIMESTAMPTZ | DateTime @db.Timestamptz | Waktu dengan zona |
UUID | String @db.Uuid | Kunci acak/publik |
JSONB | Json | Data semi-terstruktur |
ENUM | enum { ... } | Pilihan tetap |
type[] | Type[] | Array skalar (hemat, non-relasional) |
Daftar Pustaka & Bacaan Lanjutan
- Codd, E. F. (1970). A Relational Model of Data for Large Shared Data Banks. Communications of the ACM, 13(6).
- Chen, P. (1976). The Entity-Relationship Model — Toward a Unified View of Data. ACM TODS, 1(1).
- Kleppmann, M. (2017). Designing Data-Intensive Applications. O'Reilly Media.
- Date, C. J. (2011). SQL and Relational Theory. O'Reilly Media.
- Brooks, F. (1975/1995). The Mythical Man-Month. Addison-Wesley.
- Winand, M. Use The Index, Luke! — panduan indexing daring (use-the-index-luke.com).
- Dokumentasi resmi PostgreSQL — postgresql.org/docs (rujukan utama tipe, constraint, EXPLAIN).
- Dokumentasi Prisma — prisma.io/docs (schema, migrate, client).
- Dokumentasi Neon — neon.tech/docs (branching, pooling, serverless driver).
- McKinley, D. (2015). Choose Boring Technology. Esai daring.
- Karwin, B. (2010). SQL Antipatterns: Avoiding the Pitfalls of Database Programming. Pragmatic Bookshelf.
- Fowler, M. (2003). Patterns of Enterprise Application Architecture. Addison-Wesley.
Kutipan dalam buku ini disajikan sebagai atribusi kepada tokoh dan karya nyata; teks kutipan sebagian diparafrasakan dari gagasan asli untuk keperluan pengajaran dan diterjemahkan ke Bahasa Indonesia. Rujuk sumber asli untuk teks verbatim.
Lampiran C — Latihan Terpandu: Sistem Perpustakaan
Mari terapkan seluruh alur buku pada satu kasus baru, langkah demi langkah. Tujuan: memodelkan sistem peminjaman buku perpustakaan.
Langkah 1 — Temukan entitas (kata benda)
Deskripsi: “Anggota meminjam eksemplar buku. Setiap judul buku punya banyak eksemplar fisik. Satu peminjaman mencatat kapan dipinjam dan jatuh tempo.” Kata benda → entitas: Member, Book (judul), Copy (eksemplar), Loan (peminjaman).
Langkah 2 — Tentukan relasi & kardinalitas
- Book → Copy: satu judul punya banyak eksemplar (1:N).
- Member → Loan: satu anggota punya banyak peminjaman (1:N).
- Copy → Loan: satu eksemplar bisa dipinjam berkali-kali dari waktu ke waktu (1:N), tetapi hanya satu peminjaman aktif pada satu saat.
Perhatikan: relasi Member↔Copy yang tampak N:M sebenarnya diwujudkan melalui entitas Loan — ia bukan sekadar junction pasif, melainkan entitas bermakna dengan atributnya sendiri (tanggal). Ini pola penting: junction table yang “tumbuh” menjadi entitas penuh.
Langkah 3 — Atribut & constraint
Book: isbn (unik), title, author. Copy: barcode (unik), book_id (FK), condition. Loan: copy_id (FK), member_id (FK), borrowed_at, due_at, returned_at (NULL = masih dipinjam).
Langkah 4 — Cegah dua peminjaman aktif
Aturan bisnis: satu eksemplar tak boleh punya dua peminjaman yang belum dikembalikan. Tegakkan di database dengan partial unique index:
CREATE UNIQUE INDEX uniq_active_loan ON loans (copy_id)
WHERE returned_at IS NULL; -- hanya berlaku untuk pinjaman aktif
Langkah 5 — Verifikasi visual
Tempel JSON ini ke ERD Builder untuk melihat modelnya:
{ "entities": [
{ "name": "members", "fields": [ {"name":"id","type":"BIGINT","pk":true}, {"name":"name","type":"TEXT"}, {"name":"email","type":"TEXT"} ] },
{ "name": "books", "fields": [ {"name":"id","type":"BIGINT","pk":true}, {"name":"isbn","type":"TEXT"}, {"name":"title","type":"TEXT"} ] },
{ "name": "copies", "fields": [ {"name":"id","type":"BIGINT","pk":true}, {"name":"book_id","type":"BIGINT","fk":true}, {"name":"barcode","type":"TEXT"} ] },
{ "name": "loans", "fields": [ {"name":"id","type":"BIGINT","pk":true}, {"name":"copy_id","type":"BIGINT","fk":true}, {"name":"member_id","type":"BIGINT","fk":true}, {"name":"due_at","type":"TIMESTAMPTZ"} ] }
], "relations": [
["books","copies","1-N"], ["members","loans","1-N"], ["copies","loans","1-N"]
] }
Kolofon & Tentang Edisi Ini
Buku ini adalah dokumen HTML mandiri (self-contained): seluruh gaya, ilustrasi SVG, dan ERD Builder interaktif tertanam dalam satu berkas, tanpa ketergantungan jaringan kecuali font Google dan favicon. Ia dirancang agar dapat dibaca nyaman di layar, dicetak ke PDF (gunakan tombol Cetak), sekaligus mudah diurai oleh AI/LLM — struktur judul berjenjang, tabel, dan blok kode berlabel dipilih justru untuk keterbacaan mesin maupun manusia.
| Aspek | Keterangan |
|---|---|
| Judul | Database & Pemodelan Data (ERD) — Fondasi Setiap Aplikasi |
| Penulis | Galih Prasetyo |
| Tahun | 2026 |
| Bahasa | Indonesia (id) |
| Cakupan | Relasional, ERD, normalisasi, PostgreSQL, Neon, Prisma, indexing, transaksi, multi-tenant, caching, NoSQL, pola pemodelan |
| Tema desain | Data blueprint — biru, teal, slate; font Space Grotesk, Inter, JetBrains Mono |
| Fitur interaktif | ERD Builder (JSON → diagram SVG), vanilla JS |
| Edisi | Pertama (v1.0) |
Semua contoh kode ditujukan sebagai titik awal yang benar secara konseptual; sesuaikan dengan versi PostgreSQL, Prisma, dan penyedia hosting Anda. Umpan balik dan koreksi menjadikan edisi berikutnya lebih baik.
