Lima chapter sebelumnya membangun relasi antartabel satu per satu, dan setiap kali polanya sama: satu fakta diletakkan di satu tempat, lalu dihubungkan lewat rujukan. Prinsip itu punya nama resmi — normalisasi — dan alasan yang lebih tua dari PostgreSQL itu sendiri: menghilangkan redundansi dan anomali. Redundansi berarti satu fakta tersimpan berkali-kali; anomali berarti redundansi itu mulai melawan pemiliknya, lewat tiga cara klasik yang akan kita pancing satu per satu dengan data nyata. Chapter ini membuka seri normalisasi: tujuannya dijabarkan, ketiga anomalinya dibuktikan pada tabel datar milik pemilik Toko Maju Jaya, lalu tabel itu dibedah menjadi tiga tabel yang saling merujuk — bentuk normal yang menjadi titik berangkat chapter 1NF, 2NF, dan 3NF berikutnya.

Tujuan Normalisasi #

Normalisasi adalah proses menyusun ulang struktur tabel supaya setiap fakta hidup tepat satu kali. Dua tujuannya saling mengunci:

TujuanArtinya praktisGejala bila dilanggar
tanpa redundansisatu fakta tersimpan di satu tempat sajadata sama berulang di banyak baris
tanpa anomaliinsert, update, delete tidak merusak fakta lainoperasi wajar mengubah atau menghapus fakta yang tidak disentuh

Kata “fakta” di sini perlu dibaca sempit: harga Buku Tulis adalah satu fakta; kota Siti adalah satu fakta; kuantitas Buku Tulis pada pesanan tertentu adalah satu fakta tersendiri. Tabel yang menyimpan harga Buku Tulis di banyak baris menyimpan satu fakta berkali-kali — dan mulai detik itu, setiap perubahan harga harus mengejar semua salinannya. Uji paling jujur untuk desain Anda: jalankan tiga operasi paling wajar (tambah produk baru, ubah satu harga, hapus satu pesanan) dan lihat apakah ada fakta lain yang ikut rusak.

Meja Peraga: Tabel Datar Toko Maju Jaya #

Sebelum normal, ada dulu yang datar. Pemilik toko dulu mencatat semua penjualan di satu tabel buku besar — setiap baris satu pesanan, lengkap dengan nama pelanggan, kotanya, produk, dan harganya:

sql buat-flat.sql
-- Catatan lama pemilik Toko Maju Jaya: satu tabel datar untuk semuanya.
-- Setiap baris = satu pesanan; data pelanggan dan produk ikut tersimpan
-- berulang di dalamnya.
CREATE TABLE pesanan_flat (
    id_baris       integer PRIMARY KEY,
    nama_pelanggan text NOT NULL,
    kota           text NOT NULL,
    tanggal        date NOT NULL,
    nama_produk    text NOT NULL,
    harga          numeric(12, 2) NOT NULL,
    kuantitas      integer NOT NULL
);

INSERT INTO pesanan_flat (id_baris, nama_pelanggan, kota, tanggal,
                          nama_produk, harga, kuantitas) VALUES
    (1, 'Budi Santoso', 'Bandung', DATE '2026-01-05', 'Buku Tulis', 4500.00, 10),
    (2, 'Siti Rahayu',  'Jakarta', DATE '2026-01-09', 'Buku Tulis', 4500.00,  2),
    (3, 'Siti Rahayu',  'Jakarta', DATE '2026-02-11', 'Penggaris',  2500.00,  1);

SELECT id_baris, nama_pelanggan, kota, tanggal, nama_produk, harga, kuantitas
FROM pesanan_flat
ORDER BY id_baris;

Output yang Dihasilkan:

text
CREATE TABLE
INSERT 0 3
 id_baris | nama_pelanggan |  kota   |  tanggal   | nama_produk |  harga  | kuantitas 
----------+----------------+---------+------------+-------------+---------+-----------
        1 | Budi Santoso   | Bandung | 2026-01-05 | Buku Tulis  | 4500.00 |        10
        2 | Siti Rahayu    | Jakarta | 2026-01-09 | Buku Tulis  | 4500.00 |         2
        3 | Siti Rahayu    | Jakarta | 2026-02-11 | Penggaris   | 2500.00 |         1
