Lewati ke konten utama

Denormalisasi dalam Basis Data: Kapan dan Cara Menggunakannya

Pelajari bagaimana denormalisasi meningkatkan kinerja baca dengan mengurangi join dan menyederhanakan kueri. Pahami trade-off, teknik, dan kasus penggunaan yang menjadikannya alat ampuh untuk sistem analitik dan pelaporan.
· 15 mnt Baca

Jelajahi bersama AI

Saya telah banyak menulis tentang normalisasi dan mengapa itu merupakan fondasi yang andal untuk integritas data. Ini bukan berarti saya menarik ucapan saya. Saya tetap percaya bahwa skema yang ternormalisasi dengan baik adalah titik awal yang tepat untuk sebagian besar sistem transaksional. Artikel ini membahas pilihan sadar untuk menyimpang dari kemurnian itu dalam situasi sangat spesifik ketika kinerja baca lebih penting daripada bentuk normal yang ketat.

Denormalisasi bukan berarti melewatkan normalisasi. Ini adalah optimasi kinerja yang Anda terapkan pada model yang sudah ternormalisasi ketika kueri nyata, pengguna nyata, dan SLA nyata memberi tahu Anda bahwa join dan perhitungan langsung terlalu lambat atau terlalu mahal. Dalam praktiknya, Anda menukar baca yang lebih cepat dan kueri yang lebih sederhana dengan lebih banyak penyimpanan, penulisan yang lebih kompleks, dan pekerjaan konsistensi tambahan.

Dalam artikel ini, saya akan menunjukkan di mana denormalisasi berguna dan kapan tidak, serta cara menerapkannya dengan aman di basis data SQL. Tujuannya bukan untuk meninggalkan desain yang baik, melainkan menambahkan jalan pintas di atasnya ketika beban kerja membenarkannya.

Apa itu Denormalisasi dalam Basis Data?

Jika Anda familier dengan normalisasi dan desain basis data, berikut jawaban singkatnya: 

Denormalisasi adalah tindakan sengaja menambahkan data redundan ke skema yang sebelumnya ternormalisasi untuk mempercepat baca dan menyederhanakan kueri. Ini adalah optimasi kinerja yang terarah, bukan alasan untuk melewatkan pemodelan yang baik!

Jika Anda relatif baru dalam desain basis data, ada baiknya mengurai makna dari “ternormalisasi”, “terdenormalisasi”, dan “tidak ternormalisasi”. Anda sering mendengar 3 istilah itu secara daring, dan penting untuk tidak mencampuradukkannya.

Ternormalisasi: Data dipecah ke dalam tabel yang terstruktur dengan baik yang meminimalkan redundansi dan melindungi integritas (lihat 3NF/BCNF).

Terdenormalisasi: Anda memperkenalkan kembali redundansi yang selektif dengan kolom tambahan, nilai prahitung, atau tabel pra-join di atas model yang ternormalisasi itu untuk membuat baca umum lebih cepat. Dengan denormalisasi, Anda menjaga satu sumber kebenaran (tabel ternormalisasi), lalu memelihara satu atau lebih representasi yang lebih cepat untuk jalur panas, seperti dasbor, daftar produk, penelusuran, dll. Anda menukar penyimpanan + kompleksitas tulis dengan kecepatan baca + kueri yang lebih sederhana.

Tidak ternormalisasi: Data mentah, ad-hoc, atau berantakan di mana struktur dan kendalinya tidak pernah dirancang dengan benar. Bukan itu yang kita lakukan di sini.

Contoh dalam SQL

Berikut contoh kecil dalam SQL untuk membantu Anda memvisualisasikan perbedaan antara pemodelan ternormalisasi dan terdenormalisasi.

Pemodelan ternormalisasi

-- Source of truth
CREATE TABLE customers (
  customer_id   BIGINT PRIMARY KEY,
  name          TEXT NOT NULL,
  tier          TEXT NOT NULL
);

CREATE TABLE orders (
  order_id      BIGINT PRIMARY KEY,
  customer_id   BIGINT NOT NULL REFERENCES customers(customer_id),
  order_total   NUMERIC(12,2) NOT NULL,
  created_at    TIMESTAMP NOT NULL DEFAULT now()
);

-- Typical report needs a join
SELECT c.name, c.tier, SUM(o.order_total) AS revenue
FROM orders o
JOIN customers c ON c.customer_id = o.customer_id
GROUP BY c.name, c.tier;

Terdenormalisasi untuk jalur pelaporan

-- Add redundant fields for faster reads
ALTER TABLE orders
  ADD COLUMN customer_name TEXT,
  ADD COLUMN customer_tier TEXT;

-- Now most reports avoid the join
SELECT customer_name, customer_tier, SUM(order_total) AS revenue
FROM orders
GROUP BY customer_name, customer_tier;

