Kita sudah memisahkan database analitik dari database operasional, tetapi pertanyaan besar masih menggantung: bagaimana cara menata tabel di dalam warehouse agar pertanyaan bisnis mudah dijawab? Menyalin struktur 3NF dari sistem operasional hanya memindahkan masalah — setiap laporan tetap merangkai belasan join. Ralph Kimball menjawab tantangan tersebut dengan dimensional modeling: kerangka desain yang menata data mengikuti cara manusia membaca laporan, bukan mengikuti cara aplikasi menulis transaksi. Chapter ini membahas filosofi Kimball, empat langkah desainnya, istilah inti dimensional modeling, dan klasifikasi measure yang menentukan query mana yang sah untuk dijumlahkan.

Filosofi Kimball #

Dalam buku The Data Warehouse Toolkit, Kimball meletakkan dua prinsip. Pertama, warehouse dinilai dari kemudahan penggunaannya: data harus dapat dipahami dan ditanyakan langsung oleh pengguna bisnis, bukan hanya oleh data engineer. Kedua, warehouse dibangun bertahap per proses bisnis — satu domain (penjualan, pengiriman, inventori) dimodelkan lengkap dan langsung memberi nilai, lalu domain berikutnya menyusul. Prinsip bertahap tersebut membuat warehouse menyerupai sekumpulan data marts yang konsisten, bukan satu proyek raksasa yang baru bisa dipakai setelah dua tahun.

Dibandingkan dengan pendekatan Inmon yang membangun model entitas perusahaan secara top-down, Kimball memulai dari kebutuhan analitik di lapangan. Kedua aliran menghasilkan warehouse yang baik bila dijalankan disiplin; materi pada section ini mengikuti aliran Kimball karena aliran tersebut paling banyak dipakai untuk modeling laporan dan dashboard.

Empat Langkah Desain Dimensional Modeling #

Kimball meringkatkan desain menjadi empat keputusan berurutan. Untuk toko online kita, keempatnya terlihat seperti ini:

  1. Pilih proses bisnis: aktivitas yang ingin dianalisis — misalnya proses pesanan pelanggan, bukan seluruh bisnis sekaligus.
  2. Deklarasikan grain: definisi satu baris fact table — “satu baris = satu item produk dalam satu pesanan”. Grain harus dikunci sebelum lanjut.
  3. Tentukan dimensi: sudut pandang untuk memotong data — waktu, pelanggan, produk, dan toko.
  4. Tentukan fakta: pengukuran numerik pada grain tersebut — kuantitas, harga satuan, dan jumlah baris.

Urutan tersebut ketat karena dimensi dan fakta hanya masuk akal setelah grain ditetapkan. Kita akan melangkah lebih dalam untuk grain pada chapter Grain dalam Data Model.

Star Schema vs Normalisasi 3NF #

Aspek3NF (operasional)Dimensional (analitik)
Tujuan utamaintegritas transaksikecepatan dan kemudahan analisis
Bentuk dimensidipecah ke tabel kecildigabung dalam satu tabel lebar
Join per querybanyak, bertingkatsedikit, satu lapis
Redundansidiminimalkanditerima pada dimensi
Perubahan dataUPDATE di banyak tempatdimuat ulang terjadwal

Normalisasi melindungi konsistensi saat data berubah setiap detik. Analitik tidak menulis — analitik membaca berulang kali, sehingga struktur yang meminimalkan join menang. Dimensi yang “lebar dan berulang” memang menabrak aturan 3NF, dan pelanggaran tersebut memang disengaja.

Istilah Inti: Fact, Dimension, Attribute, Measure #

IstilahArtiContoh di toko online
Fact tabletabel peristiwa terukur pada satu grainfact_pesanan
Dimension tabletabel konteks pemotong analisisdim_pelanggan, dim_produk
Attributekolom deskriptif pada dimensinama, kota, kategori
Measurekolom numerik pada fact yang bisa diagregasiqty, jumlah

Measure perlu dirinci lebih jauh karena tidak semua measure boleh dijumlahkan pada semua arah agregasi — dan kesalahan mengjumlahkan measure yang salah menghasilkan laporan yang tampak meyakinkan padahal keliru.

Measure Additive, Semi-Additive, dan Non-Additive #

Kita menguji tiga kelas measure pada fact_penjualan kecil berikut (dijalankan nyata dengan SQLite melalui modul sqlite3 Python):

