Moving Average SQL: Haluskan Tren Data Harian
Blog/Tutorial SQL/Moving Average SQL: Haluskan Tren Data Harian

Moving Average SQL: Haluskan Tren Data Harian

BimaBima
·24 April 2026·10 menit baca

Penulis

Bima

Bima

Founder & Data Professional

Bagikan

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 BYWindow function
Jumlah baris hasilMenyusut jadi 1 per grupSama kayak input
Bisa lihat nilai asli?NggakBisa, di samping hasil agregatnya
Cocok buatTotal per bulanMoving 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.

Coba Langsung

Mau praktek langsung? Mulai latihan SQL gratis

Latihan interaktif, langsung di browser.

Buka NgulikSQL →
Bagikan:
Bima
Ditulis oleh

Bima

Founder & Data Professional

Founder Ngulik Data. Passionate about making data analysis accessible for everyone.

Artikel Terkait

7 SQL Client Gratis Terbaik buat Analis Data (2026)
Tutorial SQL
10 Juli 2026•9 menit baca

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.

BimaBima
SQL Style Guide: Cara Nulis Query yang Enak Dibaca Tim
Tutorial SQL
7 Juli 2026•9 menit baca

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.

BimaBima
Take-Home Test SQL Data Analyst: Contoh Soal + Cara Ngerjainnya
Tutorial SQL
4 Juli 2026•11 menit baca

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.

BimaBima
Kembali ke Blog
Ngulik Data logoNgulik Data

Platform edukasi data lengkap untuk professionals Indonesia. Belajar SQL, Data Analysis, dan lebih banyak lagi dengan praktek langsung dan feedback real-time.

Copyright © 2026 - All rights reserved

LINKS
SupportPricingDatasetBlogAffiliates
LEGAL
Terms of servicesPrivacy policy
Ngulik Data
DatasetLeaderboardBlogStore