Dalam contoh ini, Anda tetap menjadikan customers sebagai sumber kebenaran dan memastikan orders.customer_name / orders.customer_tier tetap sinkron (mis., trigger, pekerjaan CDC, atau backfill terjadwal). Sumber kebenaran yang sama, dua representasi, masing-masing dioptimalkan untuk tugas yang berbeda.

Apakah denormalisasi adalah desain yang buruk?

Ada kesalahpahaman umum bahwa denormalisasi hanyalah produk dari desain yang buruk. Itu tidak benar jika diukur, diberi ruang lingkup, dan dipelihara. Ya, denormalisasi sering melanggar bentuk normal yang lebih tinggi (memang itulah tujuannya), tetapi itu disengaja dan didukung oleh rencana untuk menjaga konsistensi. Desain yang buruk adalah melewatkan normalisasi sama sekali atau menaburkan redundansi tanpa strategi sinkronisasi.

Jika Anda ingin mempelajari lebih lanjut tentang desain basis data, kursus pemula kami tentang topik ini adalah tempat yang baik untuk memulai!

Normalisasi vs Denormalisasi

Berikut perbandingan berdampingan. Ini tidak lengkap, tetapi akan membantu Anda memahami trade-off dari masing-masing pendekatan.

Aspek

Normalisasi

Denormalisasi

Tujuan utama

Integritas, redundansi minimal, pembaruan mudah & benar

Baca lebih cepat, kueri lebih sederhana pada jalur panas

Terbaik untuk

Sistem OLTP dengan tulis/pembaruan sering

Dasbor baca-berat, halaman penelusuran/daftar, pelaporan/analitik

Kinerja baca

Sering memerlukan join, bagus dengan indeks yang tepat

Lebih sedikit join, bisa jauh lebih cepat dan lebih prediktabel

Kompleksitas tulis

Sederhana: satu sumber kebenaran

Lebih tinggi: harus memperbarui/menyinkronkan salinan atau agregat redundan

Penyimpanan

Hemat

Lebih besar (kolom/tabel ekstra, tampilan prahitung)

Integritas data

Kuat secara desain (kendala 3NF/BCNF)

Memerlukan mekanisme untuk mencegah drift (trigger, CDC, pekerjaan)

Kecepatan perubahan

Ganti nama/perbarui kolom bersifat lokal

Perubahan dapat menyebar ke data yang diduplikasi

Beban operasional

Lebih rendah: lebih sedikit komponen bergerak

Lebih tinggi: kebijakan penyegaran, backfill, pemantauan

Mode kegagalan

Kueri N+1, join lambat, indeks hilang

Data basi, inkonsistensi, amplifikasi tulis

Evolusi skema

Prediktabel, mudah direfaktor

Memerlukan rencana migrasi untuk representasi redundan

Contoh tipikal

Pesanan, pelanggan, transaksi

Daftar produk pra-join; tabel penjualan agregat; tampilan termaterialisasi/berindeks

Mengapa dan Kapan Melakukan Denormalisasi

Denormalisasi menjadi sangat berguna ketika pengguna nyata dan kueri nyata terhambat oleh join, agregasi, atau lookup berulang. Setelah Anda memastikan bahwa pengindeksan, penyetelan kueri, dan caching tidak cukup, Anda mungkin ingin beralih ke denormalisasi untuk mengoptimalkan baca cepat pada pola akses yang dapat diprediksi.

Kapan denormalisasi mungkin membantu

  • Beban kerja baca-berat: latensi p95/p99 didominasi join atau agregasi, CPU dihabiskan untuk hash/merge join, churn buffer cache tinggi.
  • Bentuk kueri stabil: Dasbor/endpoint yang sama berjalan sepanjang hari dengan filter serupa (mis., penjualan kemarin per kategori).
  • Join menyebar (fan-out): Satu tabel panas bergabung ke 3–5 tabel lain hanya untuk merender kartu atau tampilan daftar.
  • Titik panas agregasi: Anda berulang kali menghitung total, hitungan, atau nilai terbaru di atas rentang besar.
  • Tekanan SLA: Produk memerlukan respons di bawah 200 ms di mana rencana saat ini tumpah ke disk atau memindai terlalu banyak.

Kasus penggunaan klasik

  • Dasbor & pelaporan BI (OLAP/analitik): Pra-hitung agregat harian/bulanan, simpan tampilan termaterialisasi dari group-by yang mahal, atau simpan tabel fakta terdenormalisasi untuk irisan umum.
  • E-niaga/katalog & halaman penelusuran/daftar: Duplikasi category_name, brand_name, price_with_tax, atau proyeksi produk pra-join untuk daftar dan filter yang cepat.
  • CMS/blog/umpan berita: Simpan author_name, primary_topic, atau rendered_excerpt pada tabel artikel/posting untuk menghindari join atau transformasi runtime.
  • Umpan aktivitas & penghitung: Simpan like_count, follower_count, atau latest_comment_at sebagai atribut turunan alih-alih menghitung ulang.
  • Analitik peristiwa/log skala besar (OLAP/NoSQL): Ratakan data bertingkat untuk penyimpanan kolumnar dan simpan baris lebar yang ramah partisi untuk membuat pemindaian lebih prediktabel.

