Data warehouse adalah database terpisah dari aplikasi, yang disusun khusus untuk menjawab pertanyaan tentang banyak baris dan riwayat yang panjang. Bayangin toko dan gudangnya. Di toko depan, pelayan mengambil satu buku dari rak untuk satu pembeli: cepat, satu barang. Di gudang, barang yang sama ditumpuk per jenis, supaya waktu stok opname semua barang bisa dihitung sekaligus tanpa membongkar rak toko. Barangnya sama, susunannya beda, karena pertanyaannya beda. Database aplikasi itu toko depan, warehouse itu gudangnya.
🧪 Tebakan cepat: tabel
penjualanpunya 18 baris dan 8 kolom, jadi 144 sel. Warehouse menyimpannya per kolom. QuerySELECT kota, SUM(qty) FROM penjualan GROUP BY kotamembaca berapa sel: 144, 36, atau 18? Tebak dulu, jawabannya ada di bagian "Columnar: baca kolom yang perlu saja".
OLTP dan OLAP, sekali lagi
Di pelajaran pertama kamu sudah ketemu dua jenis kerja database. Sekarang lengkapnya:
| OLTP (Online Transaction Processing) | OLAP (Online Analytical Processing) | |
|---|---|---|
| Tujuan | transaksi operasional, saat itu juga | analisis data historis dalam jumlah besar |
| Contoh di Toko Buku | checkout menulis 1 pesanan, halaman "Pesanan saya" | "buku apa paling laku per kota per minggu?" |
| Bentuk query | banyak query kecil, tiap query menyentuh sedikit baris | sedikit query besar yang membaca hampir semua baris |
| Datanya | keadaan sekarang: harga buku hari ini | riwayat: semua pesanan sejak toko buka |
| Tool | MySQL, PostgreSQL (database aplikasi) | BigQuery, Snowflake, Redshift, ClickHouse, DuckDB |
Warehouse adalah database OLAP. Isinya disalin dari database aplikasi oleh pipeline (pelajaran ETL vs ELT), jadi query berat pemilik gak pernah memperlambat checkout.
Tabel penjualan di warehouse
Ini tabel yang dibangun pipeline Toko Buku di pelajaran sebelumnya, dari salinan mentah raw_order_lines:
CREATE TABLE penjualan (
order_id INT,
book_id INT,
tanggal DATE, -- hari pesanan
minggu DATE, -- Senin minggunya
kota VARCHAR(50), -- 'Tidak diketahui' kalau kosong
judul VARCHAR(200),
qty INT,
harga INT -- harga katalog
);
- Grain (butiran) tabelnya: satu baris = satu judul di satu pesanan. Tentukan grain dulu sebelum membuat tabel warehouse. Kalau satu tabel mencampur baris per pesanan dan baris per judul,
SUMmenghitung dobel. - Isinya 18 baris bersih: 21 buku, Rp 2.214.000.
kotadanjuduldisalin ke tiap baris, padahal di database aplikasi keduanya ada di tabelcustomersdanbooks. Ini disebut denormalisasi: tabelnya lebar dan ada data berulang, tapi pertanyaan pemilik gak butuhJOINlagi.
Columnar: baca kolom yang perlu saja
Database aplikasi menyimpan per baris (row-oriented): semua kolom satu baris ditulis berdampingan di disk. Warehouse menyimpan per kolom (columnar), seperti file Parquet di pelajaran Data Formats: semua nilai satu kolom ditulis berdampingan.
Per baris (MySQL, PostgreSQL, SQLite):
[1, 1, 2026-09-28, 2026-09-28, Jakarta, Laskar Pelangi, 1, 89000]
[2, 2, 2026-09-28, 2026-09-28, Bandung, Bumi Manusia, 1, 125000]
...
Per kolom (BigQuery, Snowflake, ClickHouse, DuckDB):
kota: [Jakarta, Bandung, Bandung, Surabaya, Jakarta, ...]
qty: [1, 1, 1, 2, 2, ...]
...
Berapa sel yang dibaca dua query ini dari 144 sel penjualan:
| Query | Per baris | Per kolom |
|---|---|---|
SELECT kota, SUM(qty) FROM penjualan GROUP BY kota |
144 sel: tiap baris dibaca utuh | 36 sel: kolom kota dan qty saja |
SELECT * FROM penjualan WHERE order_id = 6 |
8 sel dari 1 tempat (lewat index) | 8 sel dari 8 tempat, satu per kolom |
Jawabannya 36. Itu jawaban tebakannya: 2 kolom kali 18 baris. Hasilnya Bandung 5, Jakarta 9, Surabaya 5, Tidak diketahui 2. Di 1 juta baris, pelajaran Data Formats mengukur GROUP BY city 0,59 detik di CSV dan 0,009 detik di Parquet. Selain membaca lebih sedikit, kolom juga mudah dimampatkan: kota cuma punya 4 nilai berbeda, jadi tiap nilai disimpan sekali (encoding dictionary).
Baris kedua menjelaskan kenapa aplikasi tetap memakai penyimpanan per baris: membaca atau menulis satu pesanan cukup di satu tempat. Columnar cepat untuk scan, lambat untuk satu baris.
Warehouse, data mart, data lake
| Isinya | Di Toko Buku | |
|---|---|---|
| Data warehouse | semua data analitik perusahaan, bersih dan tersusun, jadi satu sumber kebenaran | raw_order_lines, penjualan, nanti juga stok dan iklan |
| Data mart | potongan warehouse untuk satu tim atau satu pertanyaan | tabel laris_mingguan untuk dashboard pemilik |
| Data lake | file mentah (Parquet, JSON Lines, CSV) di object storage seperti S3, GCS, atau R2; schema dibaca saat query | file order_lines.parquet per hari |
Di dalam warehouse, tabel biasanya berlapis: raw (salinan mentah) ke bersih ke mart (siap pakai), atau dengan nama lain bronze, silver, gold. Di Toko Buku: raw_order_lines ke penjualan ke laris_mingguan. Data lake dan lakehouse (lake yang bisa di-query seperti warehouse) dibahas di pelajaran Data Lake Architecture.
Fakta dan dimensi: sekilas
Kolom penjualan ada dua jenis:
- Fakta (fact): angka dari kejadian yang bisa dijumlah, yaitu
qtydanharga. - Dimensi (dimension): keterangan untuk mengelompokkan dan menyaring, yaitu
tanggal,minggu,kota,judul.
Di pelajaran Data Modeling (Star Schema), tabel lebar ini dipecah jadi satu tabel fakta fact_penjualan yang menyimpan angka dan kunci, dikelilingi tabel dimensi dim_buku, dim_tanggal, dan dim_pelanggan. Untuk sekarang, cukup bisa membedakan mana fakta dan mana dimensi.
💡 Data yang berubah pelan-pelan
Kolom harga diambil dari katalog books waktu tabel dibangun. Sabtu 10 Oktober, harga Bumi Manusia turun dari 125000 jadi 119000, lalu malamnya penjualan dibangun ulang penuh dari raw:
UPDATE books SET price = 119000 WHERE id = 2;
-- lalu CREATE TABLE penjualan AS SELECT ... JOIN books b ... seperti biasa
SELECT SUM(qty * harga) AS omzet FROM penjualan;
Hasilnya di SQLite 2178000, padahal sebelumnya 2214000. Ke-6 Bumi Manusia yang terjual 28 September sampai 9 Oktober dengan harga 125000 sekarang tercatat 119000: omzet minggu lalu berubah, padahal gak ada yang terjadi minggu lalu. Keterangan yang berubah pelan-pelan seperti harga, alamat, atau kota pelanggan disebut slowly changing dimension (SCD). Dua obat yang umum: simpan harga saat transaksi di baris fakta dan jangan pernah dihitung ulang, atau simpan tiap versi harga dengan kolom valid_from dan valid_to. Detailnya di pelajaran Data Modeling (Star Schema).
Load harian yang idempoten
Warehouse diisi per tanggal, satu bagian (partisi) tiap malam, dengan pola dari pelajaran ETL vs ELT:
BEGIN;
DELETE FROM penjualan WHERE tanggal = '2026-10-01';
INSERT INTO penjualan
SELECT DISTINCT r.order_id, r.book_id,
date(r.created_at) AS tanggal,
date(r.created_at, 'weekday 0', '-6 days') AS minggu,
COALESCE(r.city, 'Tidak diketahui') AS kota,
r.title AS judul,
r.qty,
b.price AS harga
FROM raw_order_lines r
JOIN books b ON b.id = r.book_id
WHERE date(r.created_at) = '2026-10-01';
COMMIT;
- Load ini cuma menyentuh tanggal 1 Oktober. Dijalankan dua kali atau sepuluh kali, tanggal itu tetap 2 baris.
- Tanggal lain gak tersentuh, jadi harga lama di baris lama aman. Tapi hati-hati: kalau tanggal lama di-load ulang (backfill) setelah harga berubah,
b.pricemembaca harga baru. - BigQuery dan Snowflake gak memaksakan primary key, jadi baris kembar gak ditolak. Idempotennya harus datang dari cara load-nya.
Pola query di warehouse
Pertanyaan analitik hampir selalu salah satu dari ini: menjumlah per waktu (GROUP BY minggu), memberi peringkat dalam kelompok (RANK(), pelajaran SQL untuk Data Engineering), atau membandingkan dengan periode sebelumnya:
SELECT kota, minggu, SUM(qty) AS buku,
LAG(SUM(qty)) OVER (PARTITION BY kota ORDER BY minggu) AS minggu_lalu
FROM penjualan
GROUP BY kota, minggu
ORDER BY kota, minggu;
| kota | minggu | buku | minggu_lalu |
|---|---|---|---|
| Bandung | 2026-09-28 | 3 | NULL |
| Bandung | 2026-10-05 | 2 | 3 |
| Jakarta | 2026-09-28 | 6 | NULL |
| Jakarta | 2026-10-05 | 3 | 6 |
| Surabaya | 2026-09-28 | 3 | NULL |
| Surabaya | 2026-10-05 | 2 | 3 |
| Tidak diketahui | 2026-09-28 | 1 | NULL |
| Tidak diketahui | 2026-10-05 | 1 | 1 |
GROUP BY kota, minggumenjumlah buku per kota per minggu.LAG(SUM(qty))mengambil angka minggu sebelumnya di kota yang sama; minggu pertama belum punya, jadiNULL.- Jakarta turun dari 6 jadi 3. Pertanyaan "naik atau turun?" ini yang paling sering ditanyakan pemilik.
Satu kebiasaan lagi: saring tanggal sedini mungkin (WHERE tanggal >= '2026-10-05'). Warehouse yang dipartisi per tanggal cuma membaca partisi yang cocok, dan di BigQuery itu langsung memotong biaya. Itu bahan pelajaran berikutnya.
⚠️ Jebakan yang sering ketemu
- Laporan berat di database aplikasi. OLTP disusun per baris untuk transaksi; scan semua baris di sana lambat dan mengganggu checkout. Salin ke warehouse.
SELECT *di warehouse. Penyimpanan per kolom cuma cepat kalau kamu menyebut kolom yang perlu.SELECT *membaca semuanya.- Membangun ulang sejarah dari katalog hari ini. Harga baru menimpa penjualan lama tanpa error. Simpan harga saat transaksi, atau riwayat harganya.
- Grain yang gak jelas. Baris per pesanan dan baris per judul dalam satu tabel membuat jumlah dobel.
TL;DR: Data warehouse adalah database OLAP yang terpisah dari aplikasi: OLTP melayani transaksi operasional saat itu juga, OLAP menganalisis data historis dalam jumlah besar. Warehouse menyimpan per kolom, jadi SELECT kota, SUM(qty) cuma membaca 36 dari 144 sel penjualan; penyimpanan per baris lebih cocok untuk membaca atau menulis satu pesanan. Data mart adalah potongan warehouse untuk satu tim, data lake adalah file mentah di object storage. Tabel penjualan itu denormalisasi dengan grain satu judul per pesanan; kolomnya fakta (qty, harga) dan dimensi (tanggal, kota, judul). Membangun ulang dari katalog yang harganya sudah berubah mengubah sejarah (Rp 2.214.000 jadi Rp 2.178.000 setelah Bumi Manusia turun harga). Isi warehouse per tanggal dengan DELETE lalu INSERT dalam satu transaksi, dan pakai pola query per waktu, peringkat, dan LAG untuk banding periode.