Star schema dan snowflake schema memperkenalkan dua pemain utama: fact table dan dimension table. Kenyataannya, keduanya bukan tipe tunggal — fact table hadir dalam tiga bentuk sesuai sifat peristiwa yang direkam, dan dimension table punya varian khusus untuk masalah desain tertentu: nomor pesanan yang tidak butuh dimensi sendiri, dimensi yang dipakai bersama lintas proses, bendera-bendera kecil yang menggunung, hingga satu dimensi waktu yang memerankan dua peran sekaligus. Chapter ini membedah varian-varian tersebut, lalu menguji role-playing dimension dengan data pengiriman pesanan Budi, Siti, dan Dewi.

Tiga Jenis Fact Table #

JenisSatu baris mewakiliContoh di toko online
Transaction factsatu peristiwa (biasanya tidak berubah lagi)satu item dalam satu pesanan
Periodic snapshotkondisi pada interval waktu tetapstok gudang setiap akhir hari
Accumulating snapshotsatu proses berdurasi yang maju bertahapsatu pesanan dari checkout hingga diterima

Transaction fact adalah jenis yang paling sering kita jumpai: baris ditulis sekali saat peristiwa terjadi dan tidak pernah diubah. Periodic snapshot diamkan prosesnya — setiap akhir hari, pipeline mengambil foto kondisi (stok, saldo) dan menyimpannya sebagai baris baru, sehingga perubahan antar periode bisa dibandingkan. Accumulating snapshot justru hidup lebih lama: satu baris mewakili satu proses dari awal hingga akhir, dan kolom-kolom tahapannya (waktu checkout, waktu bayar, waktu kirim, waktu diterima) diperbarui setiap kali proses maju — cocok untuk mengukur durasi antar tahapan.

Pilihan jenis fact mengikuti sifat peristiwanya — sekali-jadi, kondisi berkala, atau perjalanan bertahap; toko online kita bisa memakai ketiganya di tiga fact table.

Dimensi Khusus dalam Praktik #

Degenerate Dimension #

Nomor pesanan ORD-001 sering muncul di fact table tanpa dimension table sendiri — keadaan tersebut disebut degenerate dimension. Nomor pesanan unik per peristiwa (kardinalitas tinggi) dan tidak membawa atribut lain, sehingga membuat dim_pesanan berisi satu kolom hanya membuang join; menyimpan nomor langsung di fact justru praktis untuk penelusuran kembali ke transaksi sumber.

Conformed Dimension #

Saat fact table penjualan dan fact table pengiriman sama-sama memakai dim_produk identik, dimensi tersebut menjadi conformed dimension. Kesamaan inilah yang membuat lintas proses bisa dianalisis dalam satu query — pendapatan per kategori dan komplain pengiriman per kategori dapat ditaruh berdampingan karena keduanya memotong dengan atribut kategori yang sama. Kimball menyebut perencanaan conformed dimension lintas data mart sebagai bus matrix.

Junk Dimension #

Atribut bendera kecil — status bayar (lunas/belum), kanal (web/aplikasi), tipe pengiriman (reguler/ekspres) — masing-masing hanya punya dua-tiga nilai. Membuat satu dimension table per bendera menghasilkan banyak tabel mungil, sementara menempelkannya ke fact menumpuk kolom. Junk dimension menggabungkan semua bendera tersebut menjadi satu tabel berisi kombinasi nilai yang benar-benar muncul; fact cukup menyimpan satu foreign key.

Empat varian tersebut dirangkum berikut:

VarianMasalah yang dipecahkan
Degenerate dimensionnomor dokumen unik tanpa atribut lain
Conformed dimensionkonsistensi analitik lintas fact table dan data mart
Junk dimensionbanyak bendera ber-kardinalitas rendah
Role-playing dimensionsatu dimensi dipakai untuk beberapa peran waktu atau entitas

Role-Playing Dimension: dim_waktu untuk Dua Peran #

Satu dimensi dapat dihubungkan ke fact lebih dari sekali dengan peran berbeda. Pesanan kita punya dua peristiwa waktu — waktu pesanan dibuat dan waktu pesanan dikirim — dan keduanya merujuk ke dim_waktu yang sama lewat dua foreign key berbeda (dijalankan nyata dengan SQLite melalui modul sqlite3 Python, mengikuti dokumentasi SQLite ):