Kapan tidak melakukan denormalisasi dan gunakan hal lain terlebih dahulu

  • OLTP tulis-berat dengan konsistensi ketat (pesanan, pembayaran, penyesuaian inventaris).
  • Bottleneck adalah indeks yang hilang/buruk, kueri N+1, atau ORM yang cerewet. Perbaiki itu dulu.
  • Kolom bervolatilitas tinggi (mis., ketersediaan produk berubah tiap detik) di mana duplikasi memperbesar churn.
  • Tim tanpa kepemilikan dan rencana sinkronisasi yang jelas (trigger, CDC/pekerjaan, kebijakan penyegaran, pemantauan drift). Tanpa ini, Anda berisiko lebih merusak daripada membantu!
  • Dataset cukup kecil sehingga indeks penutup atau cache sudah membuatnya cepat.

Anda benar-benar ingin menggunakan denormalisasi di mana itu memberi Anda kecepatan baca paling besar dengan beban operasional tambahan paling kecil, dan hanya setelah Anda menyingkirkan perbaikan yang lebih murah.

Jika Anda menyadari bahwa denormalisasi belum tepat untuk saat ini, tetaplah di sini! Kita akan mengeksplorasi alternatif denormalisasi di bagian berikutnya.

Alternatif untuk Denormalisasi

Denormalisasi bukan jawaban untuk semua masalah Anda. Sebelum Anda menambahkan redundansi, Anda perlu memaksimalkan mesin dan aplikasi Anda. Perbaikan ini lebih murah untuk dipelihara dan sering kali memberikan hasil yang sama.

1) Pengindeksan

  • Indeks komposit/penutup: Dalam kueri SQL Anda, letakkan kolom filter terlebih dahulu, lalu kolom GROUP BY/ORDER BY. Saya sertakan kolom daftar pilihan agar mesin dapat melayani kueri hanya dari indeks.

  • Indeks terfilter/parsial: Indeks hanya irisan panas (mis., status = 'ACTIVE'), menjaga indeks tetap kecil dan cepat.

  • Indeks ekspresi/fungsional: Indeks pada LOWER(email) atau date_trunc('day', created_at) untuk menghindari pemindaian terhitung.

2) Penyetelan kueri & paginasi

  • Hindari SELECT *. Ambil hanya yang Anda tampilkan.

  • Ganti join yang tidak perlu dengan EXISTS/SEMI saat Anda hanya perlu memeriksa keberadaan.

  • Dorong predikat ke bawah: filter lebih awal, agregasi belakangan.

  • Gunakan paginasi keyset (WHERE created_at < ? ORDER BY created_at DESC LIMIT 50) untuk scroll yang stabil dan cepat.

3) Caching

  • Gunakan cache aplikasi (mis., Redis) untuk kueri panas dan fragmen yang dirender.
  • Gunakan caching HTTP (ETag/Last-Modified) untuk halaman publik dan dasbor.
  • Cache berumur pendek (30–120 dtk) sering menghilangkan kebutuhan perubahan skema.

4) Replika baca

  • Alihkan beban baca berat ke replika. Sangat bagus untuk dasbor dan ekspor.

5) Partisi & pruning

  • Partisi Rentang/Hash tabel besar sehingga pemindaian Anda hanya menyentuh partisi yang relevan (mis., 30 hari terakhir). Ini bukan denormalisasi, omong-omong, ini sekadar mengurangi jumlah data yang dipindai.

6) Penyimpanan kolumnar/OLAP

  • Pindahkan analitik berat ke Snowflake/BigQuery/ClickHouse (melalui ELT/dbt). Pertahankan OLTP ternormalisasi dan biarkan gudang data menangani bentuk lebar yang ramah pemindaian.

7) Kebersihan ORM

  • Hilangkan kueri N+1 (eager-load atau batch), tetapkan daftar pilihan yang masuk akal, dan batasi ukuran halaman. Lapisan ORM yang bersih dapat menghilangkan kebutuhan untuk mendenormalisasi.

8) Kolom terhitung/tergenerasi (dipelihara DB)

  • Biarkan DB memelihara nilai turunan (mis., price_with_tax) sebagai kolom terhitung/tergenerasi atau melalui indeks ekspresi. Itu akan memberi Anda baca cepat tanpa logika sinkronisasi di tingkat aplikasi.

Teknik Denormalisasi

