Peta bentuk normal di chapter Normalisasi Database
menjanjikan tiga jenjang, dan jenjang pertama dimulai dari aturan yang tampak sepele: satu sel, satu nilai. Tabel pesanan_flat di chapter itu kebetulan sudah lolos aturan ini, tetapi keberuntungan tidak berlanjut ketika Toko Maju Jaya mulai memindahkan daftar kontak lama dari buku telepon ke database. Formulir pendaftaran lamanya meminta “semua nomor telepon, pisahkan dengan koma” dan “minat belanja, pisahkan dengan koma” — dan hasil impornya menyimpan banyak nilai dalam satu sel. Chapter ini membuktikan kenapa bentuk seperti itu melukai setiap query, lalu memperbaikinya dengan tiga obat yang berbeda: baris terpisah, array PostgreSQL, dan junction table.
Aturan Satu Sel, Satu Nilai #
First Normal Form (1NF) mengatur dua hal sekaligus. Pertama, atomicity: setiap sel memuat tepat satu nilai yang tidak perlu dibedah lagi oleh database untuk diperiksa. Kedua, larangan repeating group: tidak ada kolom tiruan bernama telepon1, telepon2, telepon3 yang sebenarnya adalah satu jenis fakta yang disalin berulang. Nilai dianggap atomik bukan dari panjangnya, melainkan dari pertanyaannya — apakah database pernah harus memotong isi sel itu untuk menjawab “nomor mana yang cocok?” Tiga wujud pelanggaran yang paling sering ditemui:
| Wujud pelanggaran | Contoh nyata | Gejala khas |
|---|---|---|
| daftar dipisah koma dalam satu kolom | '0812-1111-2222, 0813-3333-4444' | LIKE salah sasaran, agregasi butuh bedah string |
| kolom berulang (repeating group) | telepon1, telepon2, telepon3 | pencarian memakai OR berantai, jumlah kolom terpasung |
| array yang bersembunyi | satu kolom menyimpan daftar berlabel | elemen tidak bisa di-join, dihitung, atau dijaga unik |
Pelanggaran: Daftar Dipisah Koma dalam Satu Kolom #
Tabel berikut adalah hasil impor formulir lama apa adanya. Dua kolomnya — telepon dan minat — masing-masing menyimpan daftar, bukan nilai:
-- Tabel kontak lama Toko Maju Jaya: hasil impor formulir pendaftaran
-- yang meminta "semua nomor telepon, pisahkan dengan koma" dan
-- "minat belanja, pisahkan dengan koma".
CREATE TABLE pelanggan_lama (
id integer PRIMARY KEY,
nama text NOT NULL,
telepon text NOT NULL,
minat text NOT NULL
);
INSERT INTO pelanggan_lama (id, nama, telepon, minat) VALUES
(1, 'Budi Santoso', '0812-1111-2222, 0813-3333-4444', 'alat tulis, kertas'),
(2, 'Siti Rahayu', '0813-3333-4555', 'kertas'),
(3, 'Andi Wijaya', '0815-9999-8888', 'alat tulis, buku');
SELECT id, nama, telepon, minat
FROM pelanggan_lama
ORDER BY id;
-- Petugas mencari pemilik nomor 0813-3333-4444 dengan LIKE:
-- pola '%0813-3333-4%' menangkap SEMUA nomor yang memuat potongan itu,
-- termasuk nomor Siti yang hanya berbagi awalan.
SELECT id, nama
FROM pelanggan_lama
WHERE telepon LIKE '%0813-3333-4%'
ORDER BY id;
-- Bedah string pun berjebakan: string_to_array memotong pada koma,
-- tapi elemen kedua masih menyimpan spasi di depannya.
SELECT id, string_to_array(telepon, ',') AS daftar_telepon
FROM pelanggan_lama
ORDER BY id;
-- Akibat spasi itu, pencocokan persis lewat = ANY gagal diam-diam:
-- tidak ada satu pun baris yang dianggap cocok.
SELECT id, nama
FROM pelanggan_lama
WHERE '0813-3333-4444' = ANY (string_to_array(telepon, ','))
ORDER BY id;
-- Setelah setiap elemen dirapikan dengan trim, pencocokan baru bekerja --
-- tiga lapis pembedah string hanya untuk satu pencarian nomor.
SELECT id, nama
FROM pelanggan_lama
WHERE '0813-3333-4444' = ANY (
SELECT trim(elemen)
FROM unnest(string_to_array(telepon, ',')) AS elemen
)
ORDER BY id;Output yang Dihasilkan:
CREATE TABLE
INSERT 0 3
id | nama | telepon | minat
----+--------------+--------------------------------+--------------------
1 | Budi Santoso | 0812-1111-2222, 0813-3333-4444 | alat tulis, kertas
2 | Siti Rahayu | 0813-3333-4555 | kertas
3 | Andi Wijaya | 0815-9999-8888 | alat tulis, buku
(3 rows)
id | nama
----+--------------
1 | Budi Santoso
2 | Siti Rahayu
(2 rows)
id | daftar_telepon
----+------------------------------------
1 | {0812-1111-2222," 0813-3333-4444"}
2 | {0813-3333-4555}
3 | {0815-9999-8888}
(3 rows)
id | nama
----+------
(0 rows)
id | nama
----+--------------
1 | Budi Santoso
(1 row)Empat query berikutnya adalah bukti rusaknya tabel itu, dan masing-masing rusak dengan cara berbeda:
WHERE telepon LIKE '%0813-3333-4%'— query mencari pemilik0813-3333-4444(nomor Budi), tetapi hasilnya mengembalikan dua baris: Budi dan Siti. Nomor Siti0813-3333-4555ikut tertangkap karena memuat potongan0813-3333-4sebagai bagian dari deretan karakternya. Pencarian substring tidak mengerti batas antarnomor.string_to_array(telepon, ',')— memotong sel menjadi array, tetapi hasilnya{0812-1111-2222," 0813-3333-4444"}: elemen kedua masih membawa satu spasi di depannya karena pemisah aslinya adalah koma plus spasi.'0813-3333-4444' = ANY (string_to_array(...))— pencocokan persis gagal total: nol baris. Spasi tunggal itu cukup untuk membuat' 0813-3333-4444'berbeda dari'0813-3333-4444', dan kegagalannya sunyi tanpa error.trimdi dalamunnest— baris Budi akhirnya ditemukan, dengan harga tiga lapis pembedah string yang harus ditulis ulang di setiap query yang menyentuh kolomtelepon.
Pelanggaran: Kolom Telepon yang Berulang #
Bentuk kedua tampak seperti perbaikan: pemilik toko membuat tiga kolom terpisah. Setiap sel kini benar-benar satu nomor, tetapi repeating group melanggar 1NF lewat pintu belakang — jenis fakta yang sama dipaksa menyebar ke banyak kolom:
-- Variasi kedua pelanggaran 1NF: kolom berulang (repeating groups).
-- Pemilik toko "menyelesaikan" masalah banyak nomor dengan tiga kolom.
CREATE TABLE pelanggan_kolom (
id integer PRIMARY KEY,
nama text NOT NULL,
telepon1 text,
telepon2 text,
telepon3 text
);
INSERT INTO pelanggan_kolom (id, nama, telepon1, telepon2, telepon3) VALUES
(1, 'Budi Santoso', '0812-1111-2222', '0813-3333-4444', NULL),
(2, 'Siti Rahayu', '0813-3333-4555', NULL, NULL);
SELECT id, nama, telepon1, telepon2, telepon3
FROM pelanggan_kolom
ORDER BY id;
-- Pencarian satu nomor kini harus menyebut ketiga kolom satu per satu;
-- lupa satu kolom berarti lupa satu tempat penyimpanan.
SELECT id, nama
FROM pelanggan_kolom
WHERE telepon1 = '0813-3333-4444'
OR telepon2 = '0813-3333-4444'
OR telepon3 = '0813-3333-4444'
ORDER BY id;
-- "Berapa nomor yang tersimpan untuk tiap pelanggan?" terjemahannya:
-- jumlahkan tiga kolom yang tidak kosong. Tambah telepon4, dan seluruh
-- query seperti ini harus ditulis ulang.
SELECT id, nama,
(telepon1 IS NOT NULL)::int
+ (telepon2 IS NOT NULL)::int
+ (telepon3 IS NOT NULL)::int AS jumlah_nomor
FROM pelanggan_kolom
ORDER BY id;Output yang Dihasilkan:
CREATE TABLE
INSERT 0 2
id | nama | telepon1 | telepon2 | telepon3
----+--------------+----------------+----------------+----------
1 | Budi Santoso | 0812-1111-2222 | 0813-3333-4444 |
2 | Siti Rahayu | 0813-3333-4555 | |
(2 rows)
id | nama
----+--------------
1 | Budi Santoso
(1 row)
id | nama | jumlah_nomor
----+--------------+--------------
1 | Budi Santoso | 2
2 | Siti Rahayu | 1
(2 rows)Dua query terakhir memperlihatkan pola penyakitnya. Pencarian satu nomor harus menyebut tiga kolom dengan rantai OR — satu kolom yang terlupakan berarti satu tempat penyimpanan yang tidak diperiksa. Menghitung jumlah nomor berubah menjadi penjumlahan tiga IS NOT NULL yang dikonversi ke integer. Batas tiga nomor juga dipaku permanen: pelanggan dengan nomor keempat menuntut ALTER TABLE menambah kolom baru, lalu setiap query OR berantai dan setiap laporan yang menyebut telepon1 sampai telepon3 harus dikejar satu per satu.
Perbaikan Struktural: Nilai Banyak Menjadi Baris #
Obat pertama untuk data bernilai-banyak adalah relasi satu-ke-banyak yang sudah kita kuasai: tabel telepon berdiri sendiri, setiap nomor menjadi satu baris, terikat pelanggan_id sebagai foreign key:
-- Perbaikan pertama: data bernilai-banyak pindah menjadi baris di tabelnya
-- sendiri, terikat foreign key ke pelanggan.
CREATE TABLE pelanggan (
id integer PRIMARY KEY,
nama text NOT NULL
);
INSERT INTO pelanggan (id, nama) VALUES
(1, 'Budi Santoso'),
(2, 'Siti Rahayu'),
(3, 'Andi Wijaya'),
(4, 'Dewi Lestari');
CREATE TABLE telepon (
pelanggan_id integer NOT NULL REFERENCES pelanggan (id),
jenis text NOT NULL,
nomor text NOT NULL,
PRIMARY KEY (pelanggan_id, nomor)
);
INSERT INTO telepon (pelanggan_id, jenis, nomor) VALUES
(1, 'rumah', '0812-1111-2222'),
(1, 'pribadi', '0813-3333-4444'),
(2, 'pribadi', '0813-3333-4555'),
(3, 'kantor', '0815-9999-8888');
-- Pencarian nomor menjadi pencocokan persis pada satu kolom;
-- tidak ada lagi LIKE, tidak ada lagi spasi tersembunyi.
SELECT p.nama, t.jenis, t.nomor
FROM pelanggan p
JOIN telepon t ON t.pelanggan_id = p.id
WHERE t.nomor = '0813-3333-4444'
ORDER BY p.id;
-- "Berapa nomor per pelanggan?" menjadi COUNT biasa;
-- LEFT JOIN memastikan pelanggan tanpa nomor tetap terhitung nol.
SELECT p.id, p.nama, count(t.nomor) AS jumlah_nomor
FROM pelanggan p
LEFT JOIN telepon t ON t.pelanggan_id = p.id
GROUP BY p.id, p.nama
ORDER BY p.id;
-- Pelanggan keempat mendaftar dengan nomor keempat jenisnya sendiri:
-- tanpa ALTER TABLE, tanpa query yang ditulis ulang.
INSERT INTO telepon (pelanggan_id, jenis, nomor)
VALUES (4, 'darurat', '0817-2222-3333');
SELECT p.nama, t.jenis, t.nomor
FROM pelanggan p
JOIN telepon t ON t.pelanggan_id = p.id
ORDER BY p.id, t.nomor;Output yang Dihasilkan:
CREATE TABLE
INSERT 0 4
CREATE TABLE
INSERT 0 4
nama | jenis | nomor
--------------+---------+----------------
Budi Santoso | pribadi | 0813-3333-4444
(1 row)
id | nama | jumlah_nomor
----+--------------+--------------
1 | Budi Santoso | 2
2 | Siti Rahayu | 1
3 | Andi Wijaya | 1
4 | Dewi Lestari | 0
(4 rows)
INSERT 0 1
nama | jenis | nomor
--------------+---------+----------------
Budi Santoso | rumah | 0812-1111-2222
Budi Santoso | pribadi | 0813-3333-4444
Siti Rahayu | pribadi | 0813-3333-4555
Andi Wijaya | kantor | 0815-9999-8888
Dewi Lestari | darurat | 0817-2222-3333
(5 rows)Tiga query penutup menuntaskan tiga penyakit lama sekaligus. Pencarian nomor menjadi pencocokan persis t.nomor = '0813-3333-4444' — satu kolom, tanpa LIKE, tanpa spasi tersembunyi. Jumlah nomor per pelanggan menjadi count(t.nomor) dengan LEFT JOIN, dan pelanggan tanpa nomor pun tetap muncul dengan angka nol. Insert terakhir menambah nomor jenis darurat untuk Dewi tanpa menyentuh struktur tabel sama sekali: baris baru, bukan kolom baru. Bonus yang tidak kalah penting: kolom jenis kini ada tempatnya — hal mustahil pada daftar koma, yang tidak punya cara menyimpan label per elemen.
Nuansa Array PostgreSQL #
PostgreSQL menyediakan jalan tengah yang membuat diskusi 1NF menjadi lebih menarik: tipe text[] yang menyimpan daftar dalam satu kolom. Secara tampilan ini mirip pelanggaran — satu sel berisi banyak nilai — tetapi array berbeda secara fundamental dari string berkoma: array punya struktur internal yang dikenali server, operator khusus (unnest, @>, ANY), dan bisa diindeks. Untuk data seperti topik minat yang diperlakukan sebagai satu kesatuan, array adalah pilihan yang sah:
-- Perbaikan kedua: minat belanja disimpan sebagai array PostgreSQL.
-- Tiap sel tetap berisi satu NILAI -- sebuah himpunan topik yang diperlakukan
-- sebagai satu kesatuan, dengan operator khusus untuk memeriksanya.
ALTER TABLE pelanggan ADD COLUMN minat text[];
UPDATE pelanggan SET minat = ARRAY['alat tulis', 'kertas'] WHERE id = 1;
UPDATE pelanggan SET minat = ARRAY['kertas'] WHERE id = 2;
UPDATE pelanggan SET minat = ARRAY['alat tulis', 'buku'] WHERE id = 3;
UPDATE pelanggan SET minat = ARRAY['kertas', 'kertas'] WHERE id = 4;
SELECT id, nama, minat
FROM pelanggan
ORDER BY id;
-- unnest membongkar array menjadi baris saat detailnya dibutuhkan.
SELECT id, nama, unnest(minat) AS topik
FROM pelanggan
ORDER BY id, topik;
-- Operator @> menjawab "pelanggan ini punya minat kertas?" tanpa bedah string.
SELECT id, nama
FROM pelanggan
WHERE minat @> ARRAY['kertas']
ORDER BY id;
-- Agregasi tetap sederhana: berapa pelanggan menyukai 'alat tulis'?
SELECT count(*) AS penyuka_alat_tulis
FROM pelanggan
WHERE 'alat tulis' = ANY (minat);Output yang Dihasilkan:
ALTER TABLE
UPDATE 1
UPDATE 1
UPDATE 1
UPDATE 1
id | nama | minat
----+--------------+-----------------------
1 | Budi Santoso | {"alat tulis",kertas}
2 | Siti Rahayu | {kertas}
3 | Andi Wijaya | {"alat tulis",buku}
4 | Dewi Lestari | {kertas,kertas}
(4 rows)
id | nama | topik
----+--------------+------------
1 | Budi Santoso | alat tulis
1 | Budi Santoso | kertas
2 | Siti Rahayu | kertas
3 | Andi Wijaya | alat tulis
3 | Andi Wijaya | buku
4 | Dewi Lestari | kertas
4 | Dewi Lestari | kertas
(7 rows)
id | nama
----+--------------
1 | Budi Santoso
2 | Siti Rahayu
4 | Dewi Lestari
(3 rows)
penyuka_alat_tulis
--------------------
2
(1 row)Perhatikan urutan operasinya. unnest(minat) membongkar array menjadi baris tanpa pembedah string dan tanpa jebakan spasi — server tahu persis di mana batas antarelemen. Operator minat @> ARRAY['kertas'] menjawab pertanyaan keanggotaan langsung dari server. Namun hasil query terakhir menyimpan satu fakta yang tidak nyaman: UPDATE untuk pelanggan 4 sengaja memasukkan ARRAY['kertas', 'kertas'], dan server menerimanya tanpa protes — array tidak menjaga keunikan elemen, tidak mendukung foreign key per elemen, dan elemennya tidak bisa di-join sebagai tabel.
Kapan Array Cukup, Kapan Junction Table #
Ujian praktisnya satu kalimat: ketika hubungan mulai penting, pindahkan elemen ke junction table. Topik minat yang hanya dipakai untuk filter cukup tinggal di array; begitu topik mau dihitung, di-join ke tabel topik induk, dijaga unik, atau diberi atribut tambahan — pindahkan ke baris. File berikut memigrasikan kolom minat menjadi tabel minat_pelanggan dengan composite primary key:
-- Batas array: ketika elemen daftar mulai punya identitas relasional,
-- tempat yang benar adalah junction table, bukan satu kolom.
CREATE TABLE minat_pelanggan (
pelanggan_id integer NOT NULL REFERENCES pelanggan (id),
topik text NOT NULL,
PRIMARY KEY (pelanggan_id, topik)
);
-- Migrasi array menjadi baris: unnest membongkar isi kolom,
-- DISTINCT menyapu bersih duplikat 'kertas' milik Dewi.
INSERT INTO minat_pelanggan (pelanggan_id, topik)
SELECT DISTINCT id, unnest(minat) AS topik
FROM pelanggan
ORDER BY id, topik;
SELECT pelanggan_id, topik
FROM minat_pelanggan
ORDER BY pelanggan_id, topik;
-- Pertanyaan agregasi per topik kini query biasa pada tabel biasa.
SELECT topik, count(*) AS peminat
FROM minat_pelanggan
GROUP BY topik
ORDER BY topik;
-- Join lintas tabel menjadi mungkin: siapa saja penyuka kertas?
SELECT p.nama, mp.topik
FROM pelanggan p
JOIN minat_pelanggan mp ON mp.pelanggan_id = p.id
WHERE mp.topik = 'kertas'
ORDER BY p.id;Output yang Dihasilkan:
CREATE TABLE
INSERT 0 6
pelanggan_id | topik
--------------+------------
1 | alat tulis
1 | kertas
2 | kertas
3 | alat tulis
3 | buku
4 | kertas
(6 rows)
topik | peminat
------------+---------
alat tulis | 2
buku | 1
kertas | 3
(3 rows)
nama | topik
--------------+--------
Budi Santoso | kertas
Siti Rahayu | kertas
Dewi Lestari | kertas
(3 rows)Migrasinya membuktikan dua hal sekaligus. unnest membongkar array menjadi kandidat baris, dan DISTINCT menyapu bersih duplikat kertas milik Dewi sehingga tabel hasil migrasi bersih sejak lahir — enam nilai array menjadi empat baris unik. Setelah menjadi tabel, semua operasi relational tersedia: agregasi count(*) per topik, join ke pelanggan, dan penjagaan keunikan. Penjagaan keunikan yang terakhir itulah yang dicoba dipecahkan file berikut:
-- Junction table menolak pasangan ganda lewat composite primary key --
-- penolakan yang tidak pernah diberikan oleh array.
INSERT INTO minat_pelanggan (pelanggan_id, topik)
VALUES (1, 'kertas');Output yang Dihasilkan:
psql:tolak-duplikat-topik.sql:4: ERROR: duplicate key value violates unique constraint "minat_pelanggan_pkey"
DETAIL: Key (pelanggan_id, topik)=(1, kertas) already exists.Server menolak insert dengan error duplicate key value violates unique constraint "minat_pelanggan_pkey" karena pasangan (1, 'kertas') sudah tercatat — penolakan yang sama sekali tidak ada pada versi array, yang menerima 'kertas' berkali-kali tanpa suara. Perbandingkan ketiga rumah untuk data bernilai-banyak:
| Aspek | String berkoma | Array text[] | Junction table |
|---|---|---|---|
| pencarian satu elemen | LIKE rawan salah sasaran | operator @>, ANY | query biasa + index |
| keunikan elemen | tidak dijaga | tidak dijaga | composite primary key |
| atribut per elemen | mustahil | mustahil | kolom tambahan di tabel |
| foreign key per elemen | mustahil | mustahil | mendukung penuh |
| cocok untuk | tidak pernah | tag dan label sederhana | relasi yang penting |
Daftar Uji 1NF #
Normalisasi bekerja seperti inspeksi: jalankan daftar periksa berikut pada setiap tabel yang Anda rancang, dan tabel yang lolos semua pertanyaan sudah berdiri di 1NF:
| Pertanyaan uji | Lolos bila |
|---|---|
| Apakah setiap sel bisa diperiksa tanpa memotong isinya? | tidak ada LIKE, split_part, atau string_to_array untuk mencari satu nilai |
| Apakah jenis fakta yang sama hanya punya satu kolom? | tidak ada telepon1..teleponN atau kolom berujung angka |
| Apakah satu nilai punya satu tempat pencarian? | query tidak butuh rantai OR lintas kolom sejenis |
| Apakah elemen daftar punya kebutuhan relasional? | bila ya, elemen sudah menjadi baris tabel, bukan isi array |
Tabel pelanggan_lama gagal di tiga pertanyaan pertama; hasil akhirnya — pelanggan, telepon, dan minat_pelanggan — lolos keempatnya. Namun 1NF baru memeriksa isi sel; 1NF belum bertanya bagaimana kolom-kolom bergantung pada kunci tabel. Ketika kuncinya composite — dua kolom yang menjadi satu primary key — kolom non-kunci bisa bersandar hanya pada separuh kunci, dan ketergantungan parsial itulah lawan berikutnya di chapter Second Normal Form
.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
batas-array.sql
Unduh berkas-- Batas array: ketika elemen daftar mulai punya identitas relasional,
-- tempat yang benar adalah junction table, bukan satu kolom.
CREATE TABLE minat_pelanggan (
pelanggan_id integer NOT NULL REFERENCES pelanggan (id),
topik text NOT NULL,
PRIMARY KEY (pelanggan_id, topik)
);
-- Migrasi array menjadi baris: unnest membongkar isi kolom,
-- DISTINCT menyapu bersih duplikat 'kertas' milik Dewi.
INSERT INTO minat_pelanggan (pelanggan_id, topik)
SELECT DISTINCT id, unnest(minat) AS topik
FROM pelanggan
ORDER BY id, topik;
SELECT pelanggan_id, topik
FROM minat_pelanggan
ORDER BY pelanggan_id, topik;
-- Pertanyaan agregasi per topik kini query biasa pada tabel biasa.
SELECT topik, count(*) AS peminat
FROM minat_pelanggan
GROUP BY topik
ORDER BY topik;
-- Join lintas tabel menjadi mungkin: siapa saja penyuka kertas?
SELECT p.nama, mp.topik
FROM pelanggan p
JOIN minat_pelanggan mp ON mp.pelanggan_id = p.id
WHERE mp.topik = 'kertas'
ORDER BY p.id;
kolom-berulang.sql
Unduh berkas-- Variasi kedua pelanggaran 1NF: kolom berulang (repeating groups).
-- Pemilik toko "menyelesaikan" masalah banyak nomor dengan tiga kolom.
CREATE TABLE pelanggan_kolom (
id integer PRIMARY KEY,
nama text NOT NULL,
telepon1 text,
telepon2 text,
telepon3 text
);
INSERT INTO pelanggan_kolom (id, nama, telepon1, telepon2, telepon3) VALUES
(1, 'Budi Santoso', '0812-1111-2222', '0813-3333-4444', NULL),
(2, 'Siti Rahayu', '0813-3333-4555', NULL, NULL);
SELECT id, nama, telepon1, telepon2, telepon3
FROM pelanggan_kolom
ORDER BY id;
-- Pencarian satu nomor kini harus menyebut ketiga kolom satu per satu;
-- lupa satu kolom berarti lupa satu tempat penyimpanan.
SELECT id, nama
FROM pelanggan_kolom
WHERE telepon1 = '0813-3333-4444'
OR telepon2 = '0813-3333-4444'
OR telepon3 = '0813-3333-4444'
ORDER BY id;
-- "Berapa nomor yang tersimpan untuk tiap pelanggan?" terjemahannya:
-- jumlahkan tiga kolom yang tidak kosong. Tambah telepon4, dan seluruh
-- query seperti ini harus ditulis ulang.
SELECT id, nama,
(telepon1 IS NOT NULL)::int
+ (telepon2 IS NOT NULL)::int
+ (telepon3 IS NOT NULL)::int AS jumlah_nomor
FROM pelanggan_kolom
ORDER BY id;
pelanggaran-1nf.sql
Unduh berkas-- Tabel kontak lama Toko Maju Jaya: hasil impor formulir pendaftaran
-- yang meminta "semua nomor telepon, pisahkan dengan koma" dan
-- "minat belanja, pisahkan dengan koma".
CREATE TABLE pelanggan_lama (
id integer PRIMARY KEY,
nama text NOT NULL,
telepon text NOT NULL,
minat text NOT NULL
);
INSERT INTO pelanggan_lama (id, nama, telepon, minat) VALUES
(1, 'Budi Santoso', '0812-1111-2222, 0813-3333-4444', 'alat tulis, kertas'),
(2, 'Siti Rahayu', '0813-3333-4555', 'kertas'),
(3, 'Andi Wijaya', '0815-9999-8888', 'alat tulis, buku');
SELECT id, nama, telepon, minat
FROM pelanggan_lama
ORDER BY id;
-- Petugas mencari pemilik nomor 0813-3333-4444 dengan LIKE:
-- pola '%0813-3333-4%' menangkap SEMUA nomor yang memuat potongan itu,
-- termasuk nomor Siti yang hanya berbagi awalan.
SELECT id, nama
FROM pelanggan_lama
WHERE telepon LIKE '%0813-3333-4%'
ORDER BY id;
-- Bedah string pun berjebakan: string_to_array memotong pada koma,
-- tapi elemen kedua masih menyimpan spasi di depannya.
SELECT id, string_to_array(telepon, ',') AS daftar_telepon
FROM pelanggan_lama
ORDER BY id;
-- Akibat spasi itu, pencocokan persis lewat = ANY gagal diam-diam:
-- tidak ada satu pun baris yang dianggap cocok.
SELECT id, nama
FROM pelanggan_lama
WHERE '0813-3333-4444' = ANY (string_to_array(telepon, ','))
ORDER BY id;
-- Setelah setiap elemen dirapikan dengan trim, pencocokan baru bekerja --
-- tiga lapis pembedah string hanya untuk satu pencarian nomor.
SELECT id, nama
FROM pelanggan_lama
WHERE '0813-3333-4444' = ANY (
SELECT trim(elemen)
FROM unnest(string_to_array(telepon, ',')) AS elemen
)
ORDER BY id;
perbaikan-array.sql
Unduh berkas-- Perbaikan kedua: minat belanja disimpan sebagai array PostgreSQL.
-- Tiap sel tetap berisi satu NILAI -- sebuah himpunan topik yang diperlakukan
-- sebagai satu kesatuan, dengan operator khusus untuk memeriksanya.
ALTER TABLE pelanggan ADD COLUMN minat text[];
UPDATE pelanggan SET minat = ARRAY['alat tulis', 'kertas'] WHERE id = 1;
UPDATE pelanggan SET minat = ARRAY['kertas'] WHERE id = 2;
UPDATE pelanggan SET minat = ARRAY['alat tulis', 'buku'] WHERE id = 3;
UPDATE pelanggan SET minat = ARRAY['kertas', 'kertas'] WHERE id = 4;
SELECT id, nama, minat
FROM pelanggan
ORDER BY id;
-- unnest membongkar array menjadi baris saat detailnya dibutuhkan.
SELECT id, nama, unnest(minat) AS topik
FROM pelanggan
ORDER BY id, topik;
-- Operator @> menjawab "pelanggan ini punya minat kertas?" tanpa bedah string.
SELECT id, nama
FROM pelanggan
WHERE minat @> ARRAY['kertas']
ORDER BY id;
-- Agregasi tetap sederhana: berapa pelanggan menyukai 'alat tulis'?
SELECT count(*) AS penyuka_alat_tulis
FROM pelanggan
WHERE 'alat tulis' = ANY (minat);
perbaikan-baris.sql
Unduh berkas-- Perbaikan pertama: data bernilai-banyak pindah menjadi baris di tabelnya
-- sendiri, terikat foreign key ke pelanggan.
CREATE TABLE pelanggan (
id integer PRIMARY KEY,
nama text NOT NULL
);
INSERT INTO pelanggan (id, nama) VALUES
(1, 'Budi Santoso'),
(2, 'Siti Rahayu'),
(3, 'Andi Wijaya'),
(4, 'Dewi Lestari');
CREATE TABLE telepon (
pelanggan_id integer NOT NULL REFERENCES pelanggan (id),
jenis text NOT NULL,
nomor text NOT NULL,
PRIMARY KEY (pelanggan_id, nomor)
);
INSERT INTO telepon (pelanggan_id, jenis, nomor) VALUES
(1, 'rumah', '0812-1111-2222'),
(1, 'pribadi', '0813-3333-4444'),
(2, 'pribadi', '0813-3333-4555'),
(3, 'kantor', '0815-9999-8888');
-- Pencarian nomor menjadi pencocokan persis pada satu kolom;
-- tidak ada lagi LIKE, tidak ada lagi spasi tersembunyi.
SELECT p.nama, t.jenis, t.nomor
FROM pelanggan p
JOIN telepon t ON t.pelanggan_id = p.id
WHERE t.nomor = '0813-3333-4444'
ORDER BY p.id;
-- "Berapa nomor per pelanggan?" menjadi COUNT biasa;
-- LEFT JOIN memastikan pelanggan tanpa nomor tetap terhitung nol.
SELECT p.id, p.nama, count(t.nomor) AS jumlah_nomor
FROM pelanggan p
LEFT JOIN telepon t ON t.pelanggan_id = p.id
GROUP BY p.id, p.nama
ORDER BY p.id;
-- Pelanggan keempat mendaftar dengan nomor keempat jenisnya sendiri:
-- tanpa ALTER TABLE, tanpa query yang ditulis ulang.
INSERT INTO telepon (pelanggan_id, jenis, nomor)
VALUES (4, 'darurat', '0817-2222-3333');
SELECT p.nama, t.jenis, t.nomor
FROM pelanggan p
JOIN telepon t ON t.pelanggan_id = p.id
ORDER BY p.id, t.nomor;
tolak-duplikat-topik.sql
Unduh berkas-- Junction table menolak pasangan ganda lewat composite primary key --
-- penolakan yang tidak pernah diberikan oleh array.
INSERT INTO minat_pelanggan (pelanggan_id, topik)
VALUES (1, 'kertas');