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.
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.
Tiga langkah:
DATE_TRUNC dan GROUP BY.LAG.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.
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.
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.
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.
Ini yang bikin angka kamu salah tanpa error apa pun.
Cabang Bogor tutup renovasi selama Februari 2026, jadi 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.
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.
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.
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.
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.
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.
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.
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.
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.
Analisa per kuartal, hari kerja, atau musim jadi ribet kalau tiap query ngitung ulang atribut tanggal. Tabel kalender nyimpen semua atribut itu sekali, biar tinggal di-JOIN.
Laporan penjualan harian sering bolong di hari tanpa transaksi. Ini cara bikin deret tanggal lengkap di SQL biar tiap hari muncul, walau nilainya nol.
Struktur organisasi tersimpan sebagai kolom id_atasan yang saling nunjuk. Ini cara narik rantai jabatan, hitung total bawahan, dan span of control cuma pakai SQL.