Berikut cara paling umum untuk menambahkan redundansi terkontrol ke model ternormalisasi. Untuk masing-masing, saya akan menunjukkan apa fungsinya, kapan menggunakannya, dan cara menjaganya tetap sinkron.

Pengaturan: anggap inti ternormalisasi dengan customers, orders, order_items, products.

1) Tabel “proyeksi” yang diratakan / pra-join

Apa: Buat tabel yang mempra-join kolom yang Anda butuhkan untuk baca cepat (mis., daftar produk atau dasbor pesanan).

Kapan: Jalur panas Anda menggabungkan 3 hingga 5 tabel dengan cara yang dapat diprediksi.

Cara (PostgreSQL):

-- Read-optimised projection for a typical orders list
CREATE TABLE orders_projection (
  order_id        BIGINT PRIMARY KEY,
  created_day     DATE NOT NULL,
  customer_id     BIGINT NOT NULL,
  customer_name   TEXT  NOT NULL,
  total_amount    NUMERIC(12,2) NOT NULL
);

-- Initial backfill
INSERT INTO orders_projection (order_id, created_day, customer_id, customer_name, total_amount)
SELECT o.order_id,
       o.created_at::date AS created_day,
       c.customer_id,
       c.name AS customer_name,
       SUM(oi.quantity * oi.unit_price) AS total_amount
FROM orders o
JOIN customers c ON c.customer_id = o.customer_id
JOIN order_items oi ON oi.order_id = o.order_id
GROUP BY o.order_id, created_day, c.customer_id, c.name;

-- Index for fast filtering by day/customer
CREATE INDEX ON orders_projection (created_day, customer_id);

Opsi sinkronisasi:

  • Pekerjaan pasca-tulis (enqueue “order.updated” → hitung ulang baris)
  • Pekerjaan batch malam/15-menit (dbt/cron)

2) Kolom redundan (menyalin beberapa atribut)

Apa: Duplikasi beberapa atribut yang sering dibaca ke tabel lain untuk menghindari join (mis., orders.customer_name).

Kapan: Anda hanya butuh 1 atau 2 nilai dan tidak ingin proyeksi penuh.

Cara (contoh trigger PostgreSQL):

ALTER TABLE orders
  ADD COLUMN customer_name TEXT,
  ADD COLUMN customer_tier TEXT;

-- Keep the redundant fields correct on insert/update
CREATE OR REPLACE FUNCTION sync_order_customer_fields()
RETURNS TRIGGER AS $
BEGIN
  SELECT name, tier INTO NEW.customer_name, NEW.customer_tier
  FROM customers WHERE customer_id = NEW.customer_id;
  RETURN NEW;
END;
$ LANGUAGE plpgsql;

CREATE TRIGGER trg_orders_sync_customer
BEFORE INSERT OR UPDATE OF customer_id ON orders
FOR EACH ROW EXECUTE FUNCTION sync_order_customer_fields();

-- Propagate customer name/tier changes to existing orders
CREATE OR REPLACE FUNCTION propagate_customer_changes()
RETURNS TRIGGER AS $
BEGIN
  UPDATE orders
  SET customer_name = NEW.name,
      customer_tier = NEW.tier
  WHERE customer_id = NEW.customer_id;
  RETURN NEW;
END;
$ LANGUAGE plpgsql;

CREATE TRIGGER trg_customer_propagate
AFTER UPDATE OF name, tier ON customers
FOR EACH ROW EXECUTE FUNCTION propagate_customer_changes();

Trade-off: Baca sederhana, tetapi tulis menyebar saat pelanggan diperbarui.

3) Atribut turunan (menyimpan nilai terhitung)

Apa: Persistenkan nilai yang sebaliknya Anda hitung langsung (mis., price_with_tax, latest_comment_at, item_count).

Kapan: Perhitungannya dilakukan terus-menerus dan deterministik.

Dua pola umum:

  • Kolom ter-generasi (DB menghitung ulang saat tulis: kontrol Anda lebih sedikit, tidak ada referensi lintas tabel)
  • Kolom tersimpan + trigger/pekerjaan (Anda mengontrol kapan/bagaimana ia berubah)
-- Generated column example (Postgres 12+; same-table expressions)
ALTER TABLE products
  ADD COLUMN price_with_tax NUMERIC(12,2) GENERATED ALWAYS AS (price * 1.20) STORED;

-- Counter maintained by triggers (likes per post)
ALTER TABLE posts ADD COLUMN like_count INTEGER NOT NULL DEFAULT 0;

CREATE OR REPLACE FUNCTION inc_like_count() RETURNS TRIGGER AS $
BEGIN
  UPDATE posts SET like_count = like_count + 1 WHERE post_id = NEW.post_id;
  RETURN NEW;
END; $ LANGUAGE plpgsql;