(3 rows)

Tiga baris itu tampak rapi dan mudah dibaca — dan di situlah bahayanya. Dua redundansi sudah bersarang: data Siti Rahayu dan Jakarta tersimpan dua kali (baris 2 dan 3), dan fakta “Buku Tulis berharga 4500.00” tersimpan dua kali (baris 1 dan 2). Tidak ada yang error hari ini. Tiga subbab berikutnya memancing tiga anomali itu keluar satu per satu, masing-masing dengan operasi yang paling wajar terjadi di toko.

Anomali Insert: Barang Baru Tak Punya Rumah #

Stabilo baru tiba di gudang Toko Maju Jaya. Belum ada pembelinya — wajar, barang memang didata sebelum dijual. Tabel datar tidak punya tempat untuknya:

-- Anomali insert: Stabilo baru tiba di gudang, belum ada pembelinya.
-- Tabel datar tidak punya tempat untuk mencatat barang tanpa transaksi.
INSERT INTO pesanan_flat (id_baris, nama_pelanggan, kota, tanggal,
                          nama_produk, harga, kuantitas)
VALUES (4, NULL, NULL, NULL, 'Stabilo', 6000.00, 0);

Output yang Dihasilkan:

text
psql:anomali-insert.sql:5: ERROR:  null value in column "nama_pelanggan" of relation "pesanan_flat" violates not-null constraint
DETAIL:  Failing row contains (4, null, null, null, Stabilo, 6000.00, 0).

Server menolak baris keempat karena kolom nama_pelanggan dan tanggal dideklarasikan NOT NULL, sementara insert ini memang tidak punya nilai untuk keduanya — Stabilo lahir tanpa pelanggan dan tanpa transaksi. Inilah anomali insert: data yang sah secara bisnis tidak bisa dimasukkan tanpa mengarang data palsu. Jalan keluar yang dipakai pemilik toko dulu biasanya lebih buruk lagi: Stabilo dicatat dengan pelanggan fiktif dan tanggal dipungut dari pesanan lain — dan kini laporan penjualan menghitung transaksi yang tidak pernah terjadi.

Anomali Update: Satu Fakta, Dua Jawaban #

Harga Buku Tulis naik menjadi 5000.00. Petugas membuka tabel, mengubah harga pada satu baris yang ia lihat lebih dulu:

-- Anomali update: harga Buku Tulis naik, petugas hanya mengubah
-- satu baris. Baris lain yang memuat harga lama terlupakan.
UPDATE pesanan_flat SET harga = 5000.00 WHERE id_baris = 1;

SELECT id_baris, nama_pelanggan, nama_produk, harga
FROM pesanan_flat
ORDER BY id_baris;

-- "Berapa harga Buku Tulis?" kini punya dua jawaban yang bertentangan.
SELECT nama_produk, MIN(harga) AS termurah, MAX(harga) AS termahal
FROM pesanan_flat
WHERE nama_produk = 'Buku Tulis'
GROUP BY nama_produk;

Output yang Dihasilkan:

text
UPDATE 1
 id_baris | nama_pelanggan | nama_produk |  harga  
----------+----------------+-------------+---------
        1 | Budi Santoso   | Buku Tulis  | 5000.00
        2 | Siti Rahayu    | Buku Tulis  | 4500.00
        3 | Siti Rahayu    | Penggaris   | 2500.00
(3 rows)

 nama_produk | termurah | termahal 
-------------+----------+----------
 Buku Tulis  |  4500.00 |  5000.00
(1 row)

Query penutup adalah berita duniannya: MIN(harga) dan MAX(harga) untuk Buku Tulis mengembalikan dua nilai berbeda — 4500.00 dan 5000.00. Pertanyaan bisnis sesederhana “berapa harga Buku Tulis?” kini punya dua jawaban yang saling bertentangan, dan tidak ada kolom yang mencatat jawaban mana yang benar. Inilah anomali update, dan pesannya lebih tajam dari sekadar ketidaksengajaan petugas: pada tabel datar, mengubah satu fakta memang harus dilakukan di banyak baris sekaligus. Kegagalan bukan soal kalau, tapi kapan — satu koneksi lambat, satu petugas terputus di tengah proses, dan konsistensi rusak.

