Normalisasi Database: 1NF, 2NF, 3NF dengan Contoh Nyata
Blog/Tutorial SQL/Normalisasi Database: 1NF, 2NF, 3NF dengan Contoh Nyata

Normalisasi Database: 1NF, 2NF, 3NF dengan Contoh Nyata

BimaBima
·3 April 2026·10 menit baca

Penulis

Bima

Bima

Founder & Data Professional

Bagikan

TL;DR

Normalisasi database adalah proses nyusun ulang tabel biar tiap fakta cuma disimpan sekali, jadi data gak dobel dan gak gampang jadi gak konsisten. Ada tiga bentuk normal yang dipakai sehari-hari: 1NF (satu sel satu nilai), 2NF (semua kolom nempel ke seluruh primary key), dan 3NF (gak ada kolom yang nempel ke kolom non-key lain). Buat database aplikasi, 3NF udah cukup di hampir semua kasus.

Normalisasi database adalah proses nyusun ulang tabel biar tiap fakta cuma disimpan sekali. Tujuannya satu: bikin data gak dobel, jadi update satu hal gak berubah jadi update di 20 tempat.

Ini bukan teori kuliah yang cuma buat lulus ujian. Ini yang nentuin apakah laporan omzet kamu bakal cocok atau selisih 3 juta gara-gara ada satu nama produk yang ketulis beda.

Aku bakal jalanin satu tabel penjualan UMKM yang berantakan, dirapiin pelan-pelan sampai 3NF. Satu tabel, tiga tahap, dan kamu bakal lihat sendiri masalahnya hilang di tiap tahap.

Apa itu normalisasi database?

Normalisasi database adalah aturan buat nyusun kolom dan tabel supaya tiap potong informasi cuma punya satu tempat tinggal. Kalau nama pelanggan cuma ada di tabel pelanggan, ganti nama itu berarti update satu baris — bukan 400 baris transaksi.

Aturan ini disusun bertingkat, namanya normal form. Ada 1NF, 2NF, 3NF, BCNF, sampai 5NF. Yang kepake di kerjaan sehari-hari cuma tiga yang pertama.

Konsep ini pertama kali ditulis Edgar F. Codd di IBM tahun 1970, dan sampai sekarang aturannya belum berubah. Detail formalnya bisa kamu cek di dokumentasi constraint PostgreSQL, yang jelasin gimana primary key dan foreign key nge-enforce aturan ini di level database.

Kenapa tabel yang gak dinormalisasi bikin masalah?

Ini tabel penjualan versi mentahnya. Semua numpuk jadi satu:

CREATE TABLE penjualan_mentah (
  id_transaksi   INT,
  tanggal        DATE,
  nama_pelanggan VARCHAR(100),
  kota_pelanggan VARCHAR(50),
  produk         VARCHAR(255),  -- "Beras 5kg, Gula 1kg, Minyak 2L"
  kategori       VARCHAR(50),
  harga_satuan   INT,
  qty            INT,
  total          INT
);

Tabel kayak gini punya tiga penyakit yang punya nama resmi:

MasalahContoh nyatanya
Insert anomalyMau daftarin produk baru yang belum pernah kejual? Gak bisa, soalnya butuh id_transaksi dulu.
Update anomalyPelanggan pindah kota. Kamu harus update kota_pelanggan di semua barisnya. Kelewat satu, data langsung bentrok.
Delete anomalyHapus transaksi terakhir dari satu pelanggan, dan datanya pelanggan itu ikut hilang total.

Tiga masalah itu semuanya berakar dari hal yang sama: satu fakta disimpan di banyak tempat.

Apa itu 1NF (bentuk normal pertama)?

1NF artinya satu sel cuma boleh isi satu nilai, dan tiap baris harus bisa dibedain dari baris lain. Gak ada koma, gak ada list, gak ada kolom produk_1, produk_2, produk_3.

Di tabel di atas, kolom produk isinya "Beras 5kg, Gula 1kg, Minyak 2L". Itu tiga nilai di satu sel. Melanggar 1NF.

Coba tulis query buat hitung berapa kali Beras 5kg kejual. Kamu bakal kepaksa pakai LIKE '%Beras 5kg%', dan itu bakal salah kalau ada produk lain namanya "Beras 5kg Premium".

