Dua operasi muncul hampir di setiap pipeline transformasi: menggabungkan data dari dua tabel, lalu merangkum banyak baris menjadi sedikit. Laporan “pendapatan bulanan per kota” adalah keduanya sekaligus — fakta penjualan harus diperkaya atribut kota dari dimensi, lalu diringkas dari granularitas harian menjadi bulanan. Cara menggabungkan dan merangkum menentukan biaya komputasi dan benar-tidaknya angka akhir. Chapter ini membahas strategi join untuk tabel kecil versus tabel besar, membangun agregasi bertingkat di SQLite, lalu menutup dengan jebakan klasik saat agregat diturunkan dari agregat.

Strategi Join: Tabel Kecil vs Tabel Besar #

AspekLookup dimensi kecilJoin dua tabel besar
Ukuran sisi lainratusan hingga puluhan ribu bariskedua sisi jutaan baris
Di engine terdistribusitabel kecil disalin ke semua worker (broadcast)data dikirim ulang antar worker (shuffle)
Biaya jaringankecil — satu kali salinbesar — sebanding ukuran data
Di warehouse / SQLiteJOIN biasa; engine menangani sendiriJOIN tetap benar, tetapi butuh waktu dan index
Praktik terbaikjoin dimensi ke tabel fakta di mana pun perlukumpulkan data ke satu tempat, minimalkan join berulang

Kasus paling umum di warehouse adalah join antara fakta besar dan dimensi kecil — penjualan jutaan baris bergabung dengan tabel kota yang isinya belasan baris. Engine memenangkan kasus ini dengan mudah: dimensi yang kecil bisa dibaca penuh ke memori (atau disalin ke setiap worker di sistem terdistribusi), lalu setiap baris fakta tinggal mencocokkan kuncinya. Sebaliknya, join dua tabel besar memaksa engine memindahkan data antar node; bila pipeline kita melakukan join besar berulang kali, lebih hemat merancangnya sekali dan menyimpan hasilnya.

Sebelum menulis join, periksa juga arah penggunaannya. Bila dimensi dipakai berulang kali oleh banyak query, materialisasikan hasil join atau simpan tabel ringkas agar pekerjaan tidak diulang pada setiap eksekusi. Bila dimensi berubah sepanjang hari, pastikan snapshot yang dipakai konsisten dalam satu proses — menggabungkan fakta pagi dengan dimensi sore menghasilkan laporan yang tidak bisa direproduksi.

Contoh: Fakta Harian Menjadi Agregat Bulanan #

Data berikut adalah fakta penjualan harian selama dua bulan beserta dimensi kota. Tujuannya: satu tabel ringkas berisi total pendapatan dan jumlah hari aktif per bulan per kota, lengkap dengan atribut pulau dari dimensi.

python join dan agregasi bertingkat di SQLite
import sqlite3

koneksi = sqlite3.connect(":memory:")
koneksi.execute("CREATE TABLE dim_kota (kota TEXT PRIMARY KEY, pulau TEXT)")
koneksi.executemany("INSERT INTO dim_kota VALUES (?, ?)", [
    ("Jakarta", "Jawa"),
    ("Bandung", "Jawa"),
    ("Surabaya", "Jawa"),
])
koneksi.execute(
    "CREATE TABLE fakta_penjualan_harian (tanggal TEXT, kota TEXT, total INTEGER)"
)
koneksi.executemany("INSERT INTO fakta_penjualan_harian VALUES (?, ?, ?)", [
    ("2026-09-01", "Jakarta", 1250000),
    ("2026-09-02", "Bandung", 640000),
    ("2026-09-03", "Surabaya", 530000),
    ("2026-09-04", "Jakarta", 980000),
    ("2026-09-05", "Bandung", 715000),
    ("2026-09-06", "Jakarta", 1420000),
    ("2026-09-07", "Surabaya", 1110000),
    ("2026-10-01", "Jakarta", 1180000),
    ("2026-10-02", "Bandung", 830000),
    ("2026-10-03", "Surabaya", 470000),
    ("2026-10-04", "Jakarta", 1360000),
    ("2026-10-05", "Bandung", 690000),
    ("2026-10-06", "Surabaya", 580000),
    ("2026-10-07", "Jakarta", 1050000),
])

