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 #
| Jenis | Satu baris mewakili | Contoh di toko online |
|---|---|---|
| Transaction fact | satu peristiwa (biasanya tidak berubah lagi) | satu item dalam satu pesanan |
| Periodic snapshot | kondisi pada interval waktu tetap | stok gudang setiap akhir hari |
| Accumulating snapshot | satu proses berdurasi yang maju bertahap | satu 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:
| Varian | Masalah yang dipecahkan |
|---|---|
| Degenerate dimension | nomor dokumen unik tanpa atribut lain |
| Conformed dimension | konsistensi analitik lintas fact table dan data mart |
| Junk dimension | banyak bendera ber-kardinalitas rendah |
| Role-playing dimension | satu 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
):
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:
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:
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 | 150000Mari kita telaah baris-baris kode di atas:
- Baris 2-3 (
wp.tanggal,wk.tanggal): aliaswp(waktu pesanan) danwk(waktu kirim) memisahkan dua peran — tanpa alias, database tidak tahu tanggal dari peran mana yang diminta. - Baris 4-5 (
julianday(wk.tanggal) - julianday(wp.tanggal)): fungsijuliandaymengubah tanggal menjadi jumlah hari desimal sehingga selisihnya adalah lama proses;CAST ... AS INTEGERmembulatkannya ke hari utuh. - Baris 8-9 (dua
JOINke tabel sama): satu tabeldim_waktudipakai 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:
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:
Siti | 4
Budi | 2
Dewi | 0Pada 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.