Perbaikannya: pecah jadi satu baris per produk.

CREATE TABLE penjualan_1nf (
  id_transaksi   INT,
  id_baris       INT,
  tanggal        DATE,
  nama_pelanggan VARCHAR(100),
  kota_pelanggan VARCHAR(50),
  nama_produk    VARCHAR(100),
  kategori       VARCHAR(50),
  harga_satuan   INT,
  qty            INT,
  PRIMARY KEY (id_transaksi, id_baris)
);

Sekarang tiap baris = satu produk di satu transaksi. Primary key-nya gabungan id_transaksi + id_baris.

Query hitung penjualan Beras 5kg jadi bersih:

SELECT SUM(qty) AS total_terjual
FROM penjualan_1nf
WHERE nama_produk = 'Beras 5kg';

Gak ada LIKE, gak ada tebak-tebakan. Kalau kamu belum familiar sama cara kerja SUM dan GROUP BY, dua halaman itu jelasin dasarnya.

Apa itu 2NF (bentuk normal kedua)?

2NF berlaku kalau tabel kamu udah 1NF dan tiap kolom non-key nempel ke seluruh primary key, bukan cuma sebagian.

Aturan ini cuma relevan kalau primary key-nya gabungan lebih dari satu kolom. Kalau primary key-nya cuma satu kolom, tabel kamu otomatis udah 2NF.

Di penjualan_1nf, primary key-nya (id_transaksi, id_baris). Sekarang cek kolomnya satu-satu:

  • tanggal — ditentuin cuma sama id_transaksi. Gak butuh id_baris. Melanggar.
  • nama_pelanggan — sama, cuma butuh id_transaksi. Melanggar.
  • qty — butuh dua-duanya. Transaksi 1 baris 1 qty-nya beda sama transaksi 1 baris 2. Aman.

Kolom yang cuma nempel ke sebagian key harus dipindah ke tabel sendiri:

CREATE TABLE transaksi (
  id_transaksi INT PRIMARY KEY,
  tanggal      DATE,
  id_pelanggan INT
);

CREATE TABLE transaksi_item (
  id_transaksi INT,
  id_baris     INT,
  id_produk    INT,
  qty          INT,
  PRIMARY KEY (id_transaksi, id_baris)
);

Sekarang tanggal transaksi cuma ditulis sekali per transaksi. Sebelumnya, transaksi yang isinya 8 produk nyimpen tanggal yang sama 8 kali.

Apa itu 3NF (bentuk normal ketiga)?

3NF berlaku kalau tabel udah 2NF dan gak ada kolom non-key yang ditentuin sama kolom non-key lain.

Nama teknisnya transitive dependency. Artinya: A nentuin B, B nentuin C, jadi C nyangkut ke A lewat B. Kolom C harusnya pindah ikut B.

Di tabel produk, kategori ditentuin sama nama_produk, bukan sama primary key transaksi. Beras 5kg selalu kategori Sembako, siapapun yang beli, kapanpun belinya.

Kalau kategori disimpan di tabel transaksi, dan suatu hari kamu mau ganti nama kategori "Sembako" jadi "Bahan Pokok", kamu harus update ribuan baris transaksi. Pindahin ke tabel sendiri:

CREATE TABLE produk (
  id_produk    INT PRIMARY KEY,
  nama         VARCHAR(100),
  id_kategori  INT,
  harga_satuan INT
);

CREATE TABLE kategori (
  id_kategori INT PRIMARY KEY,
  nama        VARCHAR(50)
);

Ganti nama kategori sekarang = update 1 baris. Semua laporan langsung ikut bener.

Hal yang sama berlaku buat kota_pelanggan. Kota ditentuin sama pelanggannya, bukan sama transaksinya. Jadi kota pindah ikut tabel pelanggan.

Contoh kasus: rapihin data toko_berkah

Dataset toko_berkah di ngulikdata isinya penjualan 6 cabang UMKM sembako, 18.400 baris transaksi, 1 tahun penuh. Versi mentahnya dulu satu tabel lebar, 14 kolom.

Waktu aku hitung berapa banyak data yang sebenernya diulang-ulang, hasilnya bikin kaget:

SELECT
  COUNT(*)                          AS total_baris,
  COUNT(DISTINCT nama_pelanggan)    AS pelanggan_unik,
  COUNT(DISTINCT nama_produk)       AS produk_unik,
  COUNT(DISTINCT kategori)          AS kategori_unik
FROM penjualan_mentah;

Hasilnya: 18.400 baris, tapi cuma 1.240 pelanggan unik, 86 produk unik, dan 9 kategori unik.

Artinya string "Sembako" ketulis di database sekitar 7.100 kali. Padahal cukup sekali di tabel kategori.

Yang lebih parah: waktu aku cek nama pelanggan, ada 3 varian ejaan buat orang yang sama. "Budi Santoso", "budi santoso", dan "Budi Santoso " (ada spasi di belakang). Di laporan pelanggan teratas, satu orang ini kehitung jadi 3 orang beda — dan gak satupun masuk top 10, padahal kalau digabung dia peringkat 4.

Ini yang dimaksud update anomaly. Bukan teori. Bikin laporan kamu salah beneran.

Setelah dinormalisasi jadi 6 tabel, query laporan omzet per kategori jadi gini:

SELECT
  k.nama                       AS kategori,
  SUM(ti.qty * p.harga_satuan) AS omzet
FROM transaksi_item ti
JOIN produk    p ON p.id_produk    = ti.id_produk
JOIN kategori  k ON k.id_kategori  = p.id_kategori
JOIN transaksi t ON t.id_transaksi = ti.id_transaksi
WHERE t.tanggal >= '2026-01-01'
GROUP BY k.nama
ORDER BY omzet DESC;

Lebih panjang? Iya, ada 3 JOIN. Tapi hasilnya bener, dan ganti nama kategori gak bakal ngerusak laporan bulan lalu.

Kesalahan umum waktu normalisasi

1. Nyimpen kolom hasil hitungan

Kolom total yang isinya harga_satuan * qty itu data turunan. Hitung aja pas query. Kalau disimpan, dan suatu hari harga_satuan-nya dikoreksi, kolom total jadi bohong.

Pengecualian: harga produk bisa berubah dari waktu ke waktu. Jadi harga saat transaksi memang harus disimpan di transaksi_item — itu bukan data turunan, itu fakta historis. Beda tipis, tapi penting.

2. Bikin tabel terlalu banyak

Ada yang saking semangatnya, kolom jenis_kelamin dibikinin tabel sendiri isinya 2 baris. Gak salah secara teori, tapi bikin query kamu penuh JOIN yang gak ada gunanya. Pakai judgment.

3. Denormalisasi duluan sebelum ada masalah

"JOIN itu lambat" adalah alasan paling sering dipakai buat gak normalisasi. Tapi di tabel 18.400 baris dengan index yang bener, 3 JOIN itu selesai dalam hitungan milidetik.

Denormalisasi baru masuk akal kalau kamu udah ukur dan JOIN-nya beneran jadi bottleneck. Biasanya itu kejadian di data warehouse dengan ratusan juta baris, bukan di database aplikasi biasa.

4. Lupa bikin foreign key

Mecah tabel doang gak cukup. Tanpa foreign key, gak ada yang nyegah kamu masukin id_produk = 999 yang gak ada di tabel produk. Database jadi punya baris yatim.

ALTER TABLE transaksi_item
ADD CONSTRAINT fk_produk
FOREIGN KEY (id_produk) REFERENCES produk(id_produk);

FAQ

Normalisasi database itu buat apa sih?

Buat mastiin tiap fakta cuma disimpan di satu tempat. Kalau nama produk disimpan di 18.000 baris transaksi, ganti satu nama produk berarti update 18.000 baris. Kelewat satu aja, data kamu langsung gak konsisten. Dengan normalisasi, nama produk cuma ada di tabel produk. Update sekali, semua laporan ikut bener.

Apa bedanya 2NF sama 3NF?

2NF ngurusin hubungan kolom ke primary key. Kalau primary key-nya gabungan dua kolom, tiap kolom lain harus nempel ke dua-duanya, bukan cuma salah satu. 3NF ngurusin hubungan antar kolom non-key. Kalau kolom A nentuin kolom B dan dua-duanya bukan key, pisahin B ke tabel sendiri. Contohnya kode pos yang nentuin kota.

Perlu gak sampai BCNF atau 4NF?

