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 burukPerbaikanAlasan
TanggalLahirtanggal_lahircamelCase terlipat jadi tanggallahir bila tanpa kutip — sulit dibaca
nohpteleponsingkatan yang tidak jelas menyulitkan pendatang baru
dt_regdibuat_padasingkatan privat hanya dimengerti penulisnya
userpenggunaterlalu dekat dengan kata kunci SQL, rawan perlu kutip
data2data_verifikasinomor 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:

text
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:

text
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:

  1. Kolom identitas di paling atas — biasanya id sebagai primary key.
  2. Data inti bisnis di tengah — kolom yang menjelaskan entitas: nama, email, harga, tanggal kejadian.
  3. 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:

sql tabel-awal.sql
-- 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:

text
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:

sql tabel-final.sql
-- 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:

text
DROP TABLE
CREATE TABLE

Struktur ini terlihat hidup ketika diisi: nomor urut terisi sendiri, email ganda ditolak, poin negatif ditolak, dan data tanggal tersimpan sebagai tanggal sungguhan:

sql isi-data.sql
-- 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:

text
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:

sql komentar.sql
-- 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

Output yang Dihasilkan:

text
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: heap

Kolom 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:

KolomTipeAlasan singkat
idbigint identitypenomoran dijamin server; bigint untuk ruang tumbuh
nama_lengkaptext NOT NULLnama bebas panjangnya, wajib ada
emailtext UNIQUEsatu email satu akun, divalidasi server
teleponvarchar(15)nomor punya batas panjang nyata; boleh kosong
tanggal_lahirdatetanggal tanpa jam, bisa dihitung aritmetikanya
poininteger NOT NULL DEFAULT 0hitungan; anggota baru mulai dari nol
dibuat_padatimestamptz NOT NULLmomen kejadian dengan zona waktu
catatantextteks 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
sql
-- Tanpa tanda kutip, PostgreSQL melipat nama menjadi huruf kecil: data_pelanggan
SELECT count(*) AS jumlah_baris FROM Data_Pelanggan;

isi-data.sql

Unduh berkas
sql
-- 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
sql
-- 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
sql
-- 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
sql
-- 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
sql
-- 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
);