Moving Average SQL: Haluskan Tren Data Harian
TL;DR
Moving average di SQL dihitung pakai window function AVG() OVER (ORDER BY tanggal ROWS BETWEEN 6 PRECEDING AND CURRENT ROW), yang ngerata-ratain nilai hari ini plus 6 hari sebelumnya. Gunanya buat ngilangin noise harian — kayak lonjakan weekend — biar arah tren aslinya kelihatan. Jebakan terbesarnya: tanggal yang bolong, karena ROWS ngitung baris bukan hari, jadi window kamu bisa mundur lebih jauh dari yang kamu kira.
Moving average di SQL dihitung pakai window function AVG() OVER dengan frame yang mundur beberapa baris ke belakang. Rumus dasarnya:
AVG(total) OVER (
ORDER BY tanggal
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
)
Itu rata-rata hari ini plus 6 hari sebelumnya. Dikenal sebagai 7-day moving average.
Gunanya? Data penjualan harian itu berisik. Di dataset toko_berkah, penjualan Sabtu rata-rata Rp 2,01 juta sementara Senin cuma Rp 900 ribu — selisih 123%. Kalau kamu plot mentah-mentah, yang kelihatan cuma gigi gergaji mingguan, bukan arah bisnisnya.
Moving average buang siklus itu. Yang tersisa: tren aslinya.
Apa Itu Moving Average dan Kenapa Bukan GROUP BY?
Moving average adalah rata-rata yang dihitung ulang di tiap baris pakai jendela data yang geser. Baris tanggal 7 ngerata-ratain tanggal 1-7. Baris tanggal 8 ngerata-ratain tanggal 2-8. Jendelanya maju terus.
GROUP BY gak bisa ngelakuin ini. GROUP BY mampatkan banyak baris jadi satu — kamu dapet satu angka per minggu, bukan angka bergeser per hari.
Window function beda: dia hitung agregat tapi tetep balikin semua baris. Itu bedanya.
| GROUP BY | Window function | |
|---|---|---|
| Jumlah baris hasil | Menyusut jadi 1 per grup | Sama kayak input |
| Bisa lihat nilai asli? | Nggak | Bisa, di samping hasil agregatnya |
| Cocok buat | Total per bulan | Moving average, running total, ranking |
Kalau konsep window function masih baru buat kamu, cek dulu definisi window function sebelum lanjut.
Gimana Cara Hitung Moving Average 7 Hari?
Tabel penjualan_harian di dataset toko_berkah punya 3 kolom: tanggal, kota, total.
SELECT tanggal,
total,
ROUND(AVG(total) OVER (
ORDER BY tanggal
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
), 0) AS ma_7hari
FROM penjualan_harian
ORDER BY tanggal;
Bedah tiap bagiannya:
AVG(total)— fungsi agregatnya. Bisa diganti SUM, MAX, COUNT.OVER (...)— yang bikin ini jadi window function, bukan agregat biasa.ORDER BY tanggal— urutan di dalam window. Wajib, karena "6 hari sebelumnya" gak punya arti tanpa urutan.ROWS BETWEEN 6 PRECEDING AND CURRENT ROW— framenya. 6 baris ke belakang plus baris ini = 7 baris total.
Hasilnya di data toko_berkah:
tanggal total ma_7hari
2026-03-01 1850000 1850000 <- window baru 1 baris
2026-03-02 920000 1385000 <- window 2 baris
2026-03-03 1010000 1260000
2026-03-04 1140000 1230000
2026-03-05 1220000 1228000
2026-03-06 1580000 1286667
2026-03-07 1970000 1384286 <- window penuh 7 baris
2026-03-08 1910000 1392857
2026-03-09 880000 1387143
2026-03-10 1080000 1382857
Lihat kolom total: naik-turun brutal antara 880 ribu sampai 1,97 juta. Kolom ma_7hari: adem di kisaran 1,38-1,39 juta.
Itu yang kamu mau. Nomor yang bisa dijawab: "bisnis kita lagi naik atau turun?" — tanpa keganggu hari Sabtu.
Kenapa 6 Baris Pertama Angkanya Aneh?
Di baris pertama, window-nya cuma punya 1 nilai. Rata-rata dari 1 angka ya angka itu sendiri. Baris kedua punya 2 nilai, dan seterusnya sampai baris ke-7 baru penuh.
Ini normal, dan biasanya dibiarin. Tapi kalau kamu mau angkanya cuma muncul waktu window udah penuh:
SELECT tanggal,
total,
CASE
WHEN ROW_NUMBER() OVER (ORDER BY tanggal) < 7 THEN NULL
ELSE ROUND(AVG(total) OVER (
ORDER BY tanggal
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
), 0)
END AS ma_7hari
FROM penjualan_harian
ORDER BY tanggal;
Enam baris pertama jadi NULL. Chart kamu bakal mulai gambar garis MA dari hari ke-7. Lebih jujur, karena kamu gak bilang "rata-rata 7 hari" untuk angka yang cuma pakai 2 hari.
Jebakan Terbesar: Tanggal yang Bolong
Ini yang bikin banyak moving average diam-diam salah.
ROWS BETWEEN 6 PRECEDING ngitung baris, bukan hari. Kalau toko kamu tutup tanggal 3, 4, dan 5 — gak ada baris buat tanggal itu di tabel.
Baris tanggal 8 bakal mundur 6 baris, dan mendarat di tanggal 28 Februari. Window kamu sekarang nyaring 10 hari kalender, bukan 7. Query-nya gak error. Angkanya cuma salah.
Ada 2 solusi.
Solusi 1: Pakai RANGE, bukan ROWS
SELECT tanggal,
total,
ROUND(AVG(total) OVER (
ORDER BY tanggal
RANGE BETWEEN INTERVAL '6 days' PRECEDING AND CURRENT ROW
), 0) AS ma_7hari_aman
FROM penjualan_harian
ORDER BY tanggal;
RANGE ngitung berdasarkan nilai tanggalnya. Mau ada baris atau nggak, dia cuma ambil yang tanggalnya dalam 6 hari terakhir.
Didukung PostgreSQL, BigQuery, Snowflake, dan SQL Server 2022 ke atas. MySQL masih terbatas.
Solusi 2: Isi tanggal bolongnya dulu
WITH kalender AS (
SELECT generate_series('2026-03-01'::date,
'2026-03-14'::date,
'1 day')::date AS tanggal
),
lengkap AS (
SELECT k.tanggal,
COALESCE(p.total, 0) AS total
FROM kalender k
LEFT JOIN penjualan_harian p ON p.tanggal = k.tanggal
)
SELECT tanggal,
total,
ROUND(AVG(total) OVER (
ORDER BY tanggal
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
), 0) AS ma_7hari
FROM lengkap
ORDER BY tanggal;
Bikin tabel kalender lengkap, LEFT JOIN data kamu ke situ, isi yang kosong pakai COALESCE jadi 0. Sekarang ROWS aman dipakai.
Cara ini lebih ribet tapi jalan di semua database. Dan bonus: kamu jadi bisa lihat hari mana aja yang datanya hilang.
Contoh Kasus: Tren toko_berkah yang Ketutupan Promo
Data toko_berkah Maret 2026, 31 hari. Kalau dilihat mentah, penjualan kelihatan datar-datar aja — naik-turun terus, gak jelas arahnya.
Aku hitung dua moving average sekaligus: 7 hari dan 14 hari.
SELECT tanggal,
total,
ROUND(AVG(total) OVER (ORDER BY tanggal
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW), 0) AS ma_7,
ROUND(AVG(total) OVER (ORDER BY tanggal
ROWS BETWEEN 13 PRECEDING AND CURRENT ROW), 0) AS ma_14
FROM penjualan_harian
ORDER BY tanggal;
Hasilnya: MA-7 di awal Maret 1,38 juta, di akhir Maret 1,71 juta. Naik 24% dalam sebulan.
Angka itu gak kelihatan sama sekali di data mentah, karena ada 2 hari promo flash sale yang penjualannya 3x lipat. Dua hari itu bikin mata orang fokus ke spike, bukan ke garis dasarnya yang pelan-pelan naik.
Yang lebih menarik: MA-7 nyilang ke atas MA-14 di tanggal 18. Itu sinyal momentum — rata-rata jangka pendek udah lebih tinggi dari jangka menengah. Di analisis time series, crossover kayak gini sering dipakai buat nandain titik balik tren.
Moving Average per Kota
Kalau tabel kamu punya banyak kota, moving average tanpa PARTITION BY bakal nyampur.
Baris pertama Surabaya bakal ngambil 6 baris sebelumnya — yang isinya data Bandung. Angka Surabaya jadi kontaminasi.
SELECT tanggal,
kota,
total,
ROUND(AVG(total) OVER (
PARTITION BY kota
ORDER BY tanggal
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
), 0) AS ma_7hari_kota
FROM penjualan_harian
ORDER BY kota, tanggal;
PARTITION BY kota bikin window-nya reset tiap ganti kota. Bandung ngerata-ratain Bandung doang.
Kalau Datanya Masih Level Transaksi
Tabel kamu 50.000 baris transaksi, bukan 1 baris per hari? Agregasi dulu.
WITH harian AS (
SELECT tanggal, SUM(total) AS total_harian
FROM transaksi
GROUP BY tanggal
)
SELECT tanggal,
total_harian,
ROUND(AVG(total_harian) OVER (
ORDER BY tanggal
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
), 0) AS ma_7hari
FROM harian
ORDER BY tanggal;
GROUP BY jalan duluan di CTE, window function jalan di query luar. Urutan ini penting — kalau kamu paksa gabung dalam satu SELECT tanpa CTE, kebanyakan database bakal nolak.
Kesalahan Umum
1. Lupa ORDER BY di dalam OVER
Tanpa ORDER BY, gak ada urutan, jadi "6 baris sebelumnya" gak punya makna. Beberapa database kasih error, beberapa diam-diam ngasih rata-rata seluruh tabel di tiap baris.
2. ORDER BY di luar dipikir ngatur window
ORDER BY tanggal di akhir query itu buat urutan tampilan. Yang di dalam OVER() buat urutan window. Dua-duanya beda, dan dua-duanya kamu butuh.
3. Salah hitung jumlah PRECEDING
7-day MA butuh 6 PRECEDING, bukan 7. Baris saat ini juga ikut kehitung. Kalau kamu tulis 7 PRECEDING, itu 8-day MA.
4. Pakai moving average buat data yang gak punya siklus
Kalau data kamu udah halus, moving average cuma bikin tren telat kelihatan. Setiap moving average punya lag — MA-30 baru nunjukin perubahan setelah 2 mingguan.
Detail sintaks window frame per database bisa kamu cek di dokumentasi window function PostgreSQL.
FAQ
Apa bedanya ROWS sama RANGE?
ROWS ngitung baris, RANGE ngitung nilai tanggal. Kalau data harian kamu lengkap, hasilnya sama. Kalau ada tanggal bolong, ROWS bakal mundur lebih jauh dari 7 hari dan hasilnya salah.
Kenapa baris awal moving average angkanya aneh?
Window-nya belum penuh. Baris pertama cuma punya 1 nilai buat dirata-ratain. Kalau mau disembunyiin, pakai CASE WHEN ROW_NUMBER() < 7 THEN NULL.
Berapa panjang window yang bagus buat data penjualan?
7 hari buat ngilangin efek weekend. 30 hari buat arah jangka menengah. Patokan: window harus kelipatan dari siklus yang mau kamu buang.
Moving average bisa dipakai bareng GROUP BY?
Bisa, tapi GROUP BY harus di CTE dulu, window function di query luar. Digabung dalam satu SELECT bakal ditolak kebanyakan database.
Gimana bikin moving average per kota?
Tambahin PARTITION BY kota di dalam OVER-nya. Tanpa itu, baris pertama Surabaya bakal ngambil data Bandung.
Penutup
Yang perlu diinget:
- Moving average =
AVG() OVER (ORDER BY tanggal ROWS BETWEEN n-1 PRECEDING AND CURRENT ROW). Untuk MA-7, n-1 = 6. - Tanggal bolong bikin ROWS salah. Pakai RANGE, atau isi kalendernya dulu.
- Banyak kota? PARTITION BY, atau angka kamu kecampur.
Sebelum masuk ke window function, pastiin agregasi dasarnya udah lancar. Kalau AVG dan SUM masih sering bikin kamu ragu soal NULL, benerin itu dulu.
Setelah moving average jadi, biasanya langkah berikutnya bikin chart-nya. Aku bahas cara nampilinnya di Cara Membuat Line Chart Excel.
Ambil satu tabel penjualan harian kamu, jalanin query MA-7 di atas, terus bandingin sama angka mentahnya. Kalau selisih hari tersibuk sama tersepi lebih dari 50%, moving average bakal ngubah cara kamu baca data itu.
Mau praktek langsung? Mulai latihan SQL gratis
Latihan interaktif, langsung di browser.
Artikel Terkait
7 SQL Client Gratis Terbaik buat Analis Data (2026)
Nggak perlu bayar buat nulis query. Ini 7 SQL client gratis yang beneran dipakai analis, plus kelebihan dan kekurangannya masing-masing.
SQL Style Guide: Cara Nulis Query yang Enak Dibaca Tim
Sepuluh aturan SQL style guide yang bikin query kamu kebaca sama tim — dan sama kamu sendiri 6 bulan lagi. Lengkap dengan contoh sebelum-sesudah.
Take-Home Test SQL Data Analyst: Contoh Soal + Cara Ngerjainnya
Contoh soal take-home test SQL yang beneran dipakai perusahaan, dikerjain step by step — dari baca soal, nulis query, sampai nyusun insight yang bikin recruiter nengok.