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:
| Tujuan | Artinya praktis | Gejala bila dilanggar |
|---|---|---|
| tanpa redundansi | satu fakta tersimpan di satu tempat saja | data sama berulang di banyak baris |
| tanpa anomali | insert, update, delete tidak merusak fakta lain | operasi 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:
-- 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:
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:
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:
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:
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 normal | Larangan intinya | Satu kalimat |
|---|---|---|
| 1NF | nilai berulang dalam satu sel | satu sel satu nilai atomik; daftar jadi baris |
| 2NF | ketergantungan parsial pada composite key | atribut non-kunci bergantung pada seluruh kunci |
| 3NF | ketergantungan transitif antar kolom non-kunci | atribut 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:
-- 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:
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:
- Tiga tabel dengan pembagian kerja yang jelas —
pelangganmenyimpan fakta orang,produkmenyimpan fakta barang,pesananmenyimpan fakta peristiwa beserta dua rujukannya. Id pelanggan dan produk dibangkitkanGENERATED ALWAYS AS IDENTITYkarena migrasi tidak punya kunci alami yang bisa dipercaya. INSERT INTO pelanggan ... SELECT DISTINCT nama_pelanggan, kota—DISTINCTmengeruk dua baris Siti yang ganda menjadi satu. Dua insert migrasi pertama inilah yang secara harfiah menghapus redundansi.INSERT INTO pesanan ... JOIN pelanggan ... JOIN produk— tiap baris flat diterjemahkan menjadi id rujukan. Join migrasi memakai pasangan kolom yang sama denganDISTINCTdi atas (nama dan kota untuk pelanggan, nama dan harga untuk produk) supaya terjemahannya tidak ambigu.DROP TABLE pesanan_flat— tabel lama pensiun setelah seluruh datanya berpindah; mempertahankannya berarti memelihara dua sumber kebenaran.- Laporan akhir disusun ulang lewat join — bentuknya identik dengan
SELECTdibuat-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:
-- 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:
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:
- Stabilo masuk tanpa satu pun pesanan — tabel
produkmenerima barang baru apa adanya; katalog tidak lagi disandera oleh keberadaan transaksi. - 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.
- 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-- 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-- 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-- 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-- 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-- 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-- 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;