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.
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.
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.
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;
Ada tiga hal yang lagi terjadi di query ini.
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 |
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.
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.
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.
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.
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.
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 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.
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.
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.
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.
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.
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.