Buat database aplikasi sehari-hari, 3NF udah cukup di hampir semua kasus. BCNF dan seterusnya kepake kalau kamu punya beberapa candidate key yang saling tumpang tindih, dan ini jarang muncul di skema bisnis biasa. Jangan buang waktu ngejar bentuk normal tinggi kalau 3NF-nya aja belum kelar.

Kapan boleh denormalisasi?

Boleh kalau kamu udah ukur dan JOIN-nya beneran jadi masalah, biasanya di data warehouse atau tabel laporan yang dibaca ribuan kali per menit. Simpan hasil rekapnya di tabel terpisah, tapi jangan ganggu tabel transaksi aslinya. Denormalisasi tanpa data pendukung cuma bikin masalah baru: data dobel yang gak sinkron.

Gimana kalau data aku udah terlanjur berantakan di satu tabel gede?

Jangan rombak langsung. Bikin tabel baru yang udah 3NF, isi pakai INSERT INTO SELECT DISTINCT dari tabel lama, cek jumlah barisnya cocok, baru pindahin aplikasi ke tabel baru. Tabel lama simpan dulu sebagai backup beberapa minggu. Migrasi bertahap jauh lebih aman daripada ALTER TABLE besar-besaran.

Penutup

Ringkasnya:

  • 1NF — satu sel, satu nilai. Gak ada koma di dalam sel.
  • 2NF — tiap kolom nempel ke seluruh primary key, bukan cuma separonya.
  • 3NF — gak ada kolom non-key yang nentuin kolom non-key lain.

Sampai 3NF udah cukup buat 95% database yang bakal kamu bikin. Sisanya biasanya cuma bikin pusing tanpa manfaat nyata.

Mau tes pemahaman kamu? Buka satu Google Sheet kerjaan yang paling lebar, terus tanya: kolom mana yang datanya keulang terus? Itu kandidat pertama buat dipisah jadi tabel sendiri.

Lanjut baca: ERD Database buat belajar gambar relasi antar tabel yang barusan kamu bikin, dan bedanya INNER JOIN dan LEFT JOIN buat nyatuin tabel-tabel itu balik lagi jadi laporan.

Coba Langsung

Mau praktek langsung? Mulai latihan SQL gratis

Latihan interaktif, langsung di browser.

Buka NgulikSQL →
Bagikan:
Bima
Ditulis oleh

Bima

Founder & Data Professional

Founder Ngulik Data. Passionate about making data analysis accessible for everyone.

Artikel Terkait

Churn Rate SQL: Deteksi Pelanggan yang Kabur Sejak Dini
Tutorial SQL
20 Mei 2026•11 menit baca

Churn Rate SQL: Deteksi Pelanggan yang Kabur Sejak Dini

Studi kasus toko_berkah: 1.240 pelanggan hilang dalam setahun, dan sinyalnya udah kelihatan 2 bulan sebelumnya. Ini query yang bikin kamu nangkap mereka lebih awal.

BimaBima
YoY Growth SQL: Bandingkan Performa dengan Tahun Lalu
Tutorial SQL
17 Mei 2026•11 menit baca

YoY Growth SQL: Bandingkan Performa dengan Tahun Lalu

Hitung YoY growth di SQL pakai LAG atau self-join, lengkap dengan cara ngindarin pembagi nol dan jebakan bulan yang bolong. Contohnya pakai data toko_berkah.

BimaBima
MoM Growth SQL: Hitung Pertumbuhan Bulanan Otomatis
Tutorial SQL
14 Mei 2026•10 menit baca

MoM Growth SQL: Hitung Pertumbuhan Bulanan Otomatis

Berhenti copy-paste omzet bulan lalu ke Excel buat ngitung growth. Satu query SQL pakai LAG bisa ngasih pertumbuhan bulanan otomatis, lengkap sampai bulan yang datanya kosong.

BimaBima
Kembali ke Blog
Ngulik Data logoNgulik Data

Platform edukasi data lengkap untuk professionals Indonesia. Belajar SQL, Data Analysis, dan lebih banyak lagi dengan praktek langsung dan feedback real-time.

Copyright © 2026 - All rights reserved

LINKS
SupportPricingDatasetBlogAffiliates
LEGAL
Terms of servicesPrivacy policy
Ngulik Data
DatasetLeaderboardBlogStore