Anomali Delete: Pesanan Berakhir, Barang Ikut Lenyap #

Siti membatalkan pesanan Penggarisnya. Penghapusan satu baris transaksi yang sah ini membawa kabar duka bagi katalog:

-- Anomali delete: Siti membatalkan pesanan Penggaris.
-- Satu-satunya baris yang memuat data produk Penggaris ikut terhapus,
-- sehingga katalog toko "kehilangan" barang yang masih dijual.
DELETE FROM pesanan_flat WHERE id_baris = 3;

SELECT id_baris, nama_pelanggan, nama_produk, kuantitas
FROM pesanan_flat
ORDER BY id_baris;

Output yang Dihasilkan:

text
DELETE 1
 id_baris | nama_pelanggan | nama_produk | kuantitas 
----------+----------------+-------------+-----------
        1 | Budi Santoso   | Buku Tulis  |        10
        2 | Siti Rahayu    | Buku Tulis  |         2
(2 rows)

Sebelum dihapus, baris 3 adalah satu-satunya tempat menyimpan fakta bahwa toko menjual Penggaris seharga 2500.00. Setelah DELETE, fakta itu lenyap total: toko masih punya Penggaris di rak, tapi database tidak lagi tahu barang itu ada. Inilah anomali delete — data induk (katalog) ikut terkubur bersama data peristiwa (transaksi) karena keduanya tinggal serumah. Ketiga anomali kini lengkap terbukti, dan akar masalahnya satu: tabel datar memaksa fakta yang berbeda kehidupan berbagi baris yang sama.

Peta Bentuk Normal #

Normalisasi punya peta jalan berjenjang yang disebut bentuk normal (normal form). Setiap jenjang melarang satu jenis redundansi tertentu:

Bentuk normalLarangan intinyaSatu kalimat
1NFnilai berulang dalam satu selsatu sel satu nilai atomik; daftar jadi baris
2NFketergantungan parsial pada composite keyatribut non-kunci bergantung pada seluruh kunci
3NFketergantungan transitif antar kolom non-kunciatribut non-kunci bergantung hanya pada kunci

Tiga chapter berikutnya membedah masing-masing jenjang sampai tuntas dengan contoh pelanggaran dan perbaikannya. Tabel datar kita sebenarnya sudah lolos beberapa pemeriksaan: setiap sel memuat satu nilai dan setiap baris punya kunci tunggal id_baris, sehingga peta jalan kita dimulai dari masalah yang lebih mendasar lagi — pemisahan entitas — yang justru inti dekomposisi berikut ini.

Dekomposisi: Satu Tabel Menjadi Tiga #

Penyembuhan tabel datar dilakukan dengan memisahkan entitas yang berbeda ke tabelnya masing-masing, lalu mengikatnya dengan foreign key. File berikut memuat seluruh prosesnya: salinan bersih data lama, tiga tabel baru, migrasi data, sampai laporan akhir yang disusun ulang lewat join:

sql dekomposisi.sql
-- Dekomposisi: satu tabel datar menjadi tiga tabel yang saling merujuk.
-- Kita mulai dari salinan bersih pesanan_flat (kondisi sebelum anomali),
-- lalu memindahkan setiap fakta ke satu-satunya tempat tinggalnya.
DROP TABLE IF EXISTS pesanan_flat CASCADE;

CREATE TABLE pesanan_flat (
    id_baris       integer PRIMARY KEY,
    nama_pelanggan text NOT NULL,
    kota           text NOT NULL,
    tanggal        date NOT NULL,
    nama_produk    text NOT NULL,
    harga          numeric(12, 2) NOT NULL,
    kuantitas      integer NOT NULL
);

