TL;DR
Grain tabel adalah arti dari satu baris data, misalnya satu baris sama dengan satu transaksi, atau satu item dalam transaksi. Grain nentuin kolom mana yang boleh dijumlah dan gimana tabelnya boleh di-join. Kalau kamu nge-join dua tabel dengan grain berbeda tanpa sadar, baris di sisi yang lebih kasar bakal keduplikat dan hasil SUM-nya membengkak.
Grain tabel adalah arti dari satu baris data. Satu baris di tabel transaksi artinya satu struk. Satu baris di tabel item artinya satu produk di dalam struk.
Kedengeran sepele sampai kamu nge-join dua tabel itu, terus total omzet Agustus tiba-tiba jadi dua kali lipat dari yang di kasir.
Pertanyaan soal grain juga sering muncul di wawancara data analyst, biasanya dibungkus jadi "kenapa angka di dashboard ini beda sama laporan keuangan".
Di bawah ini ada cara ngecek grain dalam dua query, contoh fan-out yang bikin angka membengkak, dan pola aman buat nge-join tabel dengan grain berbeda.
Grain adalah tingkat kedetailan satu baris di sebuah tabel, ditulis sebagai kalimat "satu baris = satu ...". Grain nentuin dua hal sekaligus: kolom mana yang aman dijumlah, dan gimana tabel ini boleh disambung ke tabel lain. Tanpa grain yang jelas, semua angka hasil fungsi agregat jadi tebakan.
Istilah ini datang dari pemodelan data gudang, tapi kepakainya di semua tempat. Bahkan file Excel punya grain, cuma jarang ditulis.
Contoh grain dari beberapa tabel yang biasa ada di sistem kasir:
| Tabel | Grain | Kolom yang aman dijumlah |
|---|---|---|
| transaksi | Satu baris = satu struk | total, diskon, pajak |
| transaksi_item | Satu baris = satu produk di dalam satu struk | qty, qty x harga_satuan |
| produk | Satu baris = satu produk | Nggak ada, ini tabel referensi |
| stok_harian | Satu baris = satu produk di satu tanggal | qty_masuk, qty_keluar |
| pelanggan | Satu baris = satu pelanggan | Nggak ada |
Kolom "total" di tabel transaksi nggak boleh dijumlah setelah di-join ke transaksi_item. Nilainya bakal keulang sebanyak jumlah item di struk itu.
Karena SUM cuma nambahin apa pun yang ada di depannya, tanpa nanya barisnya duplikat atau bukan. Kalau satu struk senilai Rp 150.000 muncul di 4 baris gara-gara punya 4 item, SUM(total) ngitung Rp 600.000 buat struk itu. Query-nya jalan, nggak ada error, dan angkanya salah.
Ini yang bikin bug grain berbahaya. Nggak ada pesan merah yang muncul.
Tiga gejala yang biasanya nunjukin ada masalah grain:
Gejala ketiga yang paling sering. Kamu cuma pengin nampilin nama kategori produk, terus tiba-tiba omzetnya naik 60 persen.
Tulis dulu tebakan kamu dalam satu kalimat, lalu buktikan pakai dua query. Query pertama bandingin jumlah baris sama jumlah nilai unik dari kandidat kunci. Query kedua nyari kombinasi kolom yang muncul lebih dari sekali. Kalau dua-duanya bersih, tebakanmu benar.
-- Langkah 1: bandingin jumlah baris dengan kandidat kunci
SELECT
COUNT(*) AS total_baris,
COUNT(DISTINCT transaksi_id) AS transaksi_unik
FROM transaksi;
Kalau dua angkanya sama, berarti transaksi_id unik dan grain tabel itu satu baris satu transaksi. Kalau total_baris lebih besar, grainnya lebih halus dari tebakanmu.
-- Langkah 2: cari kombinasi kolom yang bikin baris ganda
SELECT transaksi_id, produk_id, COUNT(*) AS jumlah_baris
FROM transaksi_item
GROUP BY transaksi_id, produk_id
HAVING COUNT(*) > 1
ORDER BY jumlah_baris DESC;
Kalau query ini balik kosong, berarti kombinasi transaksi_id plus produk_id udah cukup buat nentuin satu baris. Itu grain-nya.
Kalau ada hasilnya, kamu punya dua kemungkinan. Grainnya lebih halus lagi, misalnya ada kolom nomor baris atau varian. Atau ada data duplikat beneran yang perlu dibersihin, dan caranya udah aku bahas di panduan menghapus data duplikat di SQL.
Langkah ketiga yang nggak bisa dikerjain query: tanya orang yang ngisi data. Kadang satu baris di tabel yang sama artinya beda tergantung nilai kolom status, misalnya baris retur yang qty-nya negatif.
Namanya fan-out. Waktu kamu nge-join tabel bergrain kasar ke tabel bergrain halus, tiap baris di sisi kasar dikaliin sebanyak baris pasangannya di sisi halus. Nilai yang tadinya muncul sekali jadi muncul berkali-kali, dan SUM-nya ikut membengkak.
Contoh yang bikin angka salah:
-- SALAH: total dari tabel transaksi keulang per item
SELECT
t.tanggal,
SUM(t.total) AS omzet
FROM transaksi t
JOIN transaksi_item ti ON ti.transaksi_id = t.transaksi_id
GROUP BY t.tanggal;
Versi yang benar: samain dulu grainnya sebelum digabung. Agregasi tabel yang lebih halus sampai grainnya setara, baru di-join.
-- BENAR: agregasi dulu sampai grain sama, baru join
WITH item_per_transaksi AS (
SELECT transaksi_id, SUM(qty * harga_satuan) AS nilai_item
FROM transaksi_item
GROUP BY transaksi_id
)
SELECT
t.tanggal,
SUM(t.total) AS omzet,
SUM(i.nilai_item) AS nilai_item
FROM transaksi t
JOIN item_per_transaksi i ON i.transaksi_id = t.transaksi_id
GROUP BY t.tanggal
ORDER BY t.tanggal;
Pola ini pakai CTE biar langkah agregasinya kebaca jelas. Kalau kamu belum kenal sintaksnya, ada penjelasan lengkapnya di tutorial Common Table Expression.
Cara kerja join dan urutan pemrosesannya ada di dokumentasi resmi PostgreSQL bagian table expressions.
Alternatif lain kalau kamu cuma butuh satu angka: pakai SUM(DISTINCT ...) itu jebakan, jangan dipakai. Dua struk dengan nilai persis sama bakal kehitung sekali.
Toko_berkah itu dataset latihan ngulikdata berisi transaksi warung sembako di Semarang. Bulan Agustus ada 1.412 baris di tabel transaksi dan 3.180 baris di tabel transaksi_item. Artinya rata-rata satu struk berisi 2,25 item.
Angka benar omzet Agustus dari tabel transaksi: Rp 214.850.000.
Waktu query-nya nge-join ke transaksi_item cuma buat nampilin kategori produk, hasil SUM(total) jadi Rp 483.412.500. Membengkak 2,25 kali, persis sama dengan rata-rata jumlah item per struk.
Angka pengalinya itu petunjuk paling berguna. Kalau totalmu membengkak dan rasionya mirip rata-rata jumlah baris anak per induk, hampir pasti itu fan-out, bukan data yang beneran naik.
Yang bikin kasus ini susah ketahuan: 2,25 kali itu masih kelihatan masuk akal buat orang yang nggak hafal omzet bulanan. Kalau pengalinya 40 kali, semua orang langsung curiga.
Cara cek cepatnya cuma satu baris:
SELECT COUNT(*) FROM transaksi WHERE tanggal >= '2025-08-01' AND tanggal < '2025-09-01';
-- 1412
-- Kalau hasil query laporanmu punya 3180 baris, grain-nya udah berubah.
Tulis satu kalimat di komentar tabel atau dokumentasi model datamu, tepat di baris pertama. Formatnya selalu sama: "Satu baris = satu [entitas] per [dimensi]".
Tambahin juga daftar kolom yang aman dijumlah. Ini nyelametin orang berikutnya yang buka tabelmu enam bulan lagi.
Di alur ETL, tiap tabel hasil transformasi wajib punya grain yang ditulis sebelum query-nya dibikin. Nentuin grain belakangan hampir selalu berarti nulis ulang.
Satu kebiasaan kecil yang nolong: tiap kali selesai nulis query, tanya ke diri sendiri satu baris di hasil ini artinya apa. Kalau jawabannya nggak keluar dalam lima detik, query-nya perlu dicek ulang.
Berhubungan tapi nggak identik. Primary key adalah kolom yang bikin tiap baris unik secara teknis, sementara grain adalah arti bisnis dari baris itu. Tabel bisa punya primary key berupa id acak, tapi grain aslinya kombinasi tanggal dan produk. Kalau grain dan primary key-mu nyambung, model datamu biasanya lebih gampang dipakai orang lain.
Bandingin jumlah baris sebelum dan sesudah join. Kalau jumlah barisnya naik, grainnya berubah, titik. Cek juga rasio pembengkakannya. Kalau rasio total yang membengkak mirip rata-rata jumlah baris anak per induk, itu tanda fan-out, bukan kenaikan penjualan yang nyata.
Buat ngitung jumlah transaksi, iya, COUNT(DISTINCT transaksi_id) aman. Buat penjumlahan nilai, jangan pakai SUM(DISTINCT ...), soalnya dua struk yang nilainya kebetulan sama bakal kehitung sekali doang. Cara yang benar tetap agregasi dulu di tabel yang lebih halus, baru di-join di grain yang sama.
Punya, dan grainnya persis kolom yang kamu tulis di GROUP BY. Kalau kamu GROUP BY tanggal dan cabang, grain hasilnya satu baris per tanggal per cabang. Ini yang bikin GROUP BY jadi alat utama buat mengubah grain, dari halus jadi lebih kasar sesuai kebutuhan laporanmu.
Soalnya tutorial pemula biasanya cuma pakai satu tabel, dan di satu tabel masalah grain nggak muncul. Begitu kamu nyentuh dua tabel atau lebih di data nyata, grain langsung jadi sumber bug angka nomor satu. Dari yang aku lihat, ini juga pertanyaan yang sering muncul di wawancara posisi data analyst.
Tiga hal yang layak kamu bawa:
Mau latihan langsung di dataset transaksi beneran? Coba query grain dan fan-out ini di NgulikSQL, editornya jalan di browser tanpa install apa-apa.
Mau praktek langsung? Mulai latihan SQL gratis
Latihan interaktif, langsung di browser.
Kolom waktu isinya angka gede kayak 1704067200 dan bikin bingung? Itu Unix timestamp. Ini cara ngubahnya jadi tanggal beneran di SQL, plus balik lagi.
Data transaksi tersimpan UTC, tapi laporan harus jam WIB. Kalau salah konversi, angka penjualan tengah malam bisa kecatat di tanggal yang salah. Ini cara handle timezone di SQL dengan benar.
Nambah 30 hari ke tanggal invoice, ngurangin sebulan buat cari periode lalu, ngitung selisih hari antar order. Semua itu aritmetika tanggal, dan SQL punya operator INTERVAL buat ngerjainnya.