Langkah kedua metode Kimball — deklarasi grain — sering dilewati karena terasa abstrak, padahal grain adalah keputusan paling menentukan dalam seluruh dimensional modeling. Grain menjawab satu pertanyaan: satu baris fact table mewakili apa? Jawaban yang kabur membuat campuran peristiwa berbeda menempati satu tabel, dan agregasi yang tampak benar diam-diam menghitung ganda. Chapter ini mendefinisikan grain, menegaskan posisinya sebelum modeling dimulai, lalu membuktikan lewat uji nyata bagaimana grain yang keliru merusak laporan ongkir — dan bagaimana perbaikannya memulihkan angka yang benar.

Apa Itu Grain #

Grain adalah pernyataan eksplisit tentang satuan satu baris fact table. Contoh deklarasi grain yang baik: “satu baris = satu item produk dalam satu pesanan” atau “satu baris = saldo satu rekening pada akhir hari”. Deklarasi tersebut menentukan tiga hal sekaligus: measure apa yang sah dicatat, dimensi apa yang relevan, dan agregasi apa yang valid. Grain bukan kolom di tabel — grain adalah kesepakatan yang dijaga oleh desain tabel dan dokumentasinya.

Grain yang berbeda tidak boleh berbagi satu fact table. Kuantitas produk (grain item pesanan) dan ongkir (grain pesanan) sama-sama berupa angka, tetapi keduanya menggambarkan satuan kejadian yang berbeda; mencampurkan keduanya dalam satu tabel adalah jebakan yang akan kita buktikan.

Deklarasikan Grain Sebelum Modeling #

Kimball menempatkan deklarasi grain sebagai langkah kedua, tepat setelah memilih proses bisnis dan sebelum memilih dimensi maupun fakta. Alasannya praktis: dimensi hanya bisa dipilih setelah kita tahu satuan barisnya. Kalau grain-nya “satu item dalam satu pesanan”, maka dimensi pelanggan, produk, waktu, dan toko relevan; kalau grain-nya “satu pesanan”, dimensi produk justru tidak berlaku karena satu pesanan berisi banyak produk.

Deklarasi grain juga menyaring kebutuhan analitik. Tim manajemen yang bertanya “berapa rata-rata ongkir per pesanan” butuh grain pesanan; tim merchandiser yang bertanya “berapa pendapatan per kategori produk” butuh grain item.

Contoh Grain yang Keliru #

Tabel berikut memaksakan dua grain sekaligus: baris per item pesanan, tetapi ongkir per pesanan ikut dituliskan pada setiap baris (dijalankan nyata dengan SQLite melalui modul sqlite3 Python):

sql
CREATE TABLE pesanan_campur (
    pesanan_id TEXT NOT NULL,
    pelanggan  TEXT NOT NULL,
    produk     TEXT NOT NULL,
    kategori   TEXT NOT NULL,
    qty        INTEGER NOT NULL,
    jumlah     INTEGER NOT NULL,
    ongkir     INTEGER NOT NULL
);
INSERT INTO pesanan_campur VALUES
    ('ORD-001', 'Budi', 'Mouse Wireless M10', 'Aksesoris', 2, 300000, 20000),
    ('ORD-001', 'Budi', 'Monitor 24 Inci IPS', 'Monitor', 1, 1200000, 20000),
    ('ORD-002', 'Siti', 'Keyboard Mekanik K75', 'Aksesoris', 1, 450000, 15000),
    ('ORD-003', 'Dewi', 'Mouse Wireless M10', 'Aksesoris', 1, 150000, 25000),
    ('ORD-003', 'Dewi', 'Keyboard Mekanik K75', 'Aksesoris', 1, 450000, 25000);

Ongkir ORD-001 (20000) tertulis pada dua baris karena pesanan Budi berisi dua item. Query total ongkir pun berbunyi polos:

sql
SELECT SUM(ongkir) AS total_ongkir
FROM pesanan_campur;

Output yang Dihasilkan:

text
105000

