MoM Growth SQL: Hitung Pertumbuhan Bulanan Otomatis
TL;DR
MoM growth (month-over-month) di SQL dihitung pakai fungsi window LAG buat ambil nilai bulan sebelumnya, lalu bandingin sama bulan sekarang. Rumusnya: (bulan_ini - bulan_lalu) / bulan_lalu * 100. Jebakan utamanya bulan yang nggak punya transaksi sama sekali — baris itu hilang dari hasil GROUP BY, dan growth kamu jadi salah.
MoM growth di SQL dihitung pakai fungsi window LAG — dia ambil omzet bulan sebelumnya, terus kamu bandingin sama bulan sekarang. Satu query, langsung dapat kolom pertumbuhan buat semua bulan.
Nggak perlu export ke Excel. Nggak perlu geser-geser kolom.
Aku dulu ngerjain ini manual tiap awal bulan. Export CSV, buka Excel, bikin kolom "bulan lalu", tarik rumus ke bawah. Empat puluh menit. Tiap bulan. Sampai aku sadar SQL bisa ngelakuin ini dalam satu query yang tinggal di-refresh.
Apa itu MoM growth?
MoM growth (month-over-month growth) adalah persentase perubahan sebuah angka dibanding bulan sebelumnya.
Rumusnya:
MoM growth (%) = (bulan_ini - bulan_lalu) / bulan_lalu × 100
Omzet Januari Rp 100 juta, Februari Rp 120 juta. MoM growth Februari = (120 - 100) / 100 × 100 = 20%.
Metrik ini favorit buat lihat momentum jangka pendek. Bisnis lagi ngegas atau lagi ngerem.
Gimana cara hitung MoM growth di SQL?
Tiga langkah:
- Grupin data per bulan pakai
DATE_TRUNCdanGROUP BY. - Ambil nilai bulan sebelumnya pakai
LAG. - Hitung selisihnya dan bagi.
Langkah 1: Agregasi per bulan
SELECT DATE_TRUNC('month', tanggal) AS bulan,
SUM(qty * harga_satuan) AS omzet
FROM transaksi
WHERE status = 'selesai'
GROUP BY DATE_TRUNC('month', tanggal)
ORDER BY bulan;
DATE_TRUNC('month', tanggal) ngubah tanggal jadi tanggal 1 di bulan itu. Jadi 15 Maret dan 28 Maret sama-sama jadi 1 Maret, dan bisa digrupin bareng.
Jangan lupa WHERE status = 'selesai'. Transaksi batal nggak boleh ikut.
Langkah 2: Ambil bulan lalu pakai LAG
LAG itu window function yang ngambil nilai dari baris sebelumnya. Tanpa self-join, tanpa subquery ribet.
LAG(omzet) OVER (ORDER BY bulan)
Baca gini: "urutin baris berdasarkan bulan, terus di tiap baris, ambil omzet dari baris di atasnya."
Baris pertama nggak punya baris di atasnya, jadi hasilnya NULL. Itu benar dan wajar.
Langkah 3: Query lengkapnya
WITH omzet_bulanan AS (
SELECT DATE_TRUNC('month', tanggal) AS bulan,
SUM(qty * harga_satuan) AS omzet
FROM transaksi
WHERE status = 'selesai'
GROUP BY DATE_TRUNC('month', tanggal)
)
SELECT bulan,
omzet,
LAG(omzet) OVER (ORDER BY bulan) AS omzet_bulan_lalu,
ROUND(
(omzet - LAG(omzet) OVER (ORDER BY bulan))
/ NULLIF(LAG(omzet) OVER (ORDER BY bulan), 0) * 100,
1
) AS mom_growth_persen
FROM omzet_bulanan
ORDER BY bulan;
Perhatiin NULLIF(..., 0). Ini yang nyelametin kamu dari error "division by zero" kalau bulan lalu omzetnya nol. NULLIF ngubah nol jadi NULL, dan pembagian sama NULL hasilnya NULL — bukan crash.
Aku pakai fungsi LAG dan NULLIF di hampir setiap query growth yang aku tulis.
Gimana kalau mau per cabang?
Tambahin PARTITION BY. Ini bilang ke LAG: "reset hitungan tiap ganti cabang".
WITH omzet_bulanan AS (
SELECT cabang,
DATE_TRUNC('month', tanggal) AS bulan,
SUM(qty * harga_satuan) AS omzet
FROM transaksi
WHERE status = 'selesai'
GROUP BY cabang, DATE_TRUNC('month', tanggal)
)
SELECT cabang,
bulan,
omzet,
ROUND(
(omzet - LAG(omzet) OVER (PARTITION BY cabang ORDER BY bulan))
/ NULLIF(LAG(omzet) OVER (PARTITION BY cabang ORDER BY bulan), 0) * 100,
1
) AS mom_growth_persen
FROM omzet_bulanan
ORDER BY cabang, bulan;
Tanpa PARTITION BY, baris pertama cabang Bekasi bakal bandingin diri sama baris terakhir cabang Depok. Angkanya bakal ngaco total.
Jebakan terbesar: bulan yang nggak ada transaksi
Ini yang bikin angka kamu salah tanpa error apa pun.
Bayangin cabang Bogor tutup renovasi selama Februari 2026. Nol transaksi. Hasil GROUP BY kamu:
| bulan | omzet |
|---|---|
| 2026-01-01 | 52.000.000 |
| 2026-03-01 | 48.000.000 |
Februari nggak ada barisnya. Jadi LAG di baris Maret ngambil Januari, bukan Februari.
Hasilnya: growth Maret dilaporin -7,7% dibanding Januari. Padahal yang bener, Maret naik dari nol.
Solusinya: bikin deret bulan lengkap dulu, lalu LEFT JOIN ke data.
WITH deret_bulan AS (
SELECT generate_series(
DATE '2025-01-01',
DATE '2026-06-01',
INTERVAL '1 month'
)::date AS bulan
),
omzet_bulanan AS (
SELECT DATE_TRUNC('month', tanggal)::date AS bulan,
SUM(qty * harga_satuan) AS omzet
FROM transaksi
WHERE status = 'selesai'
GROUP BY DATE_TRUNC('month', tanggal)
)
SELECT d.bulan,
COALESCE(o.omzet, 0) AS omzet,
ROUND(
(COALESCE(o.omzet, 0) - LAG(COALESCE(o.omzet, 0)) OVER (ORDER BY d.bulan))
/ NULLIF(LAG(COALESCE(o.omzet, 0)) OVER (ORDER BY d.bulan), 0) * 100,
1
) AS mom_growth_persen
FROM deret_bulan d
LEFT JOIN omzet_bulanan o ON d.bulan = o.bulan
ORDER BY d.bulan;
generate_series itu fungsi PostgreSQL. Di BigQuery pakai GENERATE_DATE_ARRAY. Di MySQL kamu harus bikin tabel kalender manual.
Contoh kasus: MoM growth toko_berkah
Dataset toko_berkah dari ngulikdata: 12.400 transaksi, 3 cabang, Januari 2025 sampai Juni 2026.
Ini hasil query MoM growth buat 6 bulan terakhir:
| Bulan | Omzet | MoM growth |
|---|---|---|
| Jan 2026 | Rp 148.200.000 | — |
| Feb 2026 | Rp 131.900.000 | -11,0% |
| Mar 2026 | Rp 172.400.000 | +30,7% |
| Apr 2026 | Rp 189.100.000 | +9,7% |
| Mei 2026 | Rp 155.300.000 | -17,9% |
| Jun 2026 | Rp 161.800.000 | +4,2% |
Lonjakan Maret +30,7% itu Ramadan. Anjlok Mei -17,9% itu bulan setelah Lebaran, orang lagi ngerem belanja.
Ini poin penting soal MoM: dia kena efek musiman. Kalau kamu lihat -17,9% terus panik, kamu salah baca. Bandingin Mei 2026 sama Mei 2025 (YoY), dan kamu bakal lihat bisnisnya sebenernya tumbuh 12%.
MoM buat momentum. YoY buat kesehatan bisnis. Pakai dua-duanya.
Kesalahan umum
Lupa NULLIF. Satu bulan omzetnya nol, query kamu langsung error "division by zero". Ini nemu kamu di production, bukan di testing.
Lupa PARTITION BY waktu ngitung per grup. Baris pertama tiap cabang bakal ambil data dari cabang lain. Angkanya masuk akal secara visual, tapi salah total.
ORDER BY di dalam OVER nggak sama dengan ORDER BY hasil akhir. Dua-duanya perlu ada. ORDER BY di dalam OVER nentuin urutan LAG. ORDER BY di akhir cuma ngatur tampilan.
Ngelapor MoM growth doang. Angka +30% kelihatan hebat sampai kamu sadar bulan sebelumnya anjlok. Selalu tampilin angka mentahnya di sebelah persentase.
FAQ
Apa itu MoM growth?
MoM growth atau month-over-month growth adalah persentase perubahan sebuah metrik dibanding bulan sebelumnya. Rumusnya nilai bulan ini dikurangi bulan lalu, dibagi bulan lalu, dikali 100. Kalau omzet Januari 100 juta dan Februari 120 juta, MoM growth Februari adalah 20 persen. Ini metrik favorit buat lihat momentum jangka pendek.
Fungsi apa yang dipakai buat MoM growth di SQL?
LAG, salah satu window function. LAG ambil nilai dari baris sebelumnya tanpa perlu self-join. Sintaksnya LAG(omzet, 1) OVER (ORDER BY bulan). Fungsi ini jalan di PostgreSQL, MySQL 8 ke atas, SQL Server, BigQuery, dan SQLite 3.25 ke atas. MySQL 5.7 belum punya, jadi kamu harus pakai self-join.
Gimana kalau ada bulan yang nggak ada transaksi?
Bulan itu bakal hilang dari hasil GROUP BY, dan LAG bakal ambil bulan sebelumnya yang salah. Misalnya Februari kosong, LAG di bulan Maret bakal ambil Januari, bukan Februari. Solusinya bikin deret bulan lengkap dulu pakai generate_series, lalu LEFT JOIN ke data transaksi.
Kenapa hasil growth-ku muncul NULL di baris pertama?
Karena baris pertama nggak punya bulan sebelumnya buat dibandingin. LAG ngembaliin NULL di situ, dan NULL dibagi apa pun tetap NULL. Ini normal dan benar. Kalau kamu mau tampilin sebagai kosong atau tanda strip di dashboard, bungkus pakai COALESCE atau biarkan NULL supaya jujur.
Bedanya MoM growth sama YoY growth apa?
MoM bandingin sama bulan sebelumnya, YoY bandingin sama bulan yang sama tahun lalu. MoM bagus buat lihat momentum cepat, tapi kena efek musiman. Penjualan Januari selalu turun habis Desember, dan itu bukan berarti bisnis lagi jelek. YoY lebih tahan musim karena bandingin periode yang setara.
Penutup
Yang perlu kamu bawa pulang:
LAG + NULLIF = query MoM growth yang aman. Tambahin PARTITION BY kalau mau per cabang. Bikin deret bulan lengkap kalau ada kemungkinan bulan kosong.
Simpan query ini jadi view atau saved question di dashboard kamu. Sekali bikin, tinggal refresh tiap bulan.
Mau lanjut ke window function lain? Baca panduan window function SQL — di situ ada ROW_NUMBER, RANK, dan running total yang sering kepakai bareng LAG.
Detail lengkap perilaku window function ada di dokumentasi resmi PostgreSQL.
Mau praktek langsung? Mulai latihan SQL gratis
Latihan interaktif, langsung di browser.
Artikel Terkait
Churn Rate SQL: Deteksi Pelanggan yang Kabur Sejak Dini
Studi kasus toko_berkah: 1.240 pelanggan hilang dalam setahun, dan sinyalnya udah kelihatan 2 bulan sebelumnya. Ini query yang bikin kamu nangkap mereka lebih awal.
YoY Growth SQL: Bandingkan Performa dengan Tahun Lalu
Hitung YoY growth di SQL pakai LAG atau self-join, lengkap dengan cara ngindarin pembagi nol dan jebakan bulan yang bolong. Contohnya pakai data toko_berkah.
Customer Lifetime Value SQL: Hitung LTV dari Data Transaksi
LTV bilang berapa nilai satu pelanggan sepanjang dia jadi pelanggan kamu. Ini query SQL buat ngitungnya dari data transaksi, plus cara pakainya buat keputusan budget akuisisi.