Sampul buku
FONDASI · REKAYASA DATA · v1.0

Database & Pemodelan Data (ERD)

Fondasi Setiap Aplikasi — dari relasi, ERD, dan normalisasi hingga PostgreSQL, Neon, Prisma, indexing, transaksi, dan pola pemodelan produksi.
Seri Fondasi Rekayasa · Blueprint Data · Edisi Pertama

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.


Prakata

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

Panduan

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 databaseBab 1 → 2 → 3, lalu main-main dengan ERD Builder
Bisa SQL dasar, ingin mendalami desainBab 3 (ERD) → 4 (Normalisasi) → 14 (Pola)
Sedang memilih tumpukan teknologiBab 5 (Postgres) → 6 (Neon) → 7 (Prisma)
Aplikasi lambat / ingin optimasiBab 8 (Indexing) → 9 (JOIN) → 12 (Caching)
Membangun SaaS multi-penyewaBab 10 (Transaksi) → 11 (Multi-Tenant)
Menggunakan AI untuk menulis skemaBab 15 (Cara Dibaca AI)

Konvensi

CatatanAnda tidak perlu memasang apa pun untuk mengikuti sebagian besar buku ini. Namun jika ingin mempraktikkan langsung, siapkan akun gratis Neon dan Node.js 18+. Instruksi lengkap ada di Bab 6 dan 7.
Bab Satu

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:

TingkatMenjawab pertanyaanArtefak
KonseptualApa entitas bisnis dan bagaimana mereka berelasi?ERD tingkat tinggi (Pengguna, Pesanan, Produk)
LogisBagaimana strukturnya sebagai tabel, kolom, kunci?Skema ternormalisasi, tipe atribut
FisikBagaimana 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.

Jebakan #1Jangan pernah memulai dengan tabel. Mulailah dengan kata benda dalam deskripsi masalah Anda. Kata benda cenderung menjadi entitas; kata kerja cenderung menjadi relasi. “Pengguna membuat pesanan” → dua entitas, satu relasi.

Peta perjalanan buku ini

Berikut alur besar yang akan kita tempuh, dari ide bisnis sampai basis data produksi:

Entitas & ERD Normalisasi Skema SQL Prisma + Neon Produksi
Gambar 1.1 — Dari ide bisnis hingga 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.

Bab Dua

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:

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:

Praktik terbaikGunakan surrogate key sebagai PK (mis. 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).

SQL — tabel dengan tiga jenis kunci
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:

KategoriKepanjanganPerintah utama
DDLData Definition LanguageCREATE, ALTER, DROP
DMLData Manipulation LanguageINSERT, UPDATE, DELETE
DQLData Query LanguageSELECT
DCLData Control LanguageGRANT, REVOKE
TCLTransaction Control LanguageBEGIN, 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)
Bab Tiga

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:

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.

TipeContohImplementasi
One-to-One (1:1)Pengguna ↔ ProfilFK + UNIQUE di salah satu tabel (atau gabungkan)
One-to-Many (1:N)Pengguna → PesananFK di sisi “many” (kolom user_id di orders)
Many-to-Many (N:M)Pesanan ↔ ProdukTabel penghubung (junction table) dengan dua FK

Aturan emas yang wajib dihafal:

Aturan implementasi kardinalitas1:N → letakkan foreign key di sisi “banyak”. N:M → Anda selalu butuh tabel ketiga (junction/pivot). Tidak ada cara memodelkan many-to-many hanya dengan dua tabel di database relasional.
SQL — ketiga pola kardinalitas
-- 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:

tepat satu (one and only one) nol atau banyak (zero or many) satu atau banyak (one or many)
Gambar 3.1 — Simbol kardinalitas crow's foot

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.