CREATE TRIGGER trg_like_insert
AFTER INSERT ON post_likes
FOR EACH ROW EXECUTE FUNCTION inc_like_count();

CREATE OR REPLACE FUNCTION dec_like_count() RETURNS TRIGGER AS $
BEGIN
  UPDATE posts SET like_count = GREATEST(like_count - 1, 0) WHERE post_id = OLD.post_id;
  RETURN OLD;
END; $ LANGUAGE plpgsql;

CREATE TRIGGER trg_like_delete
AFTER DELETE ON post_likes
FOR EACH ROW EXECUTE FUNCTION dec_like_count();

4) Tabel agregat / ringkasan

Apa: Prahitung ringkasan seperti pendapatan harian, pengguna aktif per hari, pesanan per kategori.

Kapan: Dasbor mengulang group-by yang sama dan pemindaian mentah mahal.

CREATE TABLE daily_sales (
  sales_day    DATE PRIMARY KEY,
  gross_amount NUMERIC(14,2) NOT NULL,
  order_count  INTEGER NOT NULL
);

-- Incremental upsert for "yesterday" (run hourly/15-min)
INSERT INTO daily_sales (sales_day, gross_amount, order_count)
SELECT (o.created_at AT TIME ZONE 'UTC')::date AS sales_day,
       SUM(oi.quantity * oi.unit_price)       AS gross_amount,
       COUNT(DISTINCT o.order_id)             AS order_count
FROM orders o
JOIN order_items oi ON oi.order_id = o.order_id
WHERE o.created_at >= date_trunc('day', now() - interval '1 day')
  AND o.created_at <  date_trunc('day', now())
GROUP BY sales_day
ON CONFLICT (sales_day) DO UPDATE
  SET gross_amount = EXCLUDED.gross_amount,
      order_count  = EXCLUDED.order_count;

Opsi sinkronisasi: pekerjaan terjadwal (cron/dbt), pembaruan streaming (CDC), atau agregator berbasis peristiwa.

5) Tampilan termaterialisasi (hasil kueri yang dipersistenkan)

Apa: Simpan keluaran kueri mahal sebagai tabel fisik yang dapat Anda indeks.

Kapan: Himpunan hasilnya mahal, relatif stabil, dan Anda dapat mentolerir jendela penyegaran.

-- Expensive report
CREATE MATERIALIZED VIEW mv_category_sales AS
SELECT p.category_id,
       date_trunc('day', o.created_at) AS day,
       SUM(oi.quantity * oi.unit_price) AS revenue
FROM orders o
JOIN order_items oi ON oi.order_id = o.order_id
JOIN products p ON p.product_id = oi.product_id
GROUP BY p.category_id, date_trunc('day', o.created_at);

-- Make it fast to query
CREATE INDEX ON mv_category_sales (category_id, day);

-- Refresh patterns
-- Full, blocking:
REFRESH MATERIALIZED VIEW mv_category_sales;

-- Non-blocking reads (requires unique index on the MV):
-- 1) ensure a unique index exists (e.g., (category_id, day))
-- 2) then:
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_category_sales;

Trade-off: Anda akan mengelola irama penyegaran karena penyegaran inkremental tidak native di Postgres. Meski begitu, ini cukup mudah saat Anda menggunakan dbt atau pola “hanya-tambah + bangun ulang berjendela” kustom.

6) Proyeksi “dokumen” JSON prahitung

Apa: Simpan blob JSON terdenormalisasi yang sesuai dengan kebutuhan API/UI Anda (mis., kartu produk), bersama indeks JSON terarah.

Kapan: API Anda menyajikan bentuk yang sama berulang kali, dan Anda menginginkan satu kali baca.

CREATE TABLE product_cards (
  product_id BIGINT PRIMARY KEY,
  card_json  JSONB NOT NULL
);

-- Build or rebuild card documents
INSERT INTO product_cards (product_id, card_json)
SELECT p.product_id,
       jsonb_build_object(
         'id', p.product_id,
         'name', p.name,
         'brand', b.name,
         'price_with_tax', p.price * 1.20,
         'category', c.name
       )
FROM products p
JOIN brands b    ON b.brand_id = p.brand_id
JOIN categories c ON c.category_id = p.category_id
ON CONFLICT (product_id) DO UPDATE SET card_json = EXCLUDED.card_json;

-- GIN index for JSON queries if needed
CREATE INDEX idx_product_cards_gin ON product_cards USING GIN (card_json);

Opsi sinkronisasi: bangun ulang berbasis peristiwa saat produk/merek/kategori berubah, atau penyegaran batch sering untuk produk yang baru diperbarui.

