Denormalisasi

Model baca, materialized view, dan biaya sinkronisasi yang menyertainya.

Daftar isi

Denormalisasi adalah keputusan sengaja menyalin atau menggabungkan data ke bentuk yang paling sesuai dengan pola pembacaan, dengan konsekuensi adanya duplikasi yang harus disinkronkan. Topik ini membahas biaya join pada skema ternormalisasi, model baca sebagai bentuk denormalisasi, materialized view, dan perhitungan biaya sinkronisasi yang menyertainya.

Normalisasi dan biaya join#

Skema ternormalisasi menempatkan setiap fakta pada satu tempat, sehingga pembaruan tidak pernah menyisakan salinan yang tidak seragam. Bentuk ini menjaga integritas data dengan murah, karena satu baris pada satu tabel adalah satu-satunya tempat kebenaran untuk fakta tersebut. Harganya muncul pada waktu baca, yaitu data yang terpecah ke beberapa tabel harus digabung kembali dengan join.

Pembahasan pada topik ini memakai satu contoh yang sama, yaitu tabel sensor, tabel lokasi, dan tabel pembacaan suhu per sensor. Halaman detail sensor yang menampilkan nama lokasi beserta rerata suhu harian menggabungkan ketiga tabel itu pada setiap pembukaan halaman. Ketiga tabel tersebut menjaga kebenaran datanya dengan baik, tetapi pertanyaannya bergeser ketika halaman yang sama dibuka ribuan kali untuk setiap perubahan yang hanya terjadi sekali.

Pada skala kecil, join adalah operasi yang murah dan tidak layak dipikirkan. Pada tabel yang besar, penggabungan membutuhkan memori, penyortiran, dan pembacaan halaman dari beberapa tabel sekaligus, sehingga latensinya naik dan bentuknya sulit diprediksi. Bila pembacaan jauh lebih sering daripada penulisan, menghitung ulang gabungan yang sama pada setiap permintaan menjadi pekerjaan yang boros. Situasi inilah yang membuka ruang bagi denormalisasi.

Model baca#

Read model, atau model baca, adalah bentuk data yang sudah digabung dan disalin untuk satu pola pembacaan. Misalkan halaman detail sensor menampilkan nama lokasi bersama rekap suhu harian. Pada skema ternormalisasi, halaman itu menggabungkan tabel sensor, tabel lokasi, dan tabel pembacaan setiap kali dibuka. Pada model baca, hasil gabungan tersebut disalin menjadi satu baris atau satu dokumen yang siap dibaca dalam satu query.

Model baca mirip cache, tetapi keduanya tidak sama, dan perbedaannya menentukan tanggung jawab desain. Cache dapat dihapus kapan saja tanpa merusak data, karena sumber aslinya tetap ada dan dapat dihitung ulang. Salinan model baca justru berdiri sendiri selama dipakai, sehingga salinan itu wajib memiliki strategi pembaruan yang jelas dan tidak boleh dibiarkan basi tanpa batas. Aturan yang paling sehat adalah menyalin secukupnya, yaitu hanya atribut yang benar-benar dibaca setiap saat. Usia salinan juga perlu terlihat oleh pembaca, misalnya dengan menyimpan waktu pembaruan terakhir pada salinannya, sehingga halaman dapat menampilkan keterangan kapan data terakhir disegarkan.

Materialized view#

Materialized view adalah fitur basis data yang menyimpan hasil sebuah query secara fisik, sehingga hasil itu dapat dibaca ulang tanpa menghitung ulang. Fitur ini berbeda dari view biasa yang dihitung setiap kali dibaca. Materialized view sering dipakai untuk agregasi berat, misalnya rekap harian yang menyapu ribuan baris, karena biaya hitungnya dibayar satu kali saat dibuat dan diperbarui.

Contoh berikut membuat rekap harian per sensor dari tabel pembacaan, lalu memperbaruinya secara eksplisit.

sql Contoh: materialized view rekap harian sensor
-- tidak-dijalankan
-- Materialized view menyimpan hasil agregasi untuk dibaca cepat
CREATE MATERIALIZED VIEW rekap_sensor AS
SELECT
    sensor_id,
    DATE_TRUNC('day', waktu_baca) AS hari,
    COUNT(*)                      AS jumlah_baca,
    AVG(nilai_suhu)               AS rerata_suhu
FROM pembacaan_sensor
GROUP BY sensor_id, DATE_TRUNC('day', waktu_baca);

-- Pembaruan dilakukan secara eksplisit, bukan otomatis
REFRESH MATERIALIZED VIEW rekap_sensor;

Contoh di atas memakai sintaks PostgreSQL. Perhatikan baris terakhirnya, yaitu isi rekap tidak berubah sendiri sampai perintah REFRESH dijalankan, dan pada itulah seluruh agregasi dihitung ulang. PostgreSQL juga menyediakan REFRESH MATERIALIZED VIEW CONCURRENTLY yang memperbarui isi tanpa mengunci pembacaan, sehingga halaman yang sedang dibuka tidak tersendat. Pemilihan granularitas rekap juga menentukan biayanya, karena rekap per jam memuat lebih banyak baris daripada rekap per hari, tetapi membutuhkan pembaruan yang lebih sering pula. Ketersediaan fitur semacam ini berbeda antarbasis data, tetapi pola kerjanya sama, yaitu hasil yang berat dihitung terpisah dari jalur tulis.