Jebakan umumMelewatkan tabel penghubung pada relasi N:M, lalu “menyiasati” dengan menaruh daftar id dalam satu kolom teks berkoma ("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".

ERD BuilderJSON → Diagram · vanilla JS · tanpa jaringan
siap.
Untuk AIAlat ini murni klien: JSON diurai dengan 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.
Bab Empat

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_idcustomer_namecustomer_emailproductcategory
1Andiandi@mail.idKeyboard; MouseAksesori
2Andiandi@mail.idMonitorLayar

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.

Pelanggaran 1NF paling umumMenyimpan JSON array atau daftar berkoma di kolom yang seharusnya berelasi. (Catatan: kolom 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

BentukMenghapusAturan inti
1NFNilai non-atomik, grup berulangSatu nilai per sel
2NFKetergantungan parsialNon-kunci bergantung pada seluruh PK
3NFKetergantungan transitifNon-kunci hanya bergantung pada PK
BCNFAnomali sisa pada candidate keySetiap 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:

SQL — menjaga hitungan denormal dengan trigger
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();
Aturan praktisMulailah selalu dari skema ternormalisasi (3NF). Denormalisasi adalah optimasi — dan seperti semua optimasi, lakukan belakangan, berdasarkan pengukuran, bukan firasat. Redundansi tanpa penjaga sinkronisasi adalah bom waktu konsistensi.
Bab Lima

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:

KebutuhanTipe PostgresCatatan
Kunci integerBIGINT + IDENTITYPakai BIGINT, bukan INT; hemat penyesalan saat tumbuh
Kunci acak/publikUUIDAman dibagikan, tidak bocorkan jumlah baris
UangNUMERIC(12,2)Jangan pernah pakai FLOAT/REAL untuk uang
TeksTEXTPostgres: TEXT = VARCHAR tanpa batas, sama cepat
WaktuTIMESTAMPTZSelalu simpan zona waktu; hindari TIMESTAMP polos
Benar/salahBOOLEANBukan INT 0/1
Pilihan tetapENUM atau tabel referensiLihat bawah
Data semi-terstrukturJSONBTerindeks, bisa dikueri; bukan alasan malas normalisasi
Uang dan FLOAT0.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.

SQL — constraint sebagai pertahanan integritas
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:

PendekatanKelebihanKekurangan
ENUM nativeRingkas, cepat, hemat ruangMenambah nilai butuh ALTER TYPE; sulit dihapus
Tabel referensi + FKFleksibel, bisa punya metadata (label, urutan)Butuh JOIN
TEXT + CHECK INPaling sederhana diubahTanpa jaminan pusat, mudah salah ketik
SQL — enum native dan cara menambah nilai
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.

SQL — JSONB, operator, dan indeks GIN
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);
Aturan JSONBSimpan sebagai kolom relasional apa pun yang Anda: (a) kueri dengan WHERE sering, (b) JOIN, (c) beri constraint, atau (d) agregasi. Simpan di JSONB apa yang benar-benar bebas bentuk dan hanya dibaca utuh.
Bab Enam

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:

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:

SkenarioManfaat branching
Preview deployment (Vercel PR)Setiap PR dapat database sendiri, data realistis, terisolasi
Menguji migrasi berisikoJalankan di branch; jika rusak, buang branch, produksi aman
Debugging bug produksiBranch dari titik waktu tertentu untuk mereproduksi kondisi
Analitik beratBranch read-only agar kueri berat tak mengganggu produksi
Bash — alur branching dengan Neon CLI
# 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.

.env — variabel koneksi (jangan commit!)
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"
Pooling koneksiFungsi serverless bisa membuka ribuan koneksi singkat. Postgres punya batas koneksi. Gunakan endpoint pooled Neon (PgBouncer) untuk trafik aplikasi, dan endpoint direct untuk migrasi skema. Prisma memakai DATABASE_URL (pooled) dan directUrl (direct) persis untuk ini.
Untuk AISaat menghasilkan panduan setup Neon, selalu ingatkan pengguna: (1) 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.
Bab Tujuh

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

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:

prisma/schema.prisma
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.

Bash — siklus migrasi Prisma
# 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.

TypeScript — operasi CRUD dan relasi
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):

TypeScript — transaksi interaktif
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 vs SQL mentahPrisma unggul untuk 95% kueri CRUD dan menjaga tipe. Untuk kueri analitik kompleks atau fitur Postgres spesifik, gunakan 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
Bab Delapan

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.