sql
CREATE TABLE dim_waktu (
    waktu_id INTEGER PRIMARY KEY,
    tanggal  TEXT NOT NULL,
    bulan    TEXT NOT NULL
);
CREATE TABLE fact_pesanan (
    no_pesanan       TEXT NOT NULL,
    pelanggan        TEXT NOT NULL,
    waktu_pesanan_id INTEGER NOT NULL REFERENCES dim_waktu(waktu_id),
    waktu_kirim_id   INTEGER NOT NULL REFERENCES dim_waktu(waktu_id),
    jumlah           INTEGER NOT NULL
);

INSERT INTO dim_waktu VALUES
    (20250310, '2025-03-10', 'Maret'),
    (20250311, '2025-03-11', 'Maret'),
    (20250312, '2025-03-12', 'Maret'),
    (20250315, '2025-03-15', 'Maret');
INSERT INTO fact_pesanan VALUES
    ('ORD-001', 'Budi', 20250310, 20250312, 450000),
    ('ORD-002', 'Siti', 20250311, 20250315, 1200000),
    ('ORD-003', 'Dewi', 20250312, 20250312, 150000);

Kolom no_pesanan di atas sekaligus memperagakan degenerate dimension. Untuk membaca kedua peran waktu sekaligus, kita join dim_waktu dua kali dengan alias berbeda:

sql
SELECT f.no_pesanan, f.pelanggan,
       wp.tanggal AS tanggal_pesanan,
       wk.tanggal AS tanggal_kirim,
       CAST(julianday(wk.tanggal) - julianday(wp.tanggal) AS INTEGER)
           AS lama_proses_hari,
       f.jumlah
FROM fact_pesanan f
JOIN dim_waktu wp ON wp.waktu_id = f.waktu_pesanan_id
JOIN dim_waktu wk ON wk.waktu_id = f.waktu_kirim_id
ORDER BY f.no_pesanan;

Output yang Dihasilkan:

text
ORD-001 | Budi | 2025-03-10 | 2025-03-12 | 2 | 450000
ORD-002 | Siti | 2025-03-11 | 2025-03-15 | 4 | 1200000
ORD-003 | Dewi | 2025-03-12 | 2025-03-12 | 0 | 150000

Mari kita telaah baris-baris kode di atas:

  1. Baris 2-3 (wp.tanggal, wk.tanggal): alias wp (waktu pesanan) dan wk (waktu kirim) memisahkan dua peran — tanpa alias, database tidak tahu tanggal dari peran mana yang diminta.
  2. Baris 4-5 (julianday(wk.tanggal) - julianday(wp.tanggal)): fungsi julianday mengubah tanggal menjadi jumlah hari desimal sehingga selisihnya adalah lama proses; CAST ... AS INTEGER membulatkannya ke hari utuh.
  3. Baris 8-9 (dua JOIN ke tabel sama): satu tabel dim_waktu dipakai dua kali — inilah inti role-playing; database memperlakukan tiap alias sebagai salinan terpisah.

Hasilnya langsung bisa dianalisis: pesanan Budi diproses 2 hari, Siti 4 hari, dan Dewi dikirim di hari yang sama — rata-rata lama proses ketiga pesanan adalah 2 hari. Pertanyaan seperti “pesanan bulan apa yang prosesnya paling lambat” tinggal memotong wp.bulan sambil mengagregasi lama_proses_hari. Ukuran turunan tersebut bisa langsung diagregasi lebih jauh — rata-rata lama proses per pelanggan:

sql
SELECT f.pelanggan,
       CAST(AVG(julianday(wk.tanggal) - julianday(wp.tanggal))
            AS INTEGER) AS rata_lama_proses_hari
FROM fact_pesanan f
JOIN dim_waktu wp ON wp.waktu_id = f.waktu_pesanan_id
JOIN dim_waktu wk ON wk.waktu_id = f.waktu_kirim_id
GROUP BY f.pelanggan
ORDER BY rata_lama_proses_hari DESC;

Output yang Dihasilkan:

text
Siti | 4
Budi | 2
Dewi | 0

Pada data contoh, tiap pelanggan baru memiliki satu pesanan sehingga rata-rata tersebut setara dengan lama proses masing-masing; seiring data bertambah, angka yang sama otomatis menjadi rata-rata seluruh pesanan per pelanggan. Kolom turunan seperti lama_proses_hari dihitung saat query; bila query tersebut berat dan sering dijalankan, hasilnya lebih baik dihitung sekali oleh pipeline lalu disimpan sebagai tabel — pola umum di engine analitik seperti DuckDB .

Semua varian fact dan dimension di atas berdiri di atas satu fondasi: apa yang direpresentasikan satu baris fact. Keputusan tersebut disebut grain — dan grain yang keliru menggerus kebenaran seluruh laporan, sebagaimana akan kita buktikan pada chapter Grain dalam Data Model.