Grain Tabel: Memahami Arti Satu Baris Output Sebelum Menulis Query
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.
Apa itu grain tabel?
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.
Kenapa grain nentuin hasil query?
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:
- Total di dashboard lebih besar dari laporan resmi, dan selisihnya nggak konsisten tiap bulan.
- Jumlah baris hasil query lebih banyak dari yang kamu perkirakan.
- Angka berubah setelah kamu nambah satu kolom dari tabel lain, padahal kamu cuma pengin nambah label.
Gejala ketiga yang paling sering. Kamu cuma pengin nampilin nama kategori produk, terus tiba-tiba omzetnya naik 60 persen.
Gimana cara nentuin grain sebuah tabel?
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.
Apa yang terjadi kalau grain kecampur waktu JOIN?
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.
Contoh kasus: omzet Agustus toko_berkah membengkak
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.
Gimana nulis grain biar tim lain nggak salah pakai?
Tulis satu kalimat di komentar tabel atau dokumentasi model datamu, tepat di baris pertama. Formatnya selalu sama: "Satu baris = satu [entitas] per [dimensi]".
- transaksi: satu baris = satu struk penjualan
- transaksi_item: satu baris = satu produk per struk
- stok_harian: satu baris = satu produk per tanggal per gudang
- ringkasan_omzet_harian: satu baris = satu tanggal per cabang
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.
Kesalahan umum soal grain
- Nge-join dulu, mikir grain belakangan. Urutannya kebalik. Tentuin grain output yang kamu mau, baru susun query-nya.
- Nambah kolom label dari tabel anak. Cuma pengin nama kategori, tapi grainnya ikut berubah. Ambil labelnya dari tabel referensi, bukan dari tabel detail.
- Nganggep LEFT JOIN aman dari fan-out. LEFT JOIN tetap nggandain baris kiri kalau sisi kanan punya banyak pasangan. Yang beda cuma nasib baris tanpa pasangan.
- Percaya nama tabel. Tabel bernama ringkasan_penjualan belum tentu satu baris per tanggal. Cek pakai COUNT, jangan pakai asumsi.
- Nggak nulis grain di dokumentasi. Kamu inget hari ini, tapi rekanmu bulan depan nggak.
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.
FAQ
Grain tabel sama primary key itu sama?
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.
Gimana cara tau hasil query-ku fan-out atau emang datanya naik?
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.
Apa COUNT(DISTINCT ...) bisa jadi solusi fan-out?
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.
Apa tabel hasil GROUP BY punya grain baru?
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.
Kenapa istilah grain jarang diajarin di tutorial SQL pemula?
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.
Ringkasan dan langkah berikutnya
Tiga hal yang layak kamu bawa:
- Tulis grain tiap tabel dalam satu kalimat sebelum nulis query.
- Cek pakai COUNT(*) dibanding COUNT(DISTINCT kunci). Beda angka artinya grainmu bukan yang kamu kira.
- Kalau grain dua tabel beda, agregasi dulu yang halus sampai setara, baru di-join.
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.
Artikel terkait
Cara Membuat Pivot Dinamis di SQL (Panduan 2026)
Pivot dinamis di SQL ngubah baris jadi kolom tanpa kamu hardcode nama kolomnya. Ini cara bikinnya pakai CASE WHEN dan versi yang kolomnya ngikut data.
Menghitung YTD, QTD, dan MTD di SQL (Panduan 2026)
YTD, QTD, dan MTD ngukur total dari awal tahun, kuartal, atau bulan sampai hari ini. Ini cara ngitungnya di SQL pakai DATE_TRUNC, plus satu query gabungan.
Gap and Island Analysis di SQL
Gap and island analysis di SQL nyari deret data yang berturut-turut (island) dan celah yang bolong (gap). Cocok buat hitung streak login atau cari tanggal transaksi yang hilang.