Angka tersebut keliru. Ongkir sesungguhnya hanya 20000 + 15000 + 25000 = 60000; selisih 45000 berasal dari ongkir ORD-001 dan ORD-003 yang masing-masing terhitung dua kali karena kedua pesanan berisi dua item. Kerusakan menjalar ke potongan analitik lain: agregasi ongkir per kategori pada tabel tersebut menghasilkan 85000 untuk Aksesoris dan 20000 untuk Monitor — pembagian yang tidak bermakna karena ongkir tidak pernah dibeli per kategori.

Perbaikan: Satu Grain per Fact Table #

Perbaikannya adalah memisahkan tabel sesuai grain masing-masing — grain pesanan untuk ongkir, grain item untuk pendapatan produk:

sql
CREATE TABLE fact_pesanan_header (
    pesanan_id TEXT PRIMARY KEY,
    pelanggan  TEXT NOT NULL,
    ongkir     INTEGER NOT NULL
);
CREATE TABLE fact_pesanan_detail (
    pesanan_id TEXT NOT NULL REFERENCES fact_pesanan_header(pesanan_id),
    produk     TEXT NOT NULL,
    kategori   TEXT NOT NULL,
    qty        INTEGER NOT NULL,
    jumlah     INTEGER NOT NULL
);
INSERT INTO fact_pesanan_header VALUES
    ('ORD-001', 'Budi', 20000),
    ('ORD-002', 'Siti', 15000),
    ('ORD-003', 'Dewi', 25000);
INSERT INTO fact_pesanan_detail VALUES
    ('ORD-001', 'Mouse Wireless M10', 'Aksesoris', 2, 300000),
    ('ORD-001', 'Monitor 24 Inci IPS', 'Monitor', 1, 1200000),
    ('ORD-002', 'Keyboard Mekanik K75', 'Aksesoris', 1, 450000),
    ('ORD-003', 'Mouse Wireless M10', 'Aksesoris', 1, 150000),
    ('ORD-003', 'Keyboard Mekanik K75', 'Aksesoris', 1, 450000);

Total ongkir kini dihitung pada tabel yang grain-nya memang satu baris per pesanan:

sql
SELECT SUM(ongkir) AS total_ongkir
FROM fact_pesanan_header;

Output yang Dihasilkan:

text
60000

Angka 60000 cocok dengan penjumlahan manual — setiap ongkir terhitung tepat sekali. Sementara itu, pertanyaan pendapatan per kategori dijawab di tabel detail yang grain-nya sesuai:

sql
SELECT kategori, SUM(jumlah) AS pendapatan
FROM fact_pesanan_detail
GROUP BY kategori
ORDER BY kategori;

Output yang Dihasilkan:

text
Aksesoris | 1350000
Monitor | 1200000

Kedua output di atas berasal dari data yang sama dengan tabel campuran — hanya pemisahan grain yang berbeda. Bila analisis butuh keduanya sekaligus (misalnya margin setelah ongkir), kedua fact table dijoin lewat pesanan_id.

Grain sebagai Kontrak #

Setelah fact table dipakai dashboard dan laporan, grain berubah menjadi kontrak: konsumen di hilir berasumsi “satu baris = satu satuan grain” ketika menulis agregasi, dan pelanggaran asumsi tersebut merusak semua turunannya tanpa peringatan. Karena itu grain wajib didokumentasikan bersama tabelnya — pada komentar DDL, dokumentasi model (mis. file schema.yml di dbt), atau kamus data tim. Aturan tersebut berlaku pada engine apa pun: dokumentasi PostgreSQL maupun dokumentasi DuckDB sama-sama menyerahkan kebenaran agregasi sepenuhnya kepada perancang tabel.

Dengan grain yang terkunci, satu baris fact kini bermakna tegas. Tantangan berikutnya muncul dari kunci penghubungnya sendiri: kunci alami seperti kode produk berubah seiring waktu dan merusak history — masalah yang diselesaikan oleh Surrogate Key, chapter berikutnya.