Denormalizing When There Is No Source of Truth
Why we moved a performance management module's hierarchy data into one snapshot table: the org tables were unreliable, so we traded normal forms for correctness and easier debugging.
Di sebuah platform performance management yang saya kerjakan, kami melakukan denormalisasi pada sebagian data model, tapi bukan karena soal kecepatan read. Tabel organisasi yang seharusnya jadi single source of truth ternyata sudah tidak bisa dipercaya, dan kami butuh desain yang tidak bergantung padanya. Artinya, kami sengaja melanggar aturan normalisasi.
Masalah
Data organization management seharusnya jadi fondasi untuk semua hal lain. Strukturnya berupa rantai tabel yang ternormalisasi:
- Employee: master record tiap orang
- Position dan Position variant: jabatan dan variasinya
- Assignment: menghubungkan employee ke position variant
- Position-organization mapping: menempatkan position di struktur organisasi
- Group master: unit-unit organisasi, masing-masing milik tepat satu company
Ini seharusnya jadi single source of truth, tapi datanya salah di hampir semua layer. Ada employee yang hilang. Ada position yang di-map ke organisasi yang salah, group yang di-map ke company yang salah, dan position yang pindah organisasi di tengah sebuah periode. Sebagian penyebabnya ada di proses: admin yang bertugas menjaga data dan user yang memakainya tidak sinkron, dan celahnya akhirnya masuk ke database.
Constraint
Modul performance management harus menghitung KPI tiap employee dengan benar, dan sebuah performance record melekat pada assignment tertentu di waktu tertentu. Kalau seseorang memegang position A di Q1 dan Q2, lalu pindah ke position lain mulai Q3, tiap periode punya data performance sendiri.
Modul ini juga bergantung pada hierarki untuk menentukan siapa yang boleh approve KPI planning untuk siapa, dan siapa yang bisa memantau realisasi dan achievement KPI. Manager yang melihat subordinate yang salah atau hilang tidak bisa, atau tidak mau, meng-approve KPI yang sudah direncanakan. Dengan hierarki yang buruk, sistem ini tidak berguna bagi user yang live.
Jadi kebenaran data bergantung pada siapa memegang position apa, di bawah atasan siapa, pada periode kapan. Dengan hierarki live yang terus berubah dan penuh error, skor historis bisa salah tanpa ada yang sadar. Modul ini juga harus go live secepat mungkin, jadi menunggu data upstream dibersihkan bukan pilihan.
Keputusan
Kami berhenti melakukan join ke hierarki live dan membuat satu tabel denormalisasi yang dimiliki modul ini. Tiap row berisi employee number (primary ID employee), position, atasan (superior), dan periode saat relasi itu berlaku. Superior disimpan langsung, tidak lagi diturunkan tiap kali.
Saat data benar, mencari atasan atau subordinate seseorang berarti menelusuri sekitar lima tabel: position saat ini, lalu organisasinya, lalu position lain di organisasi itu, lalu employee di position-position tersebut. Itu masih bisa ditoleransi selama datanya benar.
Sebelum denormalisasi:

Tapi jadi tidak tertoleransi saat datanya salah. Kalau seseorang seharusnya punya lima subordinate tapi yang muncul hanya empat, saya harus cek position orang yang hilang itu, link antara position dan organisasinya, apakah dia satu organisasi dengan manager-nya, dan seterusnya, tabel demi tabel. Dengan tabel baru, ini cukup dengan membaca satu row.
Setelah denormalisasi:

Tabel ini juga berfungsi sebagai snapshot point-in-time, jadi perubahan organisasi di kemudian hari tidak bisa menulis ulang sejarah di balik sebuah skor. Dan modul ini jadi terlepas dari hierarki platform utama yang sudah tidak kami percaya.
Trade-offs dan Justifikasi
Denormalisasi biasanya dibenarkan karena performa read. Di sini alasannya adalah correctness dan kemudahan diagnosis: saat data salah, satu tabel untuk dicek dan diperbaiki lebih baik daripada lima.
- Gain: perbaikan data atau investigasi yang dulu melibatkan lima tabel atau lebih, sekarang cukup satu.
- Gain: modul bisa go live dengan data yang dikontrol sendiri, tanpa menunggu cleanup upstream.
- Cost: snapshot menduplikasi data yang juga ada di upstream, jadi bisa melenceng dari tabel live kalau tidak ada yang merawatnya.
- Cost: tim harus tahu bahwa tabel ini, bukan hierarki, yang otoritatif untuk performance.
Saya menerima cost itu karena alternatifnya adalah membangun di atas data yang kami tahu salah.
Normal Form yang Dilanggar
Tabel ini melanggar 3NF, dan BCNF ikut terlanggar. Normal form itu soal dependensi antar kolom, jadi kita lihat kolom mana bergantung pada kolom mana.
- 1NF dan 2NF terpenuhi. Tiap row adalah satu fakta atomic, dan setiap kolom bergantung pada seluruh key: employee number plus periode berlaku.
- 3NF dilanggar. Superior bukan fakta yang berdiri sendiri tentang employee. Di upstream, ia diturunkan dari position dan organisasi tempat position itu berada. Menyimpannya langsung menciptakan transitive dependency: employee dan periode menentukan position, dan position menentukan superior.
- BCNF dilanggar karena alasan yang sama: kolom non-key menentukan kolom lain.
- 4NF dan 5NF tidak jadi masalah. Tiap row mencatat satu relasi untuk satu periode, tanpa fakta multi-valued independen yang tercampur.
Duplikasinya disengaja. Normalisasi mengasumsikan satu kebenaran saat ini per fakta, tapi di sini faktanya adalah orang ini memegang position ini di bawah atasan ini selama periode ini. Itu catatan historis, mirip Type 2 slowly changing dimension di data warehousing.
Karena schema tidak lagi menjaga konsistensi, integritas pindah ke proses: aturan periode tidak overlap per employee, foreign key ke employee number, dan reconciliation check terhadap tabel upstream.
Hasil
Modul bisa rilis dengan data yang dikontrol sendiri, dan koreksi jadi perubahan di satu tabel, bukan banyak tabel.
Normalisasi mengasumsikan data upstream bisa dipercaya. Kami tidak bisa, dan deadline-nya nyata, jadi saya memilih desain yang tidak konvensional dengan sengaja dan menerima konsekuensinya.