Memilih strategi sinkronisasi (panduan cepat)

  • Trigger: Langsung dan memastikan konsistensi transaksional. Umumnya bagus untuk fan-out kecil dan volume tulis rendah tetapi dapat merusak latensi tulis jika berlebihan.
  • Dual-write aplikasi: Aplikasi menulis ke sumber + denormalisasi. Sederhana namun lebih berisiko. Anda dapat menguranginya dengan retry idempoten dan pola outbox/CDC.
  • CDC → worker: Strategi sinkron ini andal dan dapat diskalakan karena menyebarkan perubahan secara asinkron. Ideal saat konsistensi eventual dapat diterima.
  • Pekerjaan terjadwal: Opsi paling sederhana untuk agregat dan tampilan termaterialisasi. Cukup pilih jendela penyegaran yang sesuai toleransi UX Anda.

Saya menyarankan agar Anda selalu membangun observabilitas ke dalam solusi Anda. Tidak ada yang lebih buruk daripada tidak yakin apakah perubahan telah diproses atau direplikasi dengan benar. Ini juga dapat membantu Anda menghitung pemeriksaan drift dan skrip backfill.

Cara Menerapkan Denormalisasi Langkah demi Langkah

Seperti biasa, mengutak-atik basis data cukup berisiko. Saya sangat menyarankan Anda mereplikasi basis data dan mencoba menerapkan denormalisasi pada replika terlebih dahulu, untuk memastikan semuanya berperilaku seperti yang Anda harapkan. Jika Anda memiliki lingkungan pengembangan, lebih baik lagi!

Prosesnya cukup sederhana: ukur → ubah sesedikit mungkin → jaga tetap benar → verifikasi → pantau → ulangi.

1) Profilkan dan tetapkan target keberhasilan

  • Tangkap kueri persisnya (teks + parameter) dan frekuensinya.
  • Catat baseline: EXPLAIN (ANALYZE, BUFFERS) plan, latensi p95, CPU/IO, baris yang dipindai.
  • Sepakati kriteria keberhasilan (misalnya “p95 < 120 ms dengan biaya tulis ≤1,2×”).
  • Konfirmasikan toleransi konsistensi (mis., “analitik boleh basi 5 menit”).

2) Pilih denormalisasi terkecil yang berhasil

  • Idealnya coba satu kolom redundan atau satu tabel ringkasan daripada proyeksi besar.
  • Sketsa bentuk baru:
    • Kolom redundan? (yang mana, mengapa)
    • Tabel agregat atau tampilan termaterialisasi? (butir, kunci, jendela penyegaran)
  • Tentukan model konsistensi:
    • Kuat (trigger/transaksional) vs eventual (penyegaran CDC/pekerjaan/tampilan termaterialisasi).
  • Tuliskan anggaran amplifikasi tulis (berapa banyak tulis ekstra per peristiwa yang dapat diterima).

3) Rancang jalur sinkronisasi

  • Trigger (konsistensi kuat, fan-out kecil).
  • CDC/outbox → worker (berskala baik, konsistensi eventual):
    • Aplikasi menulis ke tabel sumber dan satu baris outbox dalam transaksi yang sama.
    • Worker membaca outbox dan memperbarui target terdenormalisasi secara idempoten.
  • Pekerjaan terjadwal / penyegaran MV (bagus untuk agregat):
    • Tentukan irama penyegaran, jendela, dan strategi backfill.

Idempotensi tidak bisa ditawar. Pembaruan harus aman untuk diulang (mis., UPSERT dengan prahitung deterministik). Percayalah, Anda akan berterima kasih pada diri sendiri nanti.

4) Buat, backfill, dan validasi

  1. Buat struktur (tabel, kolom, indeks, trigger/pekerjaan) tetapi biarkan aplikasi tidak berubah.
  2. Backfill dari sumber kebenaran.
  3. Validasi paritas dengan invarian:
-- Example parity check: orders_projection vs live join
SELECT COUNT(*) AS mismatches
FROM orders_projection p
JOIN orders o   ON o.order_id = p.order_id
JOIN customers c ON c.customer_id = o.customer_id
WHERE p.customer_name <> c.name
   OR p.total_amount <> (
        SELECT SUM(oi.quantity * oi.unit_price)
        FROM order_items oi WHERE oi.order_id = o.order_id
      );

4. Perbaiki ketidakcocokan; jalankan ulang hingga nol (atau dalam anggaran galat yang disepakati).

5) Alihkan dengan aman

  • Kirim perubahan aplikasi di balik feature flag atau canary (mis., 10% trafik membaca dari jalur terdenormalisasi).
  • Jalankan shadow read: hitung hasil lama di latar belakang dan bandingkan hash/agregat untuk sebagian permintaan.
  • Simpan rollback cepat (beralih kembali ke jalur ternormalisasi seketika).

6) Pantau hal yang tepat