Jebakan mahalPostgres tidak membuat indeks otomatis pada kolom foreign key. Tabel 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.
SQL — jenis-jenis indeks
-- 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.

SQL — EXPLAIN ANALYZE
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 EXPLAINArtiSikap
Seq ScanBaca seluruh tabelOK untuk tabel kecil; buruk untuk besar
Index ScanPakai indeks lalu ambil barisUmumnya diinginkan
Index Only ScanJawaban ada di indeks sajaTerbaik — tak menyentuh tabel
Bitmap Heap ScanGabung banyak indeksWajar untuk filter majemuk
Nested Loop / Hash JoinStrategi JOINPerhatikan jika baris membengkak
Biaya indeksIndeks bukan gratis: ia mempercepat baca tetapi memperlambat tulis (setiap 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
Bab Sembilan

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

JOINMengembalikan
INNER JOINHanya baris yang cocok di kedua tabel
LEFT JOINSemua baris tabel kiri; NULL bila kanan tak cocok
RIGHT JOINSemua baris tabel kanan; NULL bila kiri tak cocok
FULL OUTER JOINSemua baris kedua tabel; NULL di sisi yang tak cocok
INNER LEFT RIGHT FULL
Gambar 9.1 — Empat jenis JOIN sebagai diagram Venn
SQL — JOIN dalam praktik
-- 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).

SQL — pendapatan per produk, hanya yang di atas ambang
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.

Deteksi N+1Jika log database Anda menunjukkan kueri identik berulang-ulang hanya berbeda id, Anda kena N+1. Aktifkan logging kueri Prisma (log: ['query']) saat pengembangan untuk memergokinya.
Bab Sepuluh

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

PropertiJaminan
AtomicitySemua operasi berhasil, atau tidak satu pun (all-or-nothing). Gagal di tengah? ROLLBACK total.
ConsistencyTransaksi membawa database dari satu keadaan valid ke keadaan valid lain; semua constraint tetap terpenuhi.
IsolationTransaksi yang berjalan bersamaan tidak saling mengganggu (seolah berurutan).
DurabilitySetelah COMMIT, data bertahan meski server mati mendadak.
SQL — transaksi transfer saldo (contoh klasik atomicity)
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.

LevelDirty readNon-repeatable readPhantom read
Read Uncommittedmungkin*mungkinmungkin
Read Committed (default PG)tidakmungkinmungkin
Repeatable Readtidaktidaktidak (di PG)
Serializabletidaktidaktidak

*Di Postgres, Read Uncommitted berperilaku seperti Read Committed — dirty read tidak pernah terjadi.

Locking dan mencegah lost update

Skenario “barang terakhir”: dua transaksi membaca stock = 1, keduanya mengurangi, hasilnya terjual dua kali. Dua strategi pencegahan:

SQL — pessimistic locking dengan SELECT ... FOR UPDATE
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.

Aturan konkurensiUntuk operasi kritis (pembayaran, stok, saldo), jangan pernah “baca lalu tulis” di dua langkah tanpa proteksi. Gunakan operasi atomik (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
Bab Sebelas

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.

PolaIsolasiBiaya & kompleksitas
Shared DB, shared schema
(kolom tenant_id)
Logis (lewat kueri)Termurah, paling mudah skala; risiko kebocoran bila lupa filter
Shared DB, schema per tenantMenengahIsolasi lebih baik; migrasi jadi berlipat
Database per tenantTerkuatTermahal; 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.

SQL — skema bertenant dengan Row-Level Security
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';
Kebocoran multi-tenantBug paling menakutkan di SaaS adalah satu 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.
Untuk AISaat menghasilkan skema multi-tenant, jangan hanya menambahkan 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.
Bab Dua Belas

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-cacheHati-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, antrianSaat konsistensi kuat lebih penting dari kecepatan
TypeScript — pola cache-aside dengan Upstash Redis
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)
Aturan cachingJangan menambahkan cache sampai Anda punya bukti bahwa database adalah bottleneck. Cache menambah satu sumber kebenaran baru yang bisa basi — kompleksitas nyata. Mulai dengan indeks dan kueri yang baik; cache adalah lapisan terakhir, bukan pertama.
Bab Tiga Belas

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