INSERT INTO pesanan_flat (id_baris, nama_pelanggan, kota, tanggal,
                          nama_produk, harga, kuantitas) VALUES
    (1, 'Budi Santoso', 'Bandung', DATE '2026-01-05', 'Buku Tulis', 4500.00, 10),
    (2, 'Siti Rahayu',  'Jakarta', DATE '2026-01-09', 'Buku Tulis', 4500.00,  2),
    (3, 'Siti Rahayu',  'Jakarta', DATE '2026-02-11', 'Penggaris',  2500.00,  1);

CREATE TABLE pelanggan (
    id    integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    nama  text NOT NULL,
    kota  text NOT NULL
);

CREATE TABLE produk (
    id    integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    nama  text NOT NULL,
    harga numeric(12, 2) NOT NULL
);

CREATE TABLE pesanan (
    id           integer PRIMARY KEY,
    tanggal      date NOT NULL,
    pelanggan_id integer NOT NULL REFERENCES pelanggan (id),
    produk_id    integer NOT NULL REFERENCES produk (id),
    kuantitas    integer NOT NULL
);

-- Migrasi pelanggan: DISTINCT mengeruk dua baris ganda menjadi satu.
INSERT INTO pelanggan (nama, kota)
SELECT DISTINCT nama_pelanggan, kota
FROM pesanan_flat
ORDER BY nama_pelanggan;

-- Migrasi produk: harga Buku Tulis yang tersimpan dua kali menjadi satu baris.
INSERT INTO produk (nama, harga)
SELECT DISTINCT nama_produk, harga
FROM pesanan_flat
ORDER BY nama_produk;

-- Migrasi pesanan: baris flat diterjemahkan menjadi rujukan id.
-- Join migrasi memakai pasangan kolom yang sama dengan DISTINCT di atas.
INSERT INTO pesanan (id, tanggal, pelanggan_id, produk_id, kuantitas)
SELECT f.id_baris, f.tanggal, pl.id, pr.id, f.kuantitas
FROM pesanan_flat f
JOIN pelanggan pl ON pl.nama = f.nama_pelanggan AND pl.kota = f.kota
JOIN produk pr ON pr.nama = f.nama_produk AND pr.harga = f.harga
ORDER BY f.id_baris;

-- Tabel lama pensiun setelah seluruh datanya berpindah.
DROP TABLE pesanan_flat;

-- Laporan yang tadinya dibaca dari satu tabel kini disusun ulang
-- lewat join -- dengan data pelanggan dan produk yang hanya hidup sekali.
SELECT ps.id, pl.nama AS nama_pelanggan, pl.kota, ps.tanggal,
       pr.nama AS produk, pr.harga, ps.kuantitas
FROM pesanan ps
JOIN pelanggan pl ON pl.id = ps.pelanggan_id
JOIN produk pr ON pr.id = ps.produk_id
ORDER BY ps.id;

Output yang Dihasilkan:

text
DROP TABLE
CREATE TABLE
INSERT 0 3
CREATE TABLE
CREATE TABLE
CREATE TABLE
INSERT 0 2
INSERT 0 2
INSERT 0 3
DROP TABLE
 id | nama_pelanggan |  kota   |  tanggal   |   produk   |  harga  | kuantitas 
----+----------------+---------+------------+------------+---------+-----------
  1 | Budi Santoso   | Bandung | 2026-01-05 | Buku Tulis | 4500.00 |        10
  2 | Siti Rahayu    | Jakarta | 2026-01-09 | Buku Tulis | 4500.00 |         2
  3 | Siti Rahayu    | Jakarta | 2026-02-11 | Penggaris  | 2500.00 |         1
(3 rows)