sql
CREATE TABLE fact_penjualan (
    tanggal      TEXT NOT NULL,
    kategori     TEXT NOT NULL,
    qty          INTEGER NOT NULL,
    harga_satuan INTEGER NOT NULL
);
INSERT INTO fact_penjualan VALUES
    ('2025-01-10', 'Aksesoris', 2, 150000),
    ('2025-01-10', 'Monitor',   1, 1200000),
    ('2025-01-15', 'Aksesoris', 1, 450000),
    ('2025-02-05', 'Aksesoris', 1, 150000);

Measure additive boleh dijumlahkan pada semua dimensi. Kolom qty termasuk additive — total kuantitas per kategori langsung benar:

sql
SELECT kategori, SUM(qty) AS total_qty
FROM fact_penjualan
GROUP BY kategori
ORDER BY kategori;

Output yang Dihasilkan:

text
Aksesoris | 4
Monitor | 1

Measure non-additive tidak boleh dijumlahkan pada dimensi mana pun; harga_satuan dan semua rata-rata termasuk kelompok ini. Jebakannya: menghitung ulang rata-rata umum dengan menjumlahkan rata-rata per grup menghasilkan angka berbeda dari rata-rata sesungguhnya:

sql
SELECT 'rata-rata dari rata-rata' AS metode,
       CAST(AVG(rata_harga) AS INTEGER) AS hasil
FROM (SELECT AVG(harga_satuan) AS rata_harga
      FROM fact_penjualan GROUP BY kategori)
UNION ALL
SELECT 'rata-rata sesungguhnya',
       CAST(AVG(harga_satuan) AS INTEGER)
FROM fact_penjualan;

Output yang Dihasilkan:

text
rata-rata dari rata-rata | 725000
rata-rata sesungguhnya | 487500

Mari kita telaah baris-baris kode di atas:

  1. Baris 3-4 (subquery): AVG(harga_satuan) dihitung per kategori terlebih dahulu — Aksesoris (150000, 450000, 150000) menghasilkan 250000, Monitor menghasilkan 1200000.
  2. Baris 1-2 (AVG(rata_harga)): rata-rata dari kedua angka tersebut adalah (250000 + 1200000) / 2 = 725000 — tetapi grup Aksesoris punya tiga baris dan Monitor satu baris, sehingga bobotnya tidak setara.
  3. Baris 5-8 (UNION ALL kedua): rata-rata sesungguhnya dihitung dari keempat baris mentah: 1950000 dibagi 4 sama dengan 487500.

Selisih 725000 lawan 487500 bukan pembulatan; kedua metode menghasilkan jawaban berbeda untuk pertanyaan yang sama.

Measure semi-additive menempati wilayah tengah: boleh dijumlahkan pada sebagian dimensi saja. Saldo rekening adalah contoh klasik — total dana pada satu tanggal valid, tetapi menjumlahkan saldo lintas tanggal menghitung ulang uang yang sama:

sql
CREATE TABLE fact_saldo (
    tanggal  TEXT NOT NULL,
    rekening TEXT NOT NULL,
    saldo    INTEGER NOT NULL
);
INSERT INTO fact_saldo VALUES
    ('2025-03-01', 'Budi', 500000),
    ('2025-03-01', 'Siti', 300000),
    ('2025-03-02', 'Budi', 550000),
    ('2025-03-02', 'Siti', 350000);

SELECT tanggal, SUM(saldo) AS total_dana
FROM fact_saldo
GROUP BY tanggal
ORDER BY tanggal;

Output yang Dihasilkan:

text
2025-03-01 | 800000
2025-03-02 | 900000

Agregasi per tanggal menghasilkan total dana yang benar (800000, lalu 900000 setelah Budi dan Siti menabung). Sebaliknya, SELECT SUM(saldo) FROM fact_saldo menghasilkan 1700000 — angka tanpa makna karena saldo 2025-03-02 sudah mencakup saldo hari sebelumnya. Engine analitik seperti DuckDB mengeksekusi semua agregasi tersebut sama cepat, sehingga menjaga kebenaran measure sepenuhnya tanggung jawab perancang model — dan kebenaran tersebut dimulai dari menata fact serta dimension table, struktur yang akan kita bangun lengkap pada chapter Star Schema.