# Join (lookup dimensi kecil) + agregasi bertingkat: harian -> bulanan per kota
koneksi.execute("""
    CREATE TABLE agregat_bulanan AS
    SELECT
        substr(f.tanggal, 1, 7) AS bulan,
        d.kota,
        d.pulau,
        SUM(f.total) AS total_bulanan,
        COUNT(*) AS hari_aktif
    FROM fakta_penjualan_harian AS f
    JOIN dim_kota AS d ON f.kota = d.kota
    GROUP BY substr(f.tanggal, 1, 7), d.kota, d.pulau
    ORDER BY bulan, d.kota
""")
for baris in koneksi.execute("SELECT * FROM agregat_bulanan"):
    print(baris)
koneksi.close()

Output yang Dihasilkan:

text
('2026-09', 'Bandung', 'Jawa', 1355000, 2)
('2026-09', 'Jakarta', 'Jawa', 3650000, 3)
('2026-09', 'Surabaya', 'Jawa', 1640000, 2)
('2026-10', 'Bandung', 'Jawa', 1520000, 2)
('2026-10', 'Jakarta', 'Jawa', 3590000, 3)
('2026-10', 'Surabaya', 'Jawa', 1050000, 2)

Mari kita telaah bagian-bagian kunci di atas:

  1. Baris 4-9 (dimensi kecil): dim_kota berisi tiga baris — inilah sisi lookup: dimuat sekali, dipakai untuk setiap baris fakta.
  2. Baris 13-28 (fakta harian): empat belas baris, dua baris per kota per bulan ke atas; pada data nyata, tabel seperti ini berisi jutaan baris.
  3. Baris 34 (ekstraksi bulan): substr(f.tanggal, 1, 7) memotong "2026-09-01" menjadi "2026-09" — kunci agregasi bulanan lahir dari kolom tanggal tanpa tabel kalender tambahan.
  4. Baris 40 (join): JOIN dim_kota ON f.kota = d.kota menempelkan atribut pulau ke setiap baris fakta sebelum diringkas.
  5. Baris 41 (kelompok): GROUP BY memakai kunci bulan + kota + pulau; SUM menjumlahkan total, COUNT(*) menghitung hari aktif — Jakarta September tampil 3.650.000 dari 1.250.000 + 980.000 + 1.420.000.
  6. Baris 42 (urutan hasil): ORDER BY bulan, d.kota membuat tabel ringkasan terbaca kronologis — bulan September lengkap lebih dulu, baru Oktober.

Verifikasi silang dua angka output: total Jakarta September (3.650.000) dibagi hari aktif (3) memberi rata-rata harian 1.216.667 — konsisten dengan tiga baris fakta sumbernya.

Jebakan Agregat dari Agregat #

Ketika laporan bulanan sudah tersimpan, godaan berikutnya adalah membangun laporan tahunan dari tabel bulanan. Untuk SUM operasi itu aman: jumlah tahunan memang penjumlahan dua belas jumlah bulanan. Bahaya muncul pada AVG — rata-rata tahunan bukan rata-rata dari dua belas rata-rata bulanan, kecuali setiap bulan berisi jumlah hari yang sama persis.

Kolom hari_aktif pada contoh bukan pelengkap. Kolom tersebut menyimpan pembagi untuk rata-rata di masa depan: setiap kali laporan baru meminta rata-rata pendapatan harian, SUM(total_bulanan) / SUM(hari_aktif) menghasilkan angka yang benar tanpa membuka ulang tabel fakta.

Agregasi bertingkat yang aman menyimpan dua kolom sejak awal: nilai terjumlah (SUM) dan pembagi (COUNT). Dari pasangan tersebut, rata-rata pada granularitas mana pun bisa dihitung ulang dengan benar. Pola GROUP BY dan fungsi agregasi yang dipakai di sini dibahas lebih rinci pada chapter GROUP BY dan HAVING di section Dasar SQL, sedangkan rujukan sintaks lengkap ada di referensi bahasa SQLite .

Satu masalah belum tersentuh sepanjang section ini: apa yang terjadi bila join dan agregat tersebut menerima baris yang sama dua kali? Duplikat seperti itu tidak terlihat dari query — jumlahnya tetap benar secara sintaks, tetapi salah secara bisnis. Chapter berikutnya, Deduplikasi Data, membahas dari mana duplikat datang dan dua cara membersihkannya.