Tahap-tahap migrasi ini layak dibedah satu per satu:

  1. Tiga tabel dengan pembagian kerja yang jelas — pelanggan menyimpan fakta orang, produk menyimpan fakta barang, pesanan menyimpan fakta peristiwa beserta dua rujukannya. Id pelanggan dan produk dibangkitkan GENERATED ALWAYS AS IDENTITY karena migrasi tidak punya kunci alami yang bisa dipercaya.
  2. INSERT INTO pelanggan ... SELECT DISTINCT nama_pelanggan, kota — DISTINCT mengeruk dua baris Siti yang ganda menjadi satu. Dua insert migrasi pertama inilah yang secara harfiah menghapus redundansi.
  3. INSERT INTO pesanan ... JOIN pelanggan ... JOIN produk — tiap baris flat diterjemahkan menjadi id rujukan. Join migrasi memakai pasangan kolom yang sama dengan DISTINCT di atas (nama dan kota untuk pelanggan, nama dan harga untuk produk) supaya terjemahannya tidak ambigu.
  4. DROP TABLE pesanan_flat — tabel lama pensiun setelah seluruh datanya berpindah; mempertahankannya berarti memelihara dua sumber kebenaran.
  5. Laporan akhir disusun ulang lewat join — bentuknya identik dengan SELECT di buat-flat.sql, tapi kini data pelanggan dan produk hidup sekali di tempatnya masing-masing.

Laporan join terakhir juga menegaskan janji penting normalisasi: membaca data yang terpisah tidak lebih sulit — join yang sudah kita kuasai di section JOIN menyusunnya kembali menjadi tampilan yang sama persis.

Satu catatan desain untuk kejujuran: skema tiga tabel ini menyimpan harga hanya di produk, sehingga mengubah harga katalog menimpa harga yang dipakai seluruh riwayat pesanan. Desain produksi yang utuh menambahkan harga_saat_transaksi pada tabel item — pola snapshot yang dibangun di chapter Junction Table . Inti mekanisme normalisasi tidak berubah: snapshot adalah atribut milik peristiwa, jadi rumahnya tetap satu, di sisi peristiwa.

Uji Pulih: Ketiga Anomali Dicoba Lagi #

Ujian sesungguhnya adalah mengulangi ketiga skenario yang dulu merusak tabel datar — dan melihat semuanya gagal merusak:

sql uji-pulih.sql
-- Uji pulih: ulangi ketiga skenario anomali pada desain yang telah dinormalisasi.
-- 1. Insert: produk baru bisa dicatat tanpa satu pun pesanan.
INSERT INTO produk (nama, harga) VALUES ('Stabilo', 6000.00);

-- 2. Update: harga berubah satu kali, di satu-satunya barisnya.
UPDATE produk SET harga = 5000.00 WHERE nama = 'Buku Tulis';

-- 3. Delete: pesanan dihapus, data produk tetap hidup.
DELETE FROM pesanan WHERE id = 3;

SELECT ps.id, pl.nama AS nama_pelanggan, pr.nama AS produk, ps.kuantitas
FROM pesanan ps
JOIN pelanggan pl ON pl.id = ps.pelanggan_id
JOIN produk pr ON pr.id = ps.produk_id
ORDER BY ps.id;

SELECT id, nama, harga FROM produk ORDER BY id;

Output yang Dihasilkan:

text
INSERT 0 1
UPDATE 1
DELETE 1
 id | nama_pelanggan |   produk   | kuantitas 
----+----------------+------------+-----------
  1 | Budi Santoso   | Buku Tulis |        10
  2 | Siti Rahayu    | Buku Tulis |         2
(2 rows)

 id |    nama    |  harga  
----+------------+---------
  1 | Buku Tulis | 5000.00
  2 | Penggaris  | 2500.00
  3 | Stabilo    | 6000.00
(3 rows)

Tiga operasi di file itu adalah tiga anomali lama yang kembali dalam keadaan pulih:

  1. Stabilo masuk tanpa satu pun pesanan — tabel produk menerima barang baru apa adanya; katalog tidak lagi disandera oleh keberadaan transaksi.
  2. Harga Buku Tulis naik satu kali di satu baris — tidak ada salinan lain yang harus dikejar; hasil akhirnya terlihat konsisten pada kedua pesanan di laporan pertama.
  3. Pesanan 3 dihapus, Penggaris tetap hidup — laporan kedua masih menampilkan Penggaris dengan harganya; menghapus peristiwa tidak lagi mengubur fakta barang.