Biaya sinkronisasi#

Setiap salinan menambah pekerjaan tulis. Perubahan pada tabel utama harus disalurkan ke setiap salinan yang memuat atributnya, baik secara sinkron maupun asinkron. Pembaruan sinkron menambah latensi pada setiap tulisan, sedangkan pembaruan asinkron membuka jendela data basi yang panjangnya sekitar selang pembaruannya. Keduanya menuntut keputusan yang disadari, bukan salinan yang dilupakan.

Penyalurannya sendiri memiliki dua bentuk yang lazim. Perubahan dapat disebarkan sebagai pesan pada antrean, sehingga tiap salinan diperbarui segera setelah tabel utama berubah dan kegagalan pengiriman diulang sampai berhasil. Alternatifnya, salinan agregat seperti materialized view diperbarui sesuai jadwal, misalnya setiap beberapa menit atau sekali per jam, dengan menerima jendela basi yang lebih panjang sebagai konsekuensinya. Bentuk yang dipilih menyesuaikan seberapa basi data boleh dilihat pembaca.

Skrip berikut menghitung beban tulisan harian bila tabel utama menerima 2000 perubahan per hari dan setiap perubahan disalin ke sejumlah salinan.

python simulasi-biaya.py
"""Simulasi biaya sinkronisasi model baca denormalisasi.

Setiap perubahan pada tabel utama harus disalin ke setiap salinan yang
memuat atributnya, sehingga tulisan efektif per perubahan adalah satu
ditambah banyaknya salinan. Skrip menghitung beban tulisan harian untuk
beberapa jumlah salinan.
"""

PERUBAHAN_HARIAN = 2_000  # perubahan pada tabel utama per hari


def tulisan_efektif(jumlah_salinan: int) -> int:
    """Beban tulisan per hari: tabel utama ditambah semua salinannya."""
    return PERUBAHAN_HARIAN * (1 + jumlah_salinan)


print(f"Perubahan tabel utama per hari: {PERUBAHAN_HARIAN}")
print()
print("salinan | tulisan efektif per hari")
print("--------+-------------------------")
for jumlah in range(0, 6):
    print(f"   {jumlah}   | {tulisan_efektif(jumlah):>8}")
text Keluaran
Perubahan tabel utama per hari: 2000

salinan | tulisan efektif per hari
--------+-------------------------
   0   |     2000
   1   |     4000
   2   |     6000
   3   |     8000
   4   |    10000
   5   |    12000

Setiap salinan menambah beban tulis sebesar beban tabel utamanya, sehingga lima salinan melipatgandakan beban menjadi enam kali lipat. Angka ini menjelaskan mengapa denormalisasi diterapkan hanya pada atribut yang jarang berubah dan dibaca terus, misalnya nama lokasi sensor, dan tidak pada nilai pengukuran yang berubah setiap detik. Salinan yang diperbarui asinkron juga perlu diberi usia maksimum, sehingga halaman yang membacanya dapat menampilkan keterangan basi bila pembaruannya tertunda.

Kesalahan umum#

Tiga kesalahan berikut paling sering muncul ketika denormalisasi baru diterapkan.

  • Denormalisasi sebelum pola baca diukur. Duplikasi dibayar tanpa manfaat, dan perbaikannya adalah mulai dari skema ternormalisasi, lalu denormalisasi dilakukan berdasarkan bukti pada query yang lambat. Gejala awalnya adalah ikut tersalinnya halaman yang jarang dibaca, sehingga pekerjaan sinkronisasi bertambah tanpa pembacaan yang mengimbangi.
  • Salinan tanpa strategi pembaruan. Data basi menjadi permanen, dan kesalahannya baru ketahuan saat pembaca melaporkan angka yang tidak cocok dengan sumber utamanya. Perbaikannya adalah menetapkan mekanisme pembaruan beserta usia maksimum salinannya sejak awal.
  • Menyalin atribut yang berubah setiap saat. Sinkronisasi menjadi beban utama sistem, dan jendela basinya merugikan karena nilainya berubah begitu cepat. Perbaikannya adalah membaca atribut tersebut dari sumber aslinya atau menyimpan nilai terakhirnya pada sumber yang sama.
  • Menyalin seluruh tabel sebagai salinan baru alih-alih menyalin atribut yang dibaca. Salinan tabel meniru skema lama beserta beban yang tidak dibutuhkan, dan perbaikannya adalah merancang model baca dari pola query, bukan menyalin struktur.

Ringkasan#

  • Denormalisasi menukar biaya join pada waktu baca dengan biaya sinkronisasi pada waktu tulis.
  • Read model dan materialized view menyimpan hasil gabungan atau agregasi, sehingga pembacaan yang berat menjadi satu query.
  • Setiap salinan menambah beban tulis dan jendela data basi, sehingga salinan dibuat secukupnya dengan strategi pembaruan yang jelas.

Referensi#