TipeContohIdeal untuk
DocumentMongoDB, FirestoreDokumen bersarang, skema berevolusi cepat, prototipe
Key-ValueRedis, DynamoDBCache, session, lookup ultra-cepat
Wide-ColumnCassandra, ScyllaDBTulis masif, time-series, skala horizontal ekstrem
GraphNeo4jRelasi 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:

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).

Kabar baik dari Postgres modernBatas SQL vs NoSQL makin kabur. Postgres bisa menyimpan dokumen (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.
Jebakan “polyglot persistence”Menambahkan database baru untuk tiap kebutuhan terdengar canggih, tetapi setiap datastore adalah beban operasional: backup, pemantauan, konsistensi lintas-sistem, keahlian tim. Konsolidasi ke Postgres selama mungkin; tambah datastore khusus hanya saat rasa sakitnya nyata dan terukur.

“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)
Bab Empat Belas

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.

SQL — created_at / updated_at otomatis
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.

SQL — soft delete dengan partial unique index
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;
Trade-off soft deleteSoft delete membuat setiap kueri, indeks unik, dan foreign key jadi lebih rumit (harus sadar status hapus). Jangan terapkan buta di semua tabel; pakai di entitas yang benar-benar butuh jejak/pemulihan. Untuk kepatuhan penghapusan (mis. GDPR), soft delete saja tidak cukup — butuh penghapusan sungguhan atau anonimisasi.

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.

SQL — tabel riwayat
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.

SQL — tabel event append-only
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.

prisma/schema.prisma — skema produksi (ringkas)
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:

JSON — siap ditempel ke ERD Builder
{ "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"]
] }
Bab Lima Belas

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

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:

Prompt — template meminta skema
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.
Untuk AISaat menghasilkan skema, terapkan default aman ini tanpa diminta: (1) surrogate key 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:

PeriksaPertanyaan
Tipe uangApakah NUMERIC/Decimal, bukan float?
Indeks FKSetiap kolom FK punya indeks?
N:MAda junction table, bukan array/CSV?
NormalisasiAda data berulang yang melanggar 3NF?
ConstraintAturan bisnis ditegakkan (NOT NULL, CHECK, UNIQUE)?
Isolasi tenantMulti-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
Penutup

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:

  1. Mulai dari entitas dan relasi, bukan tabel. Kata benda jadi entitas, kata kerja jadi relasi.
  2. Normalkan ke 3NF lebih dulu; denormalkan hanya dengan bukti dan penjaga sinkronisasi.
  3. Pilih tipe yang tepat: NUMERIC untuk uang, TIMESTAMPTZ untuk waktu, surrogate key untuk identitas.
  4. Setiap foreign key butuh indeks; setiap N:M butuh junction table.
  5. Bungkus operasi kritis dalam transaksi; serahkan konkurensi pada database.
  6. Biarkan database menegakkan aturan lewat constraint — kode bisa lupa, constraint tidak.
  7. 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