Normalisasi membayar semua ini dengan satu harga: laporan yang tadinya sekali baca kini butuh join. Itulah pertukaran yang menjadi benang merah section ini — dan pada titik tertentu, dunia nyata kadang meminta arah sebaliknya: sengaja menambah redundansi demi kecepatan baca. Sebelum sampai pada keputusan sesulit itu, tiga jenjang bentuk normal harus dikuasai dulu satu per satu, dimulai dari aturan yang tampak paling sederhana tapi paling sering dilanggar: satu sel, satu nilai — di chapter First Normal Form .

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

anomali-delete.sql

Unduh berkas
sql
-- Anomali delete: Siti membatalkan pesanan Penggaris.
-- Satu-satunya baris yang memuat data produk Penggaris ikut terhapus,
-- sehingga katalog toko "kehilangan" barang yang masih dijual.
DELETE FROM pesanan_flat WHERE id_baris = 3;

SELECT id_baris, nama_pelanggan, nama_produk, kuantitas
FROM pesanan_flat
ORDER BY id_baris;

anomali-insert.sql

Unduh berkas
sql
-- Anomali insert: Stabilo baru tiba di gudang, belum ada pembelinya.
-- Tabel datar tidak punya tempat untuk mencatat barang tanpa transaksi.
INSERT INTO pesanan_flat (id_baris, nama_pelanggan, kota, tanggal,
                          nama_produk, harga, kuantitas)
VALUES (4, NULL, NULL, NULL, 'Stabilo', 6000.00, 0);

anomali-update.sql

Unduh berkas
sql
-- Anomali update: harga Buku Tulis naik, petugas hanya mengubah
-- satu baris. Baris lain yang memuat harga lama terlupakan.
UPDATE pesanan_flat SET harga = 5000.00 WHERE id_baris = 1;

SELECT id_baris, nama_pelanggan, nama_produk, harga
FROM pesanan_flat
ORDER BY id_baris;

-- "Berapa harga Buku Tulis?" kini punya dua jawaban yang bertentangan.
SELECT nama_produk, MIN(harga) AS termurah, MAX(harga) AS termahal
FROM pesanan_flat
WHERE nama_produk = 'Buku Tulis'
GROUP BY nama_produk;

buat-flat.sql

Unduh berkas
sql
-- Catatan lama pemilik Toko Maju Jaya: satu tabel datar untuk semuanya.
-- Setiap baris = satu pesanan; data pelanggan dan produk ikut tersimpan
-- berulang di dalamnya.
CREATE TABLE pesanan_flat (
    id_baris       integer PRIMARY KEY,
    nama_pelanggan text NOT NULL,
    kota           text NOT NULL,
    tanggal        date NOT NULL,
    nama_produk    text NOT NULL,
    harga          numeric(12, 2) NOT NULL,
    kuantitas      integer NOT NULL
);

INSERT INTO pesanan_flat (id_baris, nama_pelanggan, kota, tanggal,
                          nama_produk, harga, kuantitas) VALUES
    (1, 'Budi Santoso', 'Bandung', DATE '2026-01-05', 'Buku Tulis', 4500.00, 10),
    (2, 'Siti Rahayu',  'Jakarta', DATE '2026-01-09', 'Buku Tulis', 4500.00,  2),
    (3, 'Siti Rahayu',  'Jakarta', DATE '2026-02-11', 'Penggaris',  2500.00,  1);

SELECT id_baris, nama_pelanggan, kota, tanggal, nama_produk, harga, kuantitas
FROM pesanan_flat
ORDER BY id_baris;

dekomposisi.sql

Unduh berkas
sql
-- Dekomposisi: satu tabel datar menjadi tiga tabel yang saling merujuk.
-- Kita mulai dari salinan bersih pesanan_flat (kondisi sebelum anomali),
-- lalu memindahkan setiap fakta ke satu-satunya tempat tinggalnya.
DROP TABLE IF EXISTS pesanan_flat CASCADE;

