Database Normalization adalah proses strukturisasi data dalam rangka mengurangi Data Redundancy dan meningkatkan Data Integrity. Data Redundancy adalah pengulangan data yang sama sehingga beresiko terhadap kesalahan penginputan. Data Integrity adalah data yang mudah dimaintain, akurat, dan konsisten. Kali ini kita akan melakukan normalisasi data dari yang belum dinormalisasi atau Unnormalized Form (UNF) menjadi Normalized Form 1NF, 2NF, 3NF, BCNF, 4NF, 5NF, dan DKNF. Unnormalized Form memiliki redundancy yang tinggi dan integritasnya rendah. Oleh karena itu, kita butuh menormalisasi data untuk mencapai kedua goals tersebut. Biar lebih paham, mending langsung praktekkan menggunakan database yang dimiliki😊. Kita ambil contoh menggunakan data perfilman yang belum dinormalisasi atau Unnormalized Form (UNF) seperti ini:
nik |
name |
origin |
phone_no |
123 |
Kimo |
DKI Jakarta |
08555 |
321 |
Timo |
Papua |
08333 |
999 |
Hanung |
DKI Jakarta |
08111,08222 |
Kriteria untuk memenuhi 1NF adalah tidak ada Multivalue pada kolom. Pada table director
, kolom phone_no
dengan NIK 999 Multivalue karena ada lebih dari satu nomor telepon sehingga melanggar 1NF. Masalahnya adalah ribet saat data dengan NIK 999 mengganti salah satu nomornya. Misalkan dari 08222 ke 08221 maka kita harus update kolom tersebut dengan memasukkan salah satu nomor lama, lalu tambahkan nomor baru di belakangnya secara comma-separated. Untuk itu kita perlu normalisasi seperti berikut:
nik |
name |
origin |
phone_no |
123 |
Kimo |
DKI Jakarta |
08555 |
321 |
Timo |
Papua |
08333 |
999 |
Hanung |
DKI Jakarta |
08111 |
999 |
Hanung |
DKI Jakarta |
08222 |
Sekarang ga ada lagi kolom yang multivalue karena datanya sudah dipisah. Tabel di atas sudah memenuhi kriteria 1NF. Jika sutradara dengan NIK 999 mau mengganti nomornya dari 08222 ke 08221 maka cukup update satu row saja yang menyimpan value 08222.
Sebelum masuk ke tingkat normalisasi selanjutnya, kita perlu tahu dulu istilah “key” dalam database.
Candidate Key adalah satu atau kombinasi beberapa kolom unik yang menjadi penanda bahwa data tersebut merupakan satu kesatuan data yang mewakili kolom-kolom lainnya. Candidate Key dari tabel di atas ada 2, yaitu {nik
} dan {phone_no
}, karena nik
adalah kolom unik yang mewakili kolom name
& origin
. Lewat nik
kita bisa tahu nama dan asal sutradara karena satu NIK sudah pasti memiliki satu nama dan daerah asal. Kolom phone_no
juga kolom unik yang mewakili kolom nik
. Lewat phone_no
kita bisa tahu ini nomor siapa karena satu nomor sudah pasti memiliki satu NIK. Kolom name
bukan Candidate Key karena bisa saja ada orang yang namanya sama. Begitu juga dengan origin
. Candidate Key fungsinya sebagai Identifier sebuah data secara bisnis.
Primary Key adalah kolom unik yang menjadi pembeda antar row yang bisa dijadikan referensi dari data tersebut dan tidak boleh null. Primary Key fungsinya sebagai Identifier sebuah data secara teknis. Candidate Key boleh dijadikan Primary Key. Tapi dalam hal ini Candidate Key tidak dianjurkan menjadi Primary Key karena bisa berubah nilainya. Misalkan pada tabel di atas kolom nik
dijadikan Primary Key. Kemudian data tersebut berelasi dengan tabel lain. Lalu ternyata ada typo saat input NIK di awal dan harus diganti. Tentu tidak semudah itu mengganti Primary Key kalau datanya sudah berelasi. Meskipun ada fitur cascades, tetap saja ga worth it mengubah beberapa data dan tabel. Oleh karena itu, kita akan pakai auto-increment value sebagai Primary Key pada kasus ini, sedangkan Candidate Key cukup diberi constraint Unique Key. Untuk lebih jelasnya bisa dibaca tulisan tentang UUID vs Auto-Increment Key.
Unique Key adalah kolom yang nilainya unik. Bedanya dengan Primary Key adalah Unique Key membolehkan nilainya nullable maupun non-nullable.
Foreign Key adalah kolom yang menjadi penghubung relasi antar tabel sebagai referensi data dari satu tabel ke tabel lainnya.
Syarat untuk memenuhi kriteria 2NF adalah memenuhi syarat 1NF dan tidak ada Partial/Functional Dependency. Partial/Functional Dependency adalah ketika sebuah tabel memiliki lebih dari satu Candidate Key dan ada kolom yang bergantung pada salah satu Candidate Key saja. Contohnya pada data di atas Candidate Key-nya {nik
} dan {phone_no
}. Kita bisa tahu nama dan daerah asal sutradara lewat NIK karena datanya diwakili NIK. Tapi kita tidak bisa tahu nama dan daerah asal sutradara lewat nomor telepon karena itu bukan bagian dari nomor telepon. Jadi kolom name
dan origin
hanya bergantung pada Candidate Key nik
saja. Masalahnya, akan terjadi pengulangan data pada kolom Non-Candidate Key jika sutradara tersebut memiliki nomor telepon lebih dari satu sehingga ribet dimaintain. Contohnya jika sutradara dengan NIK 999 yang memiliki lebih dari satu nomor telepon pada data di atas ganti nama atau ganti daerah asal, maka butuh update lebih dari satu row. Oleh karena itu kolom-kolom yang memiliki Partial/Functional Dependency dipisah menjadi tabel sendiri agar memenuhi syarat 2NF seperti berikut:
id (PK) |
nik (UK) |
name |
origin |
1 |
123 |
Kimo |
DKI Jakarta |
2 |
321 |
Timo |
Papua |
3 |
999 |
Hanung |
DKI Jakarta |
id (PK) |
phone_no (UK) |
director_id (FK) |
1 |
08111 |
3 |
2 |
08222 |
3 |
3 |
08555 |
1 |
4 |
08333 |
2 |
Sekarang tidak ada lagi pengulangan data pada kolom Non-Candidate Key pada tabel di atas. Kita juga memisahkan Primary Key-nya menggunakan auto-increment, sedangkan Candidate Key menggunakan constraint Unique Key. Kedua tabel tersebut dihubungkan oleh kolom director_id
di tabel phone
. Itulah yang disebut Foreign Key seperti penjelasan sebelumnya. Sekarang kalau mau ganti nama sutradara cukup update satu row saja.
Misalkan tabel director
dibuat sebagai berikut:
id |
nik |
name |
origin_code |
origin_province |
1 |
123 |
Kimo |
JKT |
DKI Jakarta |
2 |
321 |
Timo |
PAP |
Papua |
3 |
999 |
Hanung |
JKT |
DKI Jakarta |
Kriteria yang memenuhi 3NF adalah sudah memenuhi 2NF dan tidak ada Transitive Dependency. Transitive Dependency adalah ketika kolom A bergantung pada kolom B yang bukan Candidate Key, dan kolom B bergantung pada kolom C yang menjadi Candidate Key, tetapi kolom C tidak bergantung pada kolom B. Jadi, ada kolom yang bergantung pada kolom lain yang bukan Candidate Key. Biar ga pusing kita ambil contoh di atas. Tabel di atas sudah memenuhi 2NF karena tidak ada Partial/Functional Dependency, Candidate Key-nya hanya nik
karena cuma NIK yang unik. Namun itu tidak memenuhi 3NF. Kolom origin_province
bergantung ke kolom origin_code
yang bukan Candidate Key, dan kolom origin_code
bergantung ke kolom nik
. Jadi, dari NIK kita bisa tahu sutradara tersebut kode asalnya apa, dan dari kode tersebut kita bisa tahu itu provinsi apa. Tapi dari kode tersebut kita tidak bisa tahu itu NIK siapa karena satu daerah bisa terdapat lebih dari satu sutradara. Masalahnya, jika data pada kolom origin_province
, misalkan sebelumnya dengan value "DKI Jakarta" diubah jadi "Jakarta", maka perlu update semua row yang memiliki value "DKI Jakarta". Kalau data sutradara yang berasal dari DKI Jakarta sangat banyak tentu ga efisien😵. Sekarang kita ubah menjadi 3NF:
id |
nik |
name |
origin_id |
1 |
123 |
Kimo |
1 |
2 |
321 |
Timo |
2 |
3 |
999 |
Hanung |
1 |
id |
origin_code |
province |
1 |
JKT |
DKI Jakarta |
2 |
PAP |
Papua |
Tabel di atas sudah memenuhi 3NF. Ga ada lagi Transitive Dependency. Saat melakukan perubahan nama provinsi, cukup ganti satu row pada tabel origin
saja, tabel director
hanya mereferensikan datanya lewat origin_id
.
BCNF adalah singkatan dari Boyce-Codd Normalization Form. Ini sebenarnya mirip dengan 3NF dan disebut 3.5NF. Keduanya memiliki kriteria yang mirip, hanya saja pada BCNF tidak ada Functional Dependency sama sekali pada tabel yang memiliki kombinasi kolom Candidate Key untuk menghindari redundancy. Tagline-nya adalah "Nothing but the key". Jadi pada tabel relasi, isinya hanya Key yang berelasi saja. Contoh kasusnya seperti ini:
id |
name |
year |
1 |
Rumah Dara |
2010 |
2 |
Ratu Ilmu Hitam |
2019 |
3 |
Sebelum Iblis Menjemput |
2018 |
id |
nik |
name |
movie_id |
1 |
123 |
Kimo |
1 |
2 |
321 |
Timo |
1 |
3 |
123 |
Kimo |
2 |
4 |
321 |
Timo |
3 |
Tabel di atas memenuhi 3NF karena tidak ada Transitive Dependency. Kolom nik
mewakili name
& movie_id
, sedangkan kolom movie_id
hanya mewakili nik
. Jadi di sini ada satu Candidate Key, yaitu kombinasi {nik
, movie_id
} karena keduanya saling bergantung. Tapi itu melanggar BCNF karena ada Functional Dependency, yaitu kolom name
yang bergantung ke nik
dan bukan Candidate Key. Permasalahannya adalah satu orang bisa menyutradai lebih dari satu film, dan satu film bisa disutradai lebih dari satu orang. Kimo menyutradai Rumah Dara & Ratu Ilmu Hitam, Timo menyutradai Rumah Dara & Sebelum Iblis Menjemput. Makanya terjadi pengulangan data pada kolom name
di atas. Next kita terapkan BCNF:
id |
name |
year |
1 |
Rumah Dara |
2010 |
2 |
Ratu Ilmu Hitam |
2019 |
3 |
Sebelum Iblis Menjemput |
2018 |
id |
nik |
name |
1 |
123 |
Kimo |
2 |
321 |
Timo |
3 |
999 |
Hanung |
director_id |
movie_id |
1 |
1 |
1 |
2 |
2 |
1 |
2 |
3 |
Kita bikin tabel Composite khusus untuk mapping referensi data director
dan movie
yang berisi kombinasi Candidate Key saja tanpa kolom lain. Sekarang tidak ada lagi redundancy. Ini cocok untuk many-to-many relationship.
1NF hingga BCNF adalah normalisasi paling dasar dan paling sering ditemui saat desain skema. Kita lanjut ke part yang lebih ekstreme😎.
Kriteria 4NF adalah memenuhi syarat BCNF dan tidak ada Multivalued Dependency. Multivalued Dependency adalah ketika terdapat lebih dari satu dependency pada tabel. Misalnya Candidate Key {A, B} saling bergantung dan Candidate Key {B, C} juga saling bergantung, sehingga terdapat lebih dari satu kombinasi Candidate Key pada tabel. Contohnya seperti berikut:
id |
nik |
name |
1 |
123 |
Kimo |
2 |
321 |
Timo |
3 |
999 |
Hanung |
id |
name |
year |
1 |
Rumah Dara |
2010 |
2 |
Ratu Ilmu Hitam |
2019 |
3 |
Sebelum Iblis Menjemput |
2018 |
4 |
Rudy Habibie |
2016 |
id |
name |
1 |
Horror |
2 |
Comedy |
3 |
Biography |
4 |
Drama |
5 |
Action |
6 |
Thriller |
Table director_movie_genre
director_id |
movie_id |
genre_id |
1 |
1 |
1 |
1 |
1 |
6 |
2 |
1 |
1 |
2 |
1 |
6 |
3 |
4 |
3 |
3 |
4 |
4 |
Tabel di atas memenuhi BCNF karena hanya ada Candidate Key pada tabel relasi. Tapi itu Multivalued karena ada 2 Candidate Key, yaitu kombinasi {director_id
, movie_id
} dan kombinasi {movie_id
, genre_id
} sehingga melanggar 4NF. Satu orang bisa menyutradai lebih dari satu film, dan satu film bisa disutradai lebih dari satu orang. Satu film bisa saja memiliki lebih dari satu genre, begitu juga satu genre bisa saja termasuk ke dalam lebih dari satu film. Jadi bisa disimpulkan bahwa, sutradara Kimo menyutradai film Rumah Dara, Rumah Dara adalah film Horror & Thriller, dan Kimo adalah sutradara film genre Horror & Thriller. Masalahnya, akan terjadi redundancy pada tabel di setiap penambahan film baru dengan multi-genre atau multi-sutradara karena director_id
secara tidak langsung ada dependency dengan kolom genre
. Mulai kompleks kan🤯? Solusinya kita akan pecah tabel director_movie_genre
menjadi seperti berikut:
director_id |
movie_id |
1 |
1 |
2 |
1 |
3 |
4 |
movie_id |
genre_id |
1 |
1 |
1 |
6 |
4 |
3 |
4 |
4 |
Setelah dipecah tidak ada lagi Multivalued Dependency karena masing-masing tabel sudah independent. Jadi tiap ada film baru, tinggal insert genre dan sutradaranya di masing-masing tabel relasi.
Kriterianya adalah memenuhi syarat 4NF dan tidak ada Join Dependency pada tabel relasi. Join Dependency artinya terjadi redundancy saat Join dan menghasilkan data yang tidak diinginkan. Misalnya Candidate Key {A, B} saling bergantung, lalu Candidate Key {B, C} di tabel lain juga saling bergantung, sehingga secara tidak langsung Candidate Key A ada dependency ke Candidate Key C melalui perantara Candidate Key B pada saat Join dan menimbulkan redundancy yang tidak diinginkan. Biar lebih paham bisa dilihat skema di bawah:
id |
name |
1 |
Horror |
2 |
Comedy |
3 |
Biography |
4 |
Drama |
5 |
Action |
6 |
Thriller |
id |
name |
year |
1 |
Rumah Dara |
2010 |
2 |
Ratu Ilmu Hitam |
2019 |
3 |
Sebelum Iblis Menjemput |
2018 |
4 |
Rudy Habibie |
2016 |
id |
name |
1 |
Slasher |
2 |
Gore |
3 |
Supernatural |
4 |
Psychological |
movie_id |
genre_id |
1 |
1 |
1 |
6 |
4 |
3 |
4 |
4 |
genre_id |
subgenre_id |
1 |
1 |
1 |
2 |
1 |
3 |
1 |
4 |
Satu film memiliki beberapa genre, satu genre bisa juga termasuk ke dalam beberapa film. Satu genre bisa memiliki beberapa subgenre, satu subgenre juga bisa termasuk ke dalam beberapa genre. Satu film bisa saja termasuk ke beberapa subgenre, dan satu subgenre juga bisa termasuk ke dalam beberapa film. Jadi bisa disimpulkan seperti film A memiliki genre B, dan genre B memiliki subgenre C & D, namun film A tersebut hanya termasuk ke dalam subgenre C saja. Makin kompleks dong😱. Skema di atas sudah memenuhi 4NF karena tidak ada Multivalued Dependency. Tapi melanggar 5NF karena pada data di atas, film Rumah Dara bergenre Horror dan Thriller. Genre Horror memiliki subgenre seperti Slasher, Gore, Supernatural, dan Psychological. Akan tetapi film Rumah dara hanya termasuk ke dalam subgenre Slasher dan Gore saja. Masalahnya, film Rumah Dara akan memiliki semua subgenre Horror saat semua table di-Join dan tidak sesuai kriteria bisnis karena akan terjadi redundancy subgenre yang tidak diinginkan seperti berikut:
movie |
genre |
subgenre |
Rumah Dara |
Horror |
Slasher |
Rumah Dara |
Horror |
Gore |
Rumah Dara |
Horror |
Supernatural |
Rumah Dara |
Horror |
Psychological |
Kita harus memastikan bahwa subgenre dari film Rumah Dara hanya Slasher dan Gore. Maka dari itu kita normalize skema di atas dengan menambahkan tabel baru seperti berikut:
movie_id |
subgenre_id |
1 |
1 |
1 |
2 |
Setelah dinormalisasi tidak ada lagi redundancy pada skema tersebut saat semua tabel di-Join dan sudah sesuai kriteria bisnis. Ini adalah normalisasi terakhir dimana tidak ada redundancy sama sekali.
movie |
genre |
subgenre |
Rumah Dara |
Horror |
Slasher |
Rumah Dara |
Horror |
Gore |
DKNF (Domain Key Normalization Form) syaratnya tidak ada constraint lain selain Key Constraint dan Domain Constraint untuk menghindari anomaly. Key Constraint artinya kolom tersebut memiliki reference pada kolom unik di tabel lain seperti normalisasi-normalisasi di atas. Sedangkan Domain Constraint artinya kolom tersebut memiliki reference secara bisnis berdasarkan satu set data di tabel lain. Biar ga bingung bisa liat contoh berikut:
id |
name |
budget |
budget_status |
1 |
Rumah Dara |
1000000000 |
Milyaran |
2 |
Ratu Ilmu Hitam |
5550000000 |
Milyaran |
3 |
Sebelum Iblis Menjemput |
1000000 |
Milyaran |
4 |
Rudy Habibie |
70000000 |
Jutaan |
Tabel di atas melanggar DKNF karena bisa saja terjadi anomaly pada kolom budget_status
. Misalkan user melakukan insert budget Rp1.000.000 tapi budget statusnya diisi Milyaran seperti data film Sebelum Iblis Menjemput di atas. Ini tentu tidak sesuai kriteria bisnis karena harusnya statusnya Jutaan. Untuk itu kita akan buat tabel budgets
sebagai Domain Constraint untuk budget_status
seperti berikut:
id |
name |
budget |
1 |
Rumah Dara |
1000000000 |
2 |
Ratu Ilmu Hitam |
5550000000 |
3 |
Sebelum Iblis Menjemput |
1000000 |
4 |
Rudy Habibie |
70000000 |
id |
budget_status |
min_budget |
max_budget |
1 |
Milyaran |
1000000000 |
999999999999 |
2 |
Jutaan |
1000000 |
999999999 |
Sekarang data di atas sudah memenuhi DKNF karena ada Domain Constraint dari kolom budget_status
di table movie
ke tabel budgets
. Sehingga tidak ada lagi anomaly data karena statusnya bersumber dari tabel budgets
. Query joinnya nanti kurang lebih seperti berikut:
SELECT
m.name,
m.budget,
b.budget_status
FROM movie m
INNER JOIN budgets b
ON m.budget BETWEEN b.min_budget AND b.max_budget
;
id |
name |
budget |
budget_status |
1 |
Rumah Dara |
1000000000 |
Milyaran |
2 |
Ratu Ilmu Hitam |
5550000000 |
Milyaran |
3 |
Sebelum Iblis Menjemput |
1000000 |
Jutaan |
4 |
Rudy Habibie |
70000000 |
Jutaan |
Kita telah melakukan praktek normalisasi data dari Unnormalized Form (UNF) menjadi Normalized Form 1NF, 2NF, 3NF, BCNF, 4NF, 5NF, dan DKNF. Sebenarnya 6NF juga ada sebagai bentuk penyempurnaannya dengan memecah setiap kolom dari tabel. Contohnya kolom tahun dan nama film pada tabel movie
dipecah lagi ke tabel baru, yaitu tabel movies_year
dan movies_name
misalnya sehingga kolom year
dan name
pada tabel movie
nanti valuenya adalah reference dari tabel-tabel tersebut. Tapi jarang dibutuhkan kasus seperti itu sih. Makanya 5NF sebenarnya udah cukup disebut bentuk paling normal. Selain itu ada juga EKNF (Elementary Key), tapi EKNF itu mirip BCNF. Lalu ada juga ETNF (Essential Tuple) yang menjadi improvement dari 4NF dalam rangka mengurangi Join Dependency, tapi 5NF biasanya lebih diutamakan untuk mencegah Join Dependency karena lebih strict. DKNF adalah bentuk normalisasi yang mereferensikan datanya lewat Key Constraint dan Domain Constraint. Oh ya, tidak semua kasus bisa langsung diterapkan Normalisasi yang tinggi, semuanya dilakukan secara bertahap dari 1NF hingga seterusnya tergantung kasus. Normalized data cocok untuk menyimpan data transaksi yang membutuhkan integritas yang kuat dengan redundancy yang minimal. Tapi bukan berarti Data yang tidak Normalized merupakan skema yang buruk. Itu lebih cocok untuk menampung data analytics karena berfokus pada history data dan biasanya lebih cocok menggunakan NoSql daripada RDBMS.