YoY Growth SQL: Bandingkan Performa dengan Tahun Lalu
TL;DR
YoY growth di SQL dihitung dengan rumus (nilai periode ini - nilai periode sama tahun lalu) / nilai periode sama tahun lalu, biasanya dikali 100 biar jadi persen. Cara paling bersih ngambil nilai tahun lalu adalah window function LAG dengan offset 12 di data bulanan yang udah rapi. Kalau ada bulan yang bolong atau nilainya nol, LAG bakal ngasih hasil ngaco, jadi kamu perlu generate deret bulan lengkap dan pakai NULLIF di pembagi.
YoY growth di SQL dihitung pakai rumus (nilai periode ini − nilai periode sama tahun lalu) dibagi nilai periode sama tahun lalu.
Cara paling bersih ngambil nilai tahun lalu adalah window function LAG dengan offset 12 di data bulanan.
Pertanyaan "omzet bulan ini naik gak dibanding tahun lalu" itu pertanyaan yang paling sering nongol di meeting bulanan. Dan hampir tiap analis pernah salah jawab gara-gara satu bulan bolong di data. Aku bahas query-nya, jebakannya, dan cara benerinnya.
Apa itu YoY growth?
YoY growth (year-over-year growth) adalah persentase perubahan sebuah metrik dibanding periode yang sama di tahun sebelumnya. Maret 2026 dibandingin Maret 2025. Kuartal 2 tahun ini dibandingin kuartal 2 tahun lalu.
Gunanya satu: ngilangin efek musiman. Omzet Desember hampir selalu lebih gede dari November, dan itu bukan berarti bisnisnya lagi tumbuh. Cuma karena Desember emang bulan belanja.
Dengan bandingin Desember ke Desember, faktor musimannya batal sendiri.
Gimana rumus YoY growth?
YoY growth (%) = (nilai_sekarang - nilai_tahun_lalu) / nilai_tahun_lalu * 100
Contoh angka: omzet Maret 2026 Rp 120 juta, omzet Maret 2025 Rp 100 juta.
(120.000.000 - 100.000.000) / 100.000.000 * 100 = 20%
Naik 20 persen. Gampang. Yang bikin ribet itu ngambil angka "tahun lalu"-nya dari database.
Cara hitung YoY growth pakai LAG
Langkah 1: agregat data mentah jadi omzet per bulan dulu. Ini pakai tabel transaksi dari dataset toko_berkah.
SELECT
DATE_TRUNC('month', tanggal) AS bulan,
SUM(total_harga) AS omzet
FROM transaksi
GROUP BY 1
ORDER BY 1;
Hasilnya satu baris per bulan, urut dari lama ke baru.
Langkah 2: pakai LAG buat narik nilai 12 baris sebelumnya. Karena datanya bulanan, 12 baris ke belakang = bulan yang sama tahun lalu.
WITH bulanan AS (
SELECT
DATE_TRUNC('month', tanggal) AS bulan,
SUM(total_harga) AS omzet
FROM transaksi
GROUP BY 1
)
SELECT
bulan,
omzet,
LAG(omzet, 12) OVER (ORDER BY bulan) AS omzet_tahun_lalu,
ROUND(
(omzet - LAG(omzet, 12) OVER (ORDER BY bulan))
/ NULLIF(LAG(omzet, 12) OVER (ORDER BY bulan), 0) * 100
, 1) AS yoy_persen
FROM bulanan
ORDER BY bulan;
Tiga hal yang lagi terjadi di query itu.
LAG(omzet, 12) ngambil nilai omzet dari 12 baris sebelumnya. Angka 12 itu offset-nya.
OVER (ORDER BY bulan) ngasih tau SQL urutan barisnya gimana. Tanpa ini, "12 baris sebelumnya" gak punya makna.
NULLIF(..., 0) ngubah pembagi nol jadi NULL, biar query-nya gak meledak. Lebih detail soal cara kerja window function bisa kamu cek di LAG.
Hasilnya kira-kira gini di data toko_berkah:
| bulan | omzet | omzet_tahun_lalu | yoy_persen |
|---|---|---|---|
| 2026-01-01 | 142.300.000 | 118.500.000 | 20,1 |
| 2026-02-01 | 129.800.000 | 121.200.000 | 7,1 |
| 2026-03-01 | 156.400.000 | 134.900.000 | 15,9 |
| 2026-04-01 | 171.200.000 | 127.600.000 | 34,2 |
Kenapa LAG bisa ngasih hasil ngaco?
Ini jebakan yang paling sering makan korban. LAG(omzet, 12) gak ngitung bulan. Dia ngitung baris.
Kalau di data toko_berkah bulan Juni 2025 gak ada transaksi sama sekali, baris Juni 2025 gak muncul di hasil GROUP BY. Deret bulannya jadi bolong.
Efeknya: LAG(omzet, 12) di baris Mei 2026 malah narik nilai April 2025, bukan Mei 2025. Semua angka setelah bulan bolong itu jadi geser dan salah semua.
Ada 2 cara benerin.
Cara 1: self-join lewat tanggal
Ini yang paling aman kalau kamu gak yakin data kamu lengkap. Match-nya lewat tanggal eksplisit, bukan lewat urutan baris.
WITH bulanan AS (
SELECT
DATE_TRUNC('month', tanggal) AS bulan,
SUM(total_harga) AS omzet
FROM transaksi
GROUP BY 1
)
SELECT
s.bulan,
s.omzet,
l.omzet AS omzet_tahun_lalu,
ROUND((s.omzet - l.omzet) / NULLIF(l.omzet, 0) * 100, 1) AS yoy_persen
FROM bulanan s
LEFT JOIN bulanan l
ON l.bulan = s.bulan - INTERVAL '12 months'
ORDER BY s.bulan;
Kalau bulan pasangannya gak ada, LEFT JOIN ngasih NULL. Jujur dan gak nyasar. Detail soal LEFT JOIN ada di halaman LEFT JOIN.
Cara 2: bikin deret bulan lengkap dulu
Generate semua bulan dari awal sampai akhir, terus LEFT JOIN data transaksi ke situ. Bulan tanpa transaksi dapet omzet 0, dan deretnya jadi rapat lagi. Habis itu LAG aman dipakai.
Di PostgreSQL, generate_series yang ngerjain. Di BigQuery, GENERATE_DATE_ARRAY.
Contoh kasus: YoY per kategori di toko_berkah
Angka YoY total sering nyembunyiin cerita. Bulan April 2026 di data toko_berkah naik 34,2 persen, dan pas dipecah per kategori, ceritanya beda banget.
WITH bulanan AS (
SELECT
kategori,
DATE_TRUNC('month', tanggal) AS bulan,
SUM(total_harga) AS omzet
FROM transaksi
GROUP BY 1, 2
)
SELECT
kategori,
bulan,
omzet,
ROUND(
(omzet - LAG(omzet, 12) OVER (PARTITION BY kategori ORDER BY bulan))
/ NULLIF(LAG(omzet, 12) OVER (PARTITION BY kategori ORDER BY bulan), 0) * 100
, 1) AS yoy_persen
FROM bulanan
ORDER BY kategori, bulan;
Kuncinya ada di PARTITION BY kategori. Tanpa itu, LAG bakal loncat antar kategori dan hasilnya kacau.
Hasil April 2026 per kategori:
| kategori | yoy_persen |
|---|---|
| Minuman | +61,4 |
| Sembako | +18,9 |
| Snack | +7,2 |
| Perawatan tubuh | −12,5 |
Jadi pertumbuhan 34,2 persen itu hampir semuanya ditopang minuman. Kategori perawatan tubuh malah turun 12,5 persen dan gak ada yang nyadar, karena ketutup angka total.
Ini alasan kenapa YoY total doang jarang cukup buat ngambil keputusan.
Kesalahan umum waktu hitung YoY
Lupa PARTITION BY. Kalau data kamu punya dimensi (kategori, kota, produk), LAG tanpa PARTITION BY bakal narik nilai dari grup sebelahnya. Angkanya keliatan wajar, tapi salah total.
Bandingin periode yang belum lengkap. Kalau hari ini tanggal 12 dan kamu bandingin omzet bulan berjalan sama omzet bulan penuh tahun lalu, YoY-nya pasti keliatan anjlok. Bandingin tanggal 1–12 lawan tanggal 1–12.
Bagi nol tanpa NULLIF. Kategori baru yang tahun lalu omzetnya nol bakal bikin query error di PostgreSQL. Bungkus pembagi pakai NULLIF, terus tampilin sebagai "baru" di dashboard.
Pakai persentase buat basis kecil. Naik dari Rp 200 ribu ke Rp 600 ribu itu YoY 200 persen, dan angka itu bakal jadi bintang di dashboard. Padahal nilainya cuma Rp 400 ribu. Selalu tampilin nilai absolut di sebelah persentase.
Sintaks lengkap window function bisa kamu cek di dokumentasi resmi PostgreSQL.
FAQ
Apa rumus YoY growth?
Rumusnya (nilai sekarang − nilai tahun lalu) dibagi nilai tahun lalu, terus dikali 100 buat jadi persen. Misal omzet Maret 2026 Rp 120 juta dan Maret 2025 Rp 100 juta, YoY growth-nya (120−100)/100 × 100 = 20 persen. Yang penting periodenya harus sebanding: Maret dibandingin Maret, bukan Maret dibandingin Februari.
LAG atau self-join, mana yang lebih bagus buat YoY?
LAG lebih pendek dan lebih cepat kalau datanya udah agregat per bulan tanpa bolong. Self-join lebih aman kalau ada bulan yang gak punya transaksi, soalnya kamu match-nya lewat tanggal eksplisit, bukan lewat urutan baris. Kalau kamu gak yakin datanya lengkap, pakai self-join atau generate deret bulan dulu sebelum pakai LAG.
Gimana kalau nilai tahun lalu nol?
Pembagian sama nol bakal bikin error atau NULL, tergantung databasenya. Bungkus pembagi pakai NULLIF, contohnya NULLIF(omzet_lalu, 0). Hasilnya jadi NULL, bukan error. Secara matematis growth dari nol memang gak terdefinisi, jadi NULL itu jawaban yang jujur. Di dashboard, tampilkan sebagai "baru" atau tanda strip.
Kenapa YoY lebih dipilih daripada MoM?
YoY otomatis ngebandingin bulan yang sama, jadi efek musiman ilang. Omzet Desember hampir selalu lebih tinggi dari November, dan itu gak berarti bisnisnya lagi tumbuh. Dengan YoY, kamu bandingin Desember sama Desember. MoM tetap berguna buat lihat tren jangka pendek, cuma harus sadar ada faktor musiman di dalamnya.
Apakah LAG jalan di semua database?
LAG jalan di PostgreSQL, MySQL 8.0 ke atas, SQL Server 2012 ke atas, BigQuery, Snowflake, dan SQLite 3.25 ke atas. Yang gak dapet cuma MySQL 5.7 dan yang lebih tua. Kalau kamu masih di MySQL 5.7, pakai self-join. Sintaks LAG-nya sendiri standar dan hampir gak ada bedanya antar database.
Penutup
Tiga poin yang perlu nempel.
LAG(metrik, 12) itu cara tercepat hitung YoY di data bulanan, asal deret bulannya gak bolong.
Kalau ada bulan yang kosong, pakai self-join lewat tanggal. Lebih panjang, tapi gak bakal bohong.
Pecah YoY per dimensi. Angka total sering nutupin kategori yang lagi jatuh.
Coba query-nya sendiri di dataset toko_berkah, terus utak-atik offset-nya jadi 1 buat dapet MoM. Latihan window function bisa kamu mulai dari LAG dan window function.
Lanjut baca: Churn Rate SQL — metrik lain yang juga sering salah hitung gara-gara periode yang gak sebanding.
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.
MoM Growth SQL: Hitung Pertumbuhan Bulanan Otomatis
Berhenti copy-paste omzet bulan lalu ke Excel buat ngitung growth. Satu query SQL pakai LAG bisa ngasih pertumbuhan bulanan otomatis, lengkap sampai bulan yang datanya kosong.
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.