Buat dasbor kecil di alat observabilitas pilihan Anda:

  • Baca: latensi p50/p95, baris terbaca, buffer hit, stabilitas rencana kueri.
  • Tulis: waktu tulis ekstra, galat trigger/pekerjaan, kelambatan antrean (CDC), stempel waktu penyegaran MV terakhir.
  • Kualitas data: penghitung drift (pemeriksaan paritas harian), jumlah baris yang di-backfill, % ketidakcocokan.
  • Biaya/jejak: ukuran tabel, bloat indeks, pertumbuhan ukuran MV.

7) Operasikan & iterasikan

  • Tetapkan kepemilikan (tim/sistem on-call) dan runbook (cara membangun ulang, backfill, memperbaiki). Ini sering terlewat, tetapi Anda akan ingin bisa merespons secepat mungkin saat insiden terjadi. 
  • Evaluasi ulang tiap kuartal: masih diperlukan? Masih bentuk yang tepat? Bisakah kita sederhanakan? Dokumentasikan keputusan dan tautkan ke dasbor.

Kelebihan dan Kekurangan Denormalisasi

Kita telah membahas detailnya di tabel Normalisasi vs Denormalisasi pada bagian sebelumnya, jadi berikut ringkasan cepat.

Yang Anda dapatkan

  • Baca lebih cepat dan latensi p95/p99 lebih prediktabel
  • Kueri lebih sederhana pada jalur panas (lebih sedikit join/agregasi)
  • CPU/IO per baca lebih rendah dan kueri analitik lebih murah

Biayanya

  • Amplifikasi tulis (pembaruan/penyisipan ekstra)
  • Risiko konsistensi (data basi/menyimpang kecuali Anda menyinkronkan dengan baik)
  • Pertumbuhan penyimpanan (kolom/tabel/tampilan duplikat)
  • Beban operasional (penyegaran, backfill, pemantauan)
  • Penyebaran perubahan (pembaruan skema/logika harus dicerminkan)

Alat dan Teknologi yang Mendukung Denormalisasi

Tidak semua basis data membantu Anda mendenormalisasi dengan cara yang sama. Beberapa memberi fitur kelas satu untuk mempersistenkan hasil kueri mahal sementara yang lain mengharapkan Anda membangunnya sendiri dengan pekerjaan, trigger, atau pipeline.

PostgreSQL

Postgres memberi Anda beberapa cara untuk mematerialisasi bentuk yang dioptimalkan baca. 

Tampilan termaterialisasi mempersistenkan hasil kueri, sehingga baca menjadi instan. Anda memilih kapan menyegarkan (terjadwal atau ad hoc), dan Anda bahkan dapat menyegarkan secara bersamaan sehingga pembaca tidak diblokir.

Untuk duplikasi ringan, kolom ter-generasi dan indeks ekspresi memungkinkan basis data memelihara nilai turunan dan mempercepat filter umum tanpa menulis logika sinkronisasi ekstra. 

Saat Anda memerlukan konsistensi kuat antara sumber dan salinan terdenormalisasi, Anda dapat menggunakan trigger untuk menjaga sinkron (namun ingat, trigger menambah pekerjaan pada jalur tulis!). Untuk penyegaran berbasis peristiwa atau batch, Anda dapat memasangkan Postgres dengan penjadwal (mis., pg_cron atau job runner eksternal) atau dengan replikasi logis/CDC untuk mengalirkan perubahan ke worker yang memperbarui tabel terdenormalisasi Anda secara asinkron.

Jika Anda ingin praktik langsung dan membuat basis data sendiri serta menguji denormalisasi, silasan lihat kursus PostgreSQL kami.

SQL Server

SQL Server sangat mengandalkan indexed views, semacam tampilan termaterialisasi yang selalu aktif, di mana mesin menjaga tampilan tetap sinkron pada setiap insert/update/delete, sehingga baca sangat cepat. Sisi lainnya adalah tulis kini memelihara tabel dasar dan tampilan, sehingga beban OLTP berat dapat melambat. 

Selain itu, Anda dapat memanfaatkan kolom terhitung yang dipersistenkan untuk menutup atribut turunan baris tunggal. 

Untuk pelaporan skala besar, indeks columnstore adalah alternatif denormalisasi: mereka mengompresi dan memindai dataset besar dengan sangat efisien, yang dapat menghilangkan kebutuhan menduplikasi data sejak awal.

Oracle

Materialized Views Oracle adalah opsi yang matang dengan penyegaran FAST yang menggunakan log perubahan untuk memperbarui hanya yang berubah, atau penyegaran COMPLETE saat Anda baik-baik saja membangun ulang semuanya. 

Dengan Query Rewrite, pengoptimal dapat secara transparan menggunakan materialized view Anda saat seseorang mengkueri tabel dasar, sehingga tim mendapatkan peningkatan kecepatan tanpa mengubah SQL aplikasi. Seperti biasa, baca lebih cepat berarti ada pekerjaan ekstra di suatu tempat: memelihara log dan menyegarkan tampilan meningkatkan beban tulis dan operasional.

