Setiap database yang baik berawal dari percakapan, bukan dari perintah CREATE TABLE. Pemilik Toko Maju Jaya bercerita tentang pelanggan yang datang, produk yang dijual, dan pesanan yang masuk — dan dari cerita itulah struktur database lahir. Kesalahan paling mahal dalam perancangan database bukan salah sintaks, melainkan salah menerjemahkan: entitas yang terlewat, atribut yang menempel di tabel yang salah, atau identitas yang dipilih dari kolom yang memang tidak unik. Chapter ini melatih proses penerjemahan itu secara sadar: mengubah narasi kebutuhan menjadi daftar entitas, mengubah entitas menjadi tabel, mengubah atribut menjadi kolom bertipe, dan memilih primary key yang layak menjadi identitas tiap baris.
Dari Cerita Pemilik Toko ke Daftar Entitas #
Berikut narasi kebutuhan yang kita terima dari pemilik Toko Maju Jaya, persis seperti yang diucapkannya:
“Saya jual alat tulis dan kertas. Setiap produk punya nama dan harga, dan saya kelompokkan ke kategori supaya rak gampang ditata. Pelanggan datang dari berbagai kota dan tinggalkan email buat info promo. Kalau mereka memesan, saya catat pesanan dengan tanggal dan totalnya. Dari pesanan itu saya bisa lihat produk mana yang laku dan pelanggan mana yang rajin belanja.”
Entitas adalah kata benda yang datanya perlu kita simpan sendiri karena memiliki kehidupan independen: produk tetap ada meski belum terjual, pelanggan tetap tercatat meski belum memesan. Sebaliknya, kata benda seperti harga, tanggal, atau kota adalah atribut — keterangan yang menempel pada entitas dan tidak berdiri sendiri. Tabel berikut memilah setiap kata benda penting dari narasi tersebut:
| Kata benda | Keputusan | Alasan |
|---|---|---|
| produk | entitas | hidup mandiri: ada sebelum dan sesudah transaksi |
| kategori | entitas | daftar berulang yang layak dikelola sendiri |
| pelanggan | entitas | identitas yang perlu dilacak lintas transaksi |
| pesanan | entitas | peristiwa yang terjadi pada waktu tertentu |
| harga, tanggal, kota, email, total | atribut | keterangan tentang entitas, bukan hal mandiri |
| Toko Maju Jaya | bukan entitas | subjek bisnisnya; satu database memodelkan satu toko |
Perhatikan satu jebakan klasik di baris terakhir: “Toko Maju Jaya” memang kata benda, tetapi toko adalah pihak yang memiliki seluruh database, bukan data yang perlu disimpan sebagai baris. Entitas yang terlalu banyak membuat skema bengkak tanpa manfaat; entitas yang terlewat membuat atribut tidak punya tempat yang wajar.
Entitas Menjadi Tabel, Atribut Menjadi Kolom #
Pemodelan data tradisional memakai istilah ER (entity-relationship) untuk tahap konsep, lalu SQL untuk tahap implementasi. Keduanya memetakan hal yang sama dengan nama berbeda:
| Istilah konsep (ER) | Istilah PostgreSQL | Contoh di Toko Maju Jaya |
|---|---|---|
| entitas | tabel (CREATE TABLE) | pelanggan, produk, pesanan, kategori |
| atribut | kolom | nama, harga, tanggal |
| nilai atribut | nilai kolom pada satu baris | 'Buku Tulis', 4500.00 |
| identifier entitas | primary key | pelanggan.id |
| relasi antarentitas | pasangan PK dan foreign key | dibahas mulai chapter Relasi One-to-One |
Pemetaan ini bukan sekadar ganti istilah. Saat sebuah atribut berpindah dari “keterangan di narasi” menjadi “kolom bertipe di tabel”, atribut itu harus tunduk pada aturan mesin: tipenya harus dipilih, nilainya bisa atau tidak bisa kosong, dan identitas barisnya harus terjamin. Mekanisme constraint yang menjaga aturan-aturan itu sudah kita kenal satu per satu di section Table Design ; di section ini kita memakainya sebagai bahan bangunan desain.
Membangun Empat Tabel Pertama #
Empat entitas hasil penerjemahan menjadi empat tabel. Kolom penghubung seperti kategori_id dan pelanggan_id sudah disiapkan sebagai kolom biasa, tetapi sengaja belum diikat foreign key — keputusan arah dan bentuk relasi adalah bahasan chapter-chapter berikutnya:
-- Skema awal Toko Maju Jaya: empat entitas, masing-masing satu tabel.
-- Kolom penghubung (kategori_id, pelanggan_id) sengaja belum diikat
-- foreign key -- bab-bab relasi berikutnya yang mengurainya.
CREATE TABLE kategori (
id integer PRIMARY KEY,
nama text NOT NULL UNIQUE
);
CREATE TABLE pelanggan (
id integer PRIMARY KEY,
nama text NOT NULL,
kota text NOT NULL,
email text NOT NULL UNIQUE
);
CREATE TABLE produk (
id integer PRIMARY KEY,
nama text NOT NULL,
harga numeric(12, 2) NOT NULL CHECK (harga >= 0),
kategori_id integer
);
CREATE TABLE pesanan (
id integer PRIMARY KEY,
tanggal date NOT NULL,
total numeric(12, 2) NOT NULL,
pelanggan_id integer
);
INSERT INTO kategori (id, nama) VALUES
(1, 'alat tulis'),
(2, 'kertas');
INSERT INTO pelanggan (id, nama, kota, email) VALUES
(1, 'Budi Santoso', 'Bandung', '[email protected]'),
(2, 'Siti Rahayu', 'Jakarta', '[email protected]'),
(3, 'Dewi Lestari', 'Medan', '[email protected]');
INSERT INTO produk (id, nama, harga, kategori_id) VALUES
(1, 'Buku Tulis', 4500.00, 1),
(2, 'Pulpen Hitam', 3000.00, 1),
(3, 'Kertas HVS A4', 55000.00, 2);
INSERT INTO pesanan (id, tanggal, total, pelanggan_id) VALUES
(101, DATE '2026-01-05', 150000.00, 1),
(102, DATE '2026-01-18', 75000.00, 1),
(103, DATE '2026-01-09', 220000.00, 2);
SELECT id, nama, kota FROM pelanggan ORDER BY id;
SELECT id, nama, harga, kategori_id FROM produk ORDER BY id;Output yang Dihasilkan:
CREATE TABLE
CREATE TABLE
CREATE TABLE
CREATE TABLE
INSERT 0 2
INSERT 0 3
INSERT 0 3
INSERT 0 3
id | nama | kota
----+--------------+---------
1 | Budi Santoso | Bandung
2 | Siti Rahayu | Jakarta
3 | Dewi Lestari | Medan
(3 rows)
id | nama | harga | kategori_id
----+---------------+----------+-------------
1 | Buku Tulis | 4500.00 | 1
2 | Pulpen Hitam | 3000.00 | 1
3 | Kertas HVS A4 | 55000.00 | 2
(3 rows)Empat perintah CREATE TABLE di file tersebut masing-masing memuat satu keputusan penting:
kategori (id integer PRIMARY KEY, nama text NOT NULL UNIQUE)— entitas paling sederhana: identitas plus satu nama yang tidak boleh ganda, karena kategori bernama sama hanya membuat kebingungan laporan.pelanggan (id integer PRIMARY KEY, ... email text NOT NULL UNIQUE)—emailmendapatUNIQUEkarena berfungsi sebagai kontak resmi: satu alamat email hanya boleh dimiliki satu pelanggan terdaftar.produk (... harga numeric(12, 2) NOT NULL CHECK (harga >= 0))— tipenumericdipilih untuk uang agar bebas dari pembulatan biner, danCHECK (harga >= 0)menolak harga minus yang jelas tidak masuk akal.pesanan (id, tanggal, total, pelanggan_id)—tanggalbertipedatekarena jam pembuatan tidak dibutuhkan narasi kebutuhan; bila kelak dibutuhkan, tipetimestamptinggal menggantikan.
Setiap Atribut Butuh Tipe Data #
Narasi pemilik toko tidak pernah menyebut tipe data — “harga” saja, bukan “numeric(12,2)”. Memilih tipe adalah pekerjaan penerjemah, dan pilihan yang tepat menentukan akurasi serta performa bertahun-tahun ke depan. Panduan lengkapnya ada di chapter Memilih Tipe Data ; tabel berikut merangkum keputusan untuk skema kita:
| Atribut | Tipe dipilih | Alasan singkat |
|---|---|---|
harga, total | numeric(12, 2) | uang butuh eksak, dua desimal, batas 999 miliar |
nama, kota, email | text | panjang tidak pasti; text tanpa batas di PostgreSQL |
tanggal | date | hanya tanggal tanpa jam |
id tiap tabel | integer | jumlah baris toko satu cabang jauh di bawah 2,1 miliar |
Dua keputusan pada tabel di atas layak ditegaskan. Pertama, numeric untuk uang menghindari kejutan pembulatan biner yang dibedah di chapter Numeric dan Decimal
— selisih rupiah sekecil apa pun menjadi masalah saat rekonsiliasi kasir. Kedua, integer dipilih karena skala datanya; bila entitas berpotensi melebihi dua miliar baris, bigint adalah pilihan yang lebih aman, dan bila id harus dibangkitkan lintas server tanpa koordinasi, uuid menggantikannya seperti dibahas di chapter UUID
.
Nama Bukan Identitas: Memilih Primary Key #
Primary key adalah janji bahwa satu baris bisa selalu ditemukan kembali. Syaratnya keras: unik, tidak kosong, dan idealnya tidak pernah berubah. Nama pelanggan gagal di ketiga syarat itu — dua orang boleh bernama persis sama:
-- Nama orang bukan identitas: dua pelanggan boleh bernama sama,
-- id primary key yang membedakan mereka.
INSERT INTO pelanggan (id, nama, kota, email) VALUES
(4, 'Budi Santoso', 'Surabaya', '[email protected]');
SELECT id, nama, kota, email FROM pelanggan ORDER BY id;Output yang Dihasilkan:
INSERT 0 1
id | nama | kota | email
----+--------------+----------+-------------------
1 | Budi Santoso | Bandung | [email protected]
2 | Siti Rahayu | Jakarta | [email protected]
3 | Dewi Lestari | Medan | [email protected]
4 | Budi Santoso | Surabaya | [email protected]
(4 rows)Tabel pelanggan kini memuat dua baris bernama Budi Santoso: satu di Bandung dengan [email protected], satu di Surabaya dengan [email protected]. Keduanya adalah manusia berbeda dengan riwayat belanja berbeda, dan hanya kolom id yang membedakan mereka. Sekarang kita uji sisi lainnya — mencoba memakai id yang sudah terpakai:
-- Id pelanggan yang sudah terpakai tidak boleh dipakai lagi:
-- primary key menolaknya di level server.
INSERT INTO pelanggan (id, nama, kota, email) VALUES
(1, 'Ratna Kusuma', 'Bandung', '[email protected]');Output yang Dihasilkan:
psql:id-duplikat.sql:4: ERROR: duplicate key value violates unique constraint "pelanggan_pkey"
DETAIL: Key (id)=(1) already exists.Server menolak baris kelima karena Key (id)=(1) already exists. Penolakan terjadi di level mesin, bukan di kode aplikasi, sehingga bug aplikasi atau proses manual tidak bisa merusak janji identitas itu. Pilihan antara primary key buatan (integer/bigint identity, uuid) dan primary key alami (kolom nyata seperti email) memiliki trade-off masing-masing yang sudah dibedah di chapter Primary Key
; untuk skema Toko Maju Jaya kita memakai id numerik buatan karena sederhana, stabil, dan tidak ikut berubah saat data pelanggan berubah.
Memeriksa Hasil Desain Lewat Katalog Sistem #
Setelah empat tabel dibuat, hasil desain bisa diaudit tanpa membuka ulang file SQL. Katalog sistem information_schema.columns mencatat setiap kolom setiap tabel beserta tipenya:
-- Audit hasil desain lewat katalog sistem: daftar seluruh kolom
-- beserta tipenya, tanpa membuka ulang file CREATE TABLE.
SELECT table_name, column_name, data_type
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name IN ('kategori', 'pelanggan', 'produk', 'pesanan')
ORDER BY table_name, ordinal_position;Output yang Dihasilkan:
table_name | column_name | data_type
------------+--------------+-----------
kategori | id | integer
kategori | nama | text
pelanggan | id | integer
pelanggan | nama | text
pelanggan | kota | text
pelanggan | email | text
pesanan | id | integer
pesanan | tanggal | date
pesanan | total | numeric
pesanan | pelanggan_id | integer
produk | id | integer
produk | nama | text
produk | harga | numeric
produk | kategori_id | integer
(14 rows)Empat belas baris hasil query tersebut adalah seluruh atribut skema kita, terurut per tabel sesuai posisi kolomnya. Perhatikan satu detail: kolom harga tampil sebagai numeric tanpa precision dan scale, padahal dideklarasikan numeric(12, 2) — katalog information_schema memang menyimpan detail presisi di kolom lain (numeric_precision, numeric_scale), bukan di data_type. Audit semacam ini berguna saat mewarisi database orang lain: daftar kolom yang ada adalah dokumen desain yang paling jujur, karena tidak pernah kedaluwarsa.
Diagram ER Skema Toko Maju Jaya #
Empat tabel yang baru dibangun terpetakan ke diagram ER berikut, lengkap dengan atribut dan tanda kunci tiap entitas:
Notasi ||--o{ pada dua relasi dibaca “satu ke nol-atau-banyak”: satu pelanggan boleh membuat banyak pesanan, satu kategori menampung banyak produk. Tanda PK menandai primary key, UK unique key, dan FK kolom rujukan yang akan diikat foreign key. Satu pertanyaan sengaja dibiarkan terbuka oleh diagram ini: bagaimana produk dan pesanan berhubungan, bila satu pesanan bisa memuat banyak produk dan satu produk bisa masuk banyak pesanan? Jawabannya butuh bentuk relasi ketiga — dan seluruh sisa section ini dibangun di atas ketiga bentuk relasi itu, dimulai dari yang paling jarang namun paling sering disalahpahami: Relasi One-to-One
.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
audit-atribut.sql
Unduh berkas-- Audit hasil desain lewat katalog sistem: daftar seluruh kolom
-- beserta tipenya, tanpa membuka ulang file CREATE TABLE.
SELECT table_name, column_name, data_type
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name IN ('kategori', 'pelanggan', 'produk', 'pesanan')
ORDER BY table_name, ordinal_position;
buat-entitas.sql
Unduh berkas-- Skema awal Toko Maju Jaya: empat entitas, masing-masing satu tabel.
-- Kolom penghubung (kategori_id, pelanggan_id) sengaja belum diikat
-- foreign key -- bab-bab relasi berikutnya yang mengurainya.
CREATE TABLE kategori (
id integer PRIMARY KEY,
nama text NOT NULL UNIQUE
);
CREATE TABLE pelanggan (
id integer PRIMARY KEY,
nama text NOT NULL,
kota text NOT NULL,
email text NOT NULL UNIQUE
);
CREATE TABLE produk (
id integer PRIMARY KEY,
nama text NOT NULL,
harga numeric(12, 2) NOT NULL CHECK (harga >= 0),
kategori_id integer
);
CREATE TABLE pesanan (
id integer PRIMARY KEY,
tanggal date NOT NULL,
total numeric(12, 2) NOT NULL,
pelanggan_id integer
);
INSERT INTO kategori (id, nama) VALUES
(1, 'alat tulis'),
(2, 'kertas');
INSERT INTO pelanggan (id, nama, kota, email) VALUES
(1, 'Budi Santoso', 'Bandung', '[email protected]'),
(2, 'Siti Rahayu', 'Jakarta', '[email protected]'),
(3, 'Dewi Lestari', 'Medan', '[email protected]');
INSERT INTO produk (id, nama, harga, kategori_id) VALUES
(1, 'Buku Tulis', 4500.00, 1),
(2, 'Pulpen Hitam', 3000.00, 1),
(3, 'Kertas HVS A4', 55000.00, 2);
INSERT INTO pesanan (id, tanggal, total, pelanggan_id) VALUES
(101, DATE '2026-01-05', 150000.00, 1),
(102, DATE '2026-01-18', 75000.00, 1),
(103, DATE '2026-01-09', 220000.00, 2);
SELECT id, nama, kota FROM pelanggan ORDER BY id;
SELECT id, nama, harga, kategori_id FROM produk ORDER BY id;
id-duplikat.sql
Unduh berkas-- Id pelanggan yang sudah terpakai tidak boleh dipakai lagi:
-- primary key menolaknya di level server.
INSERT INTO pelanggan (id, nama, kota, email) VALUES
(1, 'Ratna Kusuma', 'Bandung', '[email protected]');
nama-bukan-identitas.sql
Unduh berkas-- Nama orang bukan identitas: dua pelanggan boleh bernama sama,
-- id primary key yang membedakan mereka.
INSERT INTO pelanggan (id, nama, kota, email) VALUES
(4, 'Budi Santoso', 'Surabaya', '[email protected]');
SELECT id, nama, kota, email FROM pelanggan ORDER BY id;