IstilahDefinisi
ACIDAtomicity, Consistency, Isolation, Durability — empat jaminan transaksi database.
AtributProperti dari sebuah entitas; menjadi kolom di tabel.
B-treeStruktur indeks terurut default Postgres; efisien untuk kesetaraan, rentang, dan pengurutan.
BCNFBoyce-Codd Normal Form; versi lebih ketat dari 3NF.
CardinalityJumlah relasi antar entitas: 1:1, 1:N, atau N:M.
Composite keyPrimary key yang terdiri dari lebih dari satu kolom.
ConstraintAturan yang ditegakkan database: NOT NULL, UNIQUE, CHECK, FOREIGN KEY.
DDL / DML / DQLSub-bahasa SQL untuk mendefinisikan / memanipulasi / menanyakan data.
DenormalisasiMenyimpan data redundan secara sengaja demi performa baca.
Entity (entitas)Objek/konsep yang datanya disimpan; biasanya menjadi satu tabel.
ERDEntity-Relationship Diagram; peta visual entitas, atribut, dan relasi.
Foreign key (FK)Kolom yang menunjuk ke primary key tabel lain; menciptakan relasi.
IndexStruktur bantu yang mempercepat pencarian baris.
Isolation levelTingkat pemisahan antar-transaksi konkuren (Read Committed, Serializable, dll).
JSONBTipe Postgres untuk menyimpan JSON biner yang bisa diindeks dan dikueri.
Junction tableTabel penghubung untuk merepresentasikan relasi N:M.
MigrationPerubahan skema yang ter-versikan dan diterapkan bertahap.
NormalisasiMenata data agar setiap fakta disimpan satu kali; menghapus redundansi.
N+1Anti-pola: 1 kueri daftar + N kueri relasi per item; sangat lambat.
ORMObject-Relational Mapping; pustaka yang memetakan tabel ke objek kode (mis. Prisma).
Primary key (PK)Kolom yang mengidentifikasi setiap baris secara unik.
RLSRow-Level Security; kebijakan Postgres yang memfilter baris per konteks (mis. tenant).
Serverless PostgresPostgres yang memisahkan komputasi dari penyimpanan (mis. Neon), bisa scale-to-zero.
Soft deleteMenandai baris terhapus (deleted_at) alih-alih menghapusnya secara fisik.
Surrogate keyKunci buatan tanpa makna bisnis (BIGINT/UUID) sebagai PK.
TransactionSekelompok operasi yang diperlakukan sebagai satu unit all-or-nothing.
Weak entityEntitas 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

Integritas & tipe

Normalisasi & performa

Skala & operasi

Lampiran A — Referensi Cepat SQL

Kartu contek satu halaman untuk operasi yang paling sering Anda butuhkan. Semua contoh memakai sintaks PostgreSQL.

SQL — operasi harian
-- 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

PostgreSQLPrismaPakai untuk
BIGINTBigIntKunci, hitungan besar
INTEGERIntAngka biasa (< 2 miliar)
NUMERIC(p,s)Decimal @db.DecimalUang, presisi tetap
TEXTStringTeks apa pun
BOOLEANBooleanBenar/salah
TIMESTAMPTZDateTime @db.TimestamptzWaktu dengan zona
UUIDString @db.UuidKunci acak/publik
JSONBJsonData semi-terstruktur
ENUMenum { ... }Pilihan tetap
type[]Type[]Array skalar (hemat, non-relasional)

Daftar Pustaka & Bacaan Lanjutan

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

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:

SQL — satu peminjaman aktif per eksemplar
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:

JSON — skema perpustakaan
{ "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"]
] }
Pelajaran kunci latihan iniKetika relasi many-to-many punya atribut sendiri (tanggal, status, jumlah), ia bukan lagi junction pasif — ia entitas sejati. Loan, OrderItem, dan Enrollment adalah contoh klasik. Mengenali ini mencegah Anda memaksakan model yang terlalu sederhana.
Tentang Penyusun

Galih Prasetyo

Galih Prasetyo adalah insinyur perangkat lunak dan penulis teknis yang berfokus pada rekayasa data, arsitektur backend, dan praktik rekayasa di era AI. Ia menulis seri buku fondasi yang bertujuan membuat konsep-konsep inti — yang sering dianggap “terlalu dasar untuk ditulis” — menjadi jelas, terstruktur, dan dapat langsung dipraktikkan.

Buku ini lahir dari keyakinan sederhana: mayoritas masalah perangkat lunak yang mahal berakar pada model data yang keliru sejak awal, dan fondasi yang kokoh dapat dipelajari siapa saja yang mau bersungguh-sungguh.

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.

AspekKeterangan
JudulDatabase & Pemodelan Data (ERD) — Fondasi Setiap Aplikasi
PenulisGalih Prasetyo
Tahun2026
BahasaIndonesia (id)
CakupanRelasional, ERD, normalisasi, PostgreSQL, Neon, Prisma, indexing, transaksi, multi-tenant, caching, NoSQL, pola pemodelan
Tema desainData blueprint — biru, teal, slate; font Space Grotesk, Inter, JetBrains Mono
Fitur interaktifERD Builder (JSON → diagram SVG), vanilla JS
EdisiPertama (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.

↑ Ke Atas ◆ ERD Builder