Membuat tabel dengan CREATE TABLE hanya butuh beberapa baris — tetapi tabel itu akan hidup bertahun-tahun di dalam aplikasi, di-query ribuan kali sehari, dan dibaca oleh programmer yang tidak pernah ikut merancangnya. Keputusan yang tampak kecil saat merancang struktur — bagaimana menamai kolom, urutan mana yang dipilih, di mana dokumentasi disimpan — menentukan apakah tabel itu jadi aset yang enak dirawat atau beban yang menakutkan. Chapter ini membahas tiga keputusan tersebut satu per satu, lalu merakit semuanya menjadi satu tabel pelanggan yang realistis untuk Toko Maju Jaya, dari draf pertama yang ceroboh sampai versi final yang terdokumentasi.
Konvensi Penamaan: snake_case dan Konsistensi #
PostgreSQL menerima hampir semua bentuk nama, jadi disiplin penamaan harus datang dari tim kita sendiri. Konvensi yang paling umum dan paling aman di PostgreSQL: huruf kecil semua, kata dipisah garis bawah (snake_case), tanpa spasi, tanpa angka ajaib. Nama seperti tanggal_lahir terbaca jelas, tidak butuh tanda kutip, dan tidak bentrok dengan kata kunci SQL.
| Contoh buruk | Perbaikan | Alasan |
|---|---|---|
TanggalLahir | tanggal_lahir | camelCase terlipat jadi tanggallahir bila tanpa kutip — sulit dibaca |
nohp | telepon | singkatan yang tidak jelas menyulitkan pendatang baru |
dt_reg | dibuat_pada | singkatan privat hanya dimengerti penulisnya |
user | pengguna | terlalu dekat dengan kata kunci SQL, rawan perlu kutip |
data2 | data_verifikasi | nomor belakang nama tidak menjelaskan apa pun |
Untuk bentuk tunggal atau jamak: pilih satu dan pakai secara konsisten. Dalam bahasa Indonesia kebanyakan kata benda tidak berubah bentuknya (pelanggan sudah mewakili satu maupun seribu), sehingga masalah ini jarang terasa — tetapi bila tim memakai nama Inggris, putuskan sekali: category atau categories, jangan dicampur. Rekomendasi kurs ini: bentuk tunggal untuk nama tabel, karena tabel bernama pelanggan dibaca sebagai “definisi satu entitas pelanggan”, dan setiap barisnya memang tepat satu pelanggan.
Satu jebakan penamaan yang wajib dipahami sejak awal: nama yang ditulis dengan tanda kutip ganda mempertahankan huruf besar dan spasi persis seperti ditulis. Tabel berikut sah dan bisa dipakai:
-- Nama yang ditulis dengan tanda kutip mempertahankan huruf besar dan spasi
CREATE TABLE "Data Pelanggan" (
id integer PRIMARY KEY,
nama text NOT NULL
);
INSERT INTO "Data Pelanggan" (id, nama) VALUES (1, 'Budi Santoso');
SELECT count(*) AS jumlah_baris FROM "Data Pelanggan";Output yang Dihasilkan:
CREATE TABLE
INSERT 0 1
jumlah_baris
--------------
1
(1 row)Masalahnya muncul pada akses berikutnya. Tanpa tanda kutip, PostgreSQL melipat nama menjadi huruf kecil — Data_Pelanggan dibaca sebagai data_pelanggan — dan tabel bernama itu tidak ada:
-- Tanpa tanda kutip, PostgreSQL melipat nama menjadi huruf kecil: data_pelanggan
SELECT count(*) AS jumlah_baris FROM Data_Pelanggan;Output yang Dihasilkan:
psql:akses-tanpa-kutip.sql:2: ERROR: relation "data_pelanggan" does not exist
LINE 1: SELECT count(*) AS jumlah_baris FROM Data_Pelanggan;
^Pesan relation "data_pelanggan" does not exist memperlihatkan lipatan huruf kecil yang diam-diam terjadi. Sejak saat itu, setiap query ke tabel itu wajib membawa tanda kutip dan huruf besar yang persis sama — beban kecil yang menumpuk selama bertahun-tahun. Konvensi snake_case huruf kecil membuat kita tidak pernah menyentuh masalah ini.
Urutan Kolom: Identitas, Data Inti, Kolom Audit #
Urutan kolom di CREATE TABLE menjadi urutan bawaan output SELECT * dan tampilan \d, jadi urutan yang rapi membantu setiap orang yang membaca tabel. Pola yang terbukti nyaman dibaca tiga lapis:
- Kolom identitas di paling atas — biasanya
idsebagai primary key. - Data inti bisnis di tengah — kolom yang menjelaskan entitas: nama, email, harga, tanggal kejadian.
- Kolom audit dan catatan di ujung —
dibuat_pada,catatan, kolom teknis yang jarang dilihat pengguna akhir.
Susunan ini juga awet terhadap perubahan: ALTER TABLE ... ADD COLUMN selalu menambah kolom baru di posisi paling akhir, sehingga kolom audit yang memang diposisikan di ujung tidak akan “tersisip” di tengah struktur seiring tabel bertambah tua. Pada tabel final yang akan kita rakit di bagian berikutnya, pola tiga lapis ini diterapkan langsung dan terlihat utuh pada output \d+.
Merakit Tabel pelanggan dari Nol #
Mari kita lihat betapa mudahnya jatuh ke struktur yang lemah. Draf pertama berikut sah menurut PostgreSQL — semuanya jalan tanpa protes:
-- Versi pertama: bisa jalan, tapi lemah — semua kolom bertipe text
CREATE TABLE pelanggan (
nama text,
email text,
no_hp text,
tgl_lahir text,
saldo text
);
INSERT INTO pelanggan (nama, email, no_hp, tgl_lahir, saldo) VALUES
('Budi Santoso', '[email protected]', '0812-0000-0001', '1990-04-12', '150000'),
('Siti Rahayu', '[email protected]', NULL, '1985-09-30', '0');
SELECT nama, email, no_hp, tgl_lahir, saldo
FROM pelanggan
ORDER BY nama;Output yang Dihasilkan:
CREATE TABLE
INSERT 0 2
nama | email | no_hp | tgl_lahir | saldo
--------------+------------------+----------------+------------+--------
Budi Santoso | [email protected] | 0812-0000-0001 | 1990-04-12 | 150000
Siti Rahayu | [email protected] | | 1985-09-30 | 0
(2 rows)Struktur ini punya tiga penyakit. Pertama, semua kolom bertipe text — tanggal lahir tidak bisa dihitung umurnya, saldo tidak bisa di-SUM tanpa konversi, dan PostgreSQL tidak menolak nilai seperti 'tanggal belum diisi' di kolom tanggal. Kedua, tidak ada kolom identitas — dua pelanggan bernama sama akan sulit dibedakan, dan tidak ada target aman untuk UPDATE maupun DELETE. Ketiga, nama-nama singkatan (no_hp, tgl_lahir) menghemat lima detik saat mengetik dan membebani setiap pembaca sesudahnya.
Versi final memperbaiki ketiganya sekaligus — tipe spesifik per kolom, identitas id di posisi pertama, nama lengkap tanpa singkatan, plus constraint yang membuat server ikut menjaga kualitas data:
-- Struktur final: nama snake_case, tipe spesifik, urutan kolom konsisten
DROP TABLE IF EXISTS pelanggan CASCADE;
CREATE TABLE pelanggan (
-- identitas baris
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
-- data inti pelanggan
nama_lengkap text NOT NULL,
email text UNIQUE,
telepon varchar(15),
tanggal_lahir date,
poin integer NOT NULL DEFAULT 0 CHECK (poin >= 0),
-- kolom audit dan catatan
dibuat_pada timestamptz NOT NULL,
catatan text
);Output yang Dihasilkan:
DROP TABLE
CREATE TABLEStruktur ini terlihat hidup ketika diisi: nomor urut terisi sendiri, email ganda ditolak, poin negatif ditolak, dan data tanggal tersimpan sebagai tanggal sungguhan:
-- Struktur yang baik membuat data masuk tanpa drama
INSERT INTO pelanggan (nama_lengkap, email, telepon, tanggal_lahir, dibuat_pada, catatan) VALUES
('Budi Santoso', '[email protected]', '0812-0000-0001', DATE '1990-04-12',
TIMESTAMPTZ '2026-01-05 09:15:00+07', NULL),
('Siti Rahayu', '[email protected]', NULL, DATE '1985-09-30',
TIMESTAMPTZ '2026-01-06 10:30:00+07', 'Pelanggan sejak era toko fisik'),
('Andi Wijaya', '[email protected]', '0813-1111-2222', DATE '2000-01-15',
TIMESTAMPTZ '2026-01-10 16:45:00+07', NULL);
SELECT id, nama_lengkap, email, telepon, poin
FROM pelanggan
ORDER BY id;Output yang Dihasilkan:
INSERT 0 3
id | nama_lengkap | email | telepon | poin
----+--------------+------------------+----------------+------
1 | Budi Santoso | [email protected] | 0812-0000-0001 | 0
2 | Siti Rahayu | [email protected] | | 0
3 | Andi Wijaya | [email protected] | 0813-1111-2222 | 0
(3 rows)Dokumentasi Struktur dengan COMMENT ON #
Tabel yang tidak terdokumentasi mengandalkan ingatan orang yang membuatnya — dan orang itu bisa pindah kerja. PostgreSQL menyediakan perintah COMMENT ON untuk menyimpan dokumentasi langsung di katalog database: satu untuk tabel, satu untuk setiap kolom yang perlu dijelaskan. Dokumentasi ini ikut tampil di \d+ dan bisa dibaca oleh tool administrasi yang sama bagi seluruh tim:
-- Dokumentasi struktur tersimpan di katalog database, bukan di kepala programmer
COMMENT ON TABLE pelanggan IS 'Pelanggan terdaftar Toko Maju Jaya';
COMMENT ON COLUMN pelanggan.id IS 'Identitas baris; nomor urut yang dijamin server';
COMMENT ON COLUMN pelanggan.email IS 'Alamat email untuk login dan notifikasi';
COMMENT ON COLUMN pelanggan.poin IS 'Poin loyalitas; tidak boleh negatif';
COMMENT ON COLUMN pelanggan.dibuat_pada IS 'Waktu pendaftaran pelanggan';
\d+ pelangganOutput yang Dihasilkan:
COMMENT
COMMENT
COMMENT
COMMENT
COMMENT
Table "public.pelanggan"
Column | Type | Collation | Nullable | Default | Storage | Compression | Stats target | Description
---------------+--------------------------+-----------+----------+------------------------------+----------+-------------+--------------+-------------------------------------------------
id | bigint | | not null | generated always as identity | plain | | | Identitas baris; nomor urut yang dijamin server
nama_lengkap | text | | not null | | extended | | |
email | text | | | | extended | | | Alamat email untuk login dan notifikasi
telepon | character varying(15) | | | | extended | | |
tanggal_lahir | date | | | | plain | | |
poin | integer | | not null | 0 | plain | | | Poin loyalitas; tidak boleh negatif
dibuat_pada | timestamp with time zone | | not null | | plain | | | Waktu pendaftaran pelanggan
catatan | text | | | | extended | | |
Indexes:
"pelanggan_pkey" PRIMARY KEY, btree (id)
"pelanggan_email_key" UNIQUE CONSTRAINT, btree (email)
Check constraints:
"pelanggan_poin_check" CHECK (poin >= 0)
Access method: heapKolom Description pada output \d+ kini membawa penjelasan yang sebelumnya hanya hidup di obrolan tim — dan sekaligus memperlihatkan susunan kolom id → data inti → kolom audit yang kita rancang. Komentar ini tidak memakan ruang pada baris data: PostgreSQL menyimpannya sekali di katalog, bukan per baris.
Rekap Pemilihan Tipe per Kolom #
Setiap tipe pada tabel final di atas sudah dibahas tuntas pada section Tipe Data — khususnya rangkuman praktisnya di chapter Memilih Tipe Data . Sebagai pengingat, inilah alasan singkat tiap keputusan:
| Kolom | Tipe | Alasan singkat |
|---|---|---|
id | bigint identity | penomoran dijamin server; bigint untuk ruang tumbuh |
nama_lengkap | text NOT NULL | nama bebas panjangnya, wajib ada |
email | text UNIQUE | satu email satu akun, divalidasi server |
telepon | varchar(15) | nomor punya batas panjang nyata; boleh kosong |
tanggal_lahir | date | tanggal tanpa jam, bisa dihitung aritmetikanya |
poin | integer NOT NULL DEFAULT 0 | hitungan; anggota baru mulai dari nol |
dibuat_pada | timestamptz NOT NULL | momen kejadian dengan zona waktu |
catatan | text | teks bebas, memang opsional |
Tiga constraint yang ikut terpasang — PRIMARY KEY, UNIQUE, dan CHECK — masing-masing adalah topik tersendiri yang dibahas mendalam pada chapter-chapter berikutnya di section ini. Kita mulai dari yang paling fundamental: primary key, penjaga identitas setiap baris.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
akses-tanpa-kutip.sql
Unduh berkas-- Tanpa tanda kutip, PostgreSQL melipat nama menjadi huruf kecil: data_pelanggan
SELECT count(*) AS jumlah_baris FROM Data_Pelanggan;
isi-data.sql
Unduh berkas-- Struktur yang baik membuat data masuk tanpa drama
INSERT INTO pelanggan (nama_lengkap, email, telepon, tanggal_lahir, dibuat_pada, catatan) VALUES
('Budi Santoso', '[email protected]', '0812-0000-0001', DATE '1990-04-12',
TIMESTAMPTZ '2026-01-05 09:15:00+07', NULL),
('Siti Rahayu', '[email protected]', NULL, DATE '1985-09-30',
TIMESTAMPTZ '2026-01-06 10:30:00+07', 'Pelanggan sejak era toko fisik'),
('Andi Wijaya', '[email protected]', '0813-1111-2222', DATE '2000-01-15',
TIMESTAMPTZ '2026-01-10 16:45:00+07', NULL);
SELECT id, nama_lengkap, email, telepon, poin
FROM pelanggan
ORDER BY id;
komentar.sql
Unduh berkas-- Dokumentasi struktur tersimpan di katalog database, bukan di kepala programmer
COMMENT ON TABLE pelanggan IS 'Pelanggan terdaftar Toko Maju Jaya';
COMMENT ON COLUMN pelanggan.id IS 'Identitas baris; nomor urut yang dijamin server';
COMMENT ON COLUMN pelanggan.email IS 'Alamat email untuk login dan notifikasi';
COMMENT ON COLUMN pelanggan.poin IS 'Poin loyalitas; tidak boleh negatif';
COMMENT ON COLUMN pelanggan.dibuat_pada IS 'Waktu pendaftaran pelanggan';
\d+ pelanggan
nama-terkutip.sql
Unduh berkas-- Nama yang ditulis dengan tanda kutip mempertahankan huruf besar dan spasi
CREATE TABLE "Data Pelanggan" (
id integer PRIMARY KEY,
nama text NOT NULL
);
INSERT INTO "Data Pelanggan" (id, nama) VALUES (1, 'Budi Santoso');
SELECT count(*) AS jumlah_baris FROM "Data Pelanggan";
tabel-awal.sql
Unduh berkas-- Versi pertama: bisa jalan, tapi lemah — semua kolom bertipe text
CREATE TABLE pelanggan (
nama text,
email text,
no_hp text,
tgl_lahir text,
saldo text
);
INSERT INTO pelanggan (nama, email, no_hp, tgl_lahir, saldo) VALUES
('Budi Santoso', '[email protected]', '0812-0000-0001', '1990-04-12', '150000'),
('Siti Rahayu', '[email protected]', NULL, '1985-09-30', '0');
SELECT nama, email, no_hp, tgl_lahir, saldo
FROM pelanggan
ORDER BY nama;
tabel-final.sql
Unduh berkas-- Struktur final: nama snake_case, tipe spesifik, urutan kolom konsisten
DROP TABLE IF EXISTS pelanggan CASCADE;
CREATE TABLE pelanggan (
-- identitas baris
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
-- data inti pelanggan
nama_lengkap text NOT NULL,
email text UNIQUE,
telepon varchar(15),
tanggal_lahir date,
poin integer NOT NULL DEFAULT 0 CHECK (poin >= 0),
-- kolom audit dan catatan
dibuat_pada timestamptz NOT NULL,
catatan text
);