MySQL / MariaDB

MySQL tidak memiliki tampilan termaterialisasi native, jadi denormalisasi biasanya dibangun dengan tabel fisik plus pekerjaan terjadwal atau trigger untuk menjaganya tetap terbaru. 

Kolom ter-generasi membantu untuk nilai turunan sederhana. Ini bekerja baik untuk dasbor dan ringkasan yang dapat diprediksi, namun sekali lagi, ada baiknya bersikap sengaja tentang irama penyegaran dan kompleksitas trigger agar Anda tidak tanpa sengaja membebani jalur tulis yang kritis. Banyak tim menggabungkannya dengan replika baca untuk sepenuhnya mengalihkan kueri pelaporan.

Snowflake / BigQuery (lapisan analitik)

Gudang kolumnar dibuat untuk data terdenormalisasi yang lebar dan ramah pemindaian. Anda biasanya memodelkan tabel fakta lebar dengan atribut bermanfaat yang sudah disematkan, lalu mengandalkan partisi/klastering untuk memangkas yang dipindai. 

Kedua platform mendukung tampilan termaterialisasi dan tugas/kueri terjadwal untuk menjaga agregat tetap segar tanpa menyentuh sistem OLTP. Anda menukar biaya penyimpanan dan penyegaran untuk baca yang sangat prediktabel dan murah dalam skala besar. Ini sempurna untuk dasbor dan BI.

Ini sedikit lebih maju daripada kursus PostgreSQL kami, jadi jika Anda sudah nyaman dengan basis data silahkan coba pengantar pemodelan data dengan Snowflake.

dbt (pemodelan dan orkestrasi)

dbt dikenal sebagai lapisan “infrastruktur sebagai kode” untuk analitik terdenormalisasi. Anda mendefinisikan model sekali, memilih materialisasi (tabel, tampilan, inkremental), dan membiarkan dbt menangani build, dependensi, dan pengujian. Model inkremental sangat berguna untuk tabel ringkasan yang hanya perlu data baru ditambahkan dan digabung. Ini dengan mudah menjadi salah satu alat favorit saya!

CDC & pipeline data

Saat Anda ingin perubahan aplikasi mengalir ke struktur terdenormalisasi secara andal, Change Data Capture adalah andalannya. Alat seperti Debezium (self-hosted) atau konektor terkelola (Fivetran, Airbyte) mengalirkan perubahan tingkat baris dari OLTP ke worker atau gudang yang memperbarui tabel proyeksi, penghitung, atau agregat. Ini biasanya konsisten secara eventual (bagus untuk dasbor dan umpan), dan berskala jauh lebih baik daripada memasukkan semuanya ke dalam trigger. Buat pembaruan bersifat idempoten, pantau kelambatan, dan simpan jalur backfill untuk peristiwa yang terlambat atau terlewat.

Intisari & Pemikiran Akhir

Denormalisasi adalah optimasi pragmatis yang dilapiskan di atas desain ternormalisasi, bukan penolakannya. Anda menukar penyimpanan dan kompleksitas tulis untuk baca yang lebih cepat dan lebih sederhana pada jalur yang spesifik dan dipahami dengan baik.

Ingat:

  • Mulailah dengan ternormalisasi. Gunakan denormalisasi hanya di mana kueri bernama dan terukur membutuhkannya.
  • Habiskan perbaikan yang lebih murah terlebih dahulu (indeks, penulisan ulang kueri, caching, replika, OLAP).
  • Pilih denormalisasi sekecil mungkin yang memberikan hasil (satu kolom redundan, agregat kecil, atau tampilan termaterialisasi).
  • Bersikaplah eksplisit tentang kesegaran (konsistensi kuat versus eventual) dan bangun sinkronisasi yang idempoten.
  • Ukur sebelum/sesudah, pantau drift, dan siapkan sakelar rollback.
  • Perlakukan tabel ternormalisasi sebagai sumber kebenaran! Bagian terdenormalisasi adalah salinan yang dioptimalkan baca yang dapat Anda bangun ulang.
Marie Fayard

Penulis

Marie Fayard

Saya adalah tech lead berorientasi produk yang berspesialisasi dalam mengembangkan startup tahap awal dari prototipe pertama hingga mencapai product-market fit dan seterusnya. Saya sangat penasaran dengan cara orang menggunakan teknologi, dan saya senang bekerja erat dengan para founder serta tim lintas fungsi untuk mewujudkan ide-ide berani. Saat tidak membangun produk, saya mencari inspirasi di berbagai penjuru dunia atau melepas penat di studio yoga.

Topik

Terkait

blogs

40 Pertanyaan Wawancara DBMS Teratas di 2026

blogs

Tutorial Korelasi di R

blogs

Spaghetti Plot dan Jalur Badai