CREATE TABLE pesanan_flat (
    id_baris       integer PRIMARY KEY,
    nama_pelanggan text NOT NULL,
    kota           text NOT NULL,
    tanggal        date NOT NULL,
    nama_produk    text NOT NULL,
    harga          numeric(12, 2) NOT NULL,
    kuantitas      integer NOT NULL
);

INSERT INTO pesanan_flat (id_baris, nama_pelanggan, kota, tanggal,
                          nama_produk, harga, kuantitas) VALUES
    (1, 'Budi Santoso', 'Bandung', DATE '2026-01-05', 'Buku Tulis', 4500.00, 10),
    (2, 'Siti Rahayu',  'Jakarta', DATE '2026-01-09', 'Buku Tulis', 4500.00,  2),
    (3, 'Siti Rahayu',  'Jakarta', DATE '2026-02-11', 'Penggaris',  2500.00,  1);

CREATE TABLE pelanggan (
    id    integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    nama  text NOT NULL,
    kota  text NOT NULL
);

CREATE TABLE produk (
    id    integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    nama  text NOT NULL,
    harga numeric(12, 2) NOT NULL
);

CREATE TABLE pesanan (
    id           integer PRIMARY KEY,
    tanggal      date NOT NULL,
    pelanggan_id integer NOT NULL REFERENCES pelanggan (id),
    produk_id    integer NOT NULL REFERENCES produk (id),
    kuantitas    integer NOT NULL
);

-- Migrasi pelanggan: DISTINCT mengeruk dua baris ganda menjadi satu.
INSERT INTO pelanggan (nama, kota)
SELECT DISTINCT nama_pelanggan, kota
FROM pesanan_flat
ORDER BY nama_pelanggan;

-- Migrasi produk: harga Buku Tulis yang tersimpan dua kali menjadi satu baris.
INSERT INTO produk (nama, harga)
SELECT DISTINCT nama_produk, harga
FROM pesanan_flat
ORDER BY nama_produk;

-- Migrasi pesanan: baris flat diterjemahkan menjadi rujukan id.
-- Join migrasi memakai pasangan kolom yang sama dengan DISTINCT di atas.
INSERT INTO pesanan (id, tanggal, pelanggan_id, produk_id, kuantitas)
SELECT f.id_baris, f.tanggal, pl.id, pr.id, f.kuantitas
FROM pesanan_flat f
JOIN pelanggan pl ON pl.nama = f.nama_pelanggan AND pl.kota = f.kota
JOIN produk pr ON pr.nama = f.nama_produk AND pr.harga = f.harga
ORDER BY f.id_baris;

-- Tabel lama pensiun setelah seluruh datanya berpindah.
DROP TABLE pesanan_flat;

-- Laporan yang tadinya dibaca dari satu tabel kini disusun ulang
-- lewat join -- dengan data pelanggan dan produk yang hanya hidup sekali.
SELECT ps.id, pl.nama AS nama_pelanggan, pl.kota, ps.tanggal,
       pr.nama AS produk, pr.harga, ps.kuantitas
FROM pesanan ps
JOIN pelanggan pl ON pl.id = ps.pelanggan_id
JOIN produk pr ON pr.id = ps.produk_id
ORDER BY ps.id;

uji-pulih.sql

Unduh berkas
sql
-- Uji pulih: ulangi ketiga skenario anomali pada desain yang telah dinormalisasi.
-- 1. Insert: produk baru bisa dicatat tanpa satu pun pesanan.
INSERT INTO produk (nama, harga) VALUES ('Stabilo', 6000.00);

-- 2. Update: harga berubah satu kali, di satu-satunya barisnya.
UPDATE produk SET harga = 5000.00 WHERE nama = 'Buku Tulis';

-- 3. Delete: pesanan dihapus, data produk tetap hidup.
DELETE FROM pesanan WHERE id = 3;

SELECT ps.id, pl.nama AS nama_pelanggan, pr.nama AS produk, ps.kuantitas
FROM pesanan ps
JOIN pelanggan pl ON pl.id = ps.pelanggan_id
JOIN produk pr ON pr.id = ps.produk_id
ORDER BY ps.id;

SELECT id, nama, harga FROM produk ORDER BY id;