Pendapatan bulanan jarang bergerak mulus: satu bulan naik karena pesanan besar, bulan berikutnya turun karena musim sepi, dan angka satu bulan saja hampir selalu menyesatkan pembaca laporan. Analis mengatasinya dengan moving average — rata-rata bergerak yang dihitung dari beberapa periode terakhir dan bergeser satu bulan setiap kali data baru datang, sehingga lonjakan sekali muncul teredam dan tren jangka menengah menjadi terbaca. Di PostgreSQL, pola ini ditulis dengan avg(...) OVER (ORDER BY ... ROWS BETWEEN n PRECEDING AND CURRENT ROW) — kalimat yang sekaligus memperkenalkan konsep window frame: batasan jumlah baris yang dilihat oleh fungsi jendela untuk tiap baris hasil. Chapter ini membangun rata-rata bergerak tiga bulan pada data penjualan, membandingkan beberapa ukuran jendela, lalu memakai frame yang sama untuk agregat lain seperti sum, min, dan max.
Tabel Latihan #
Kita pakai tabel penjualan_bulanan yang diperpanjang sampai delapan bulan — Januari sampai Agustus 2026 — dengan tiga produk yang sama seperti chapter sebelumnya. Delapan bulan cukup untuk melihat perilaku frame di awal rangkaian sekaligus tren di tengahnya:
-- tabel latihan: pendapatan bulanan per produk Toko Maju Jaya, Januari-Agustus 2026
CREATE TABLE penjualan_bulanan (
id integer PRIMARY KEY,
produk text NOT NULL,
bulan date NOT NULL,
pendapatan numeric(12,2) NOT NULL
);
INSERT INTO penjualan_bulanan (id, produk, bulan, pendapatan) VALUES
( 1, 'Gula Pasir', DATE '2026-01-01', 3200000.00),
( 2, 'Kopi Gayo', DATE '2026-01-01', 9200000.00),
( 3, 'Teh Melati', DATE '2026-01-01', 4100000.00),
( 4, 'Gula Pasir', DATE '2026-02-01', 3600000.00),
( 5, 'Kopi Gayo', DATE '2026-02-01', 9800000.00),
( 6, 'Teh Melati', DATE '2026-02-01', 4300000.00),
( 7, 'Gula Pasir', DATE '2026-03-01', 3100000.00),
( 8, 'Kopi Gayo', DATE '2026-03-01', 8900000.00),
( 9, 'Teh Melati', DATE '2026-03-01', 4600000.00),
(10, 'Gula Pasir', DATE '2026-04-01', 3800000.00),
(11, 'Kopi Gayo', DATE '2026-04-01', 10200000.00),
(12, 'Teh Melati', DATE '2026-04-01', 4400000.00),
(13, 'Gula Pasir', DATE '2026-05-01', 3500000.00),
(14, 'Kopi Gayo', DATE '2026-05-01', 10800000.00),
(15, 'Teh Melati', DATE '2026-05-01', 4800000.00),
(16, 'Gula Pasir', DATE '2026-06-01', 3900000.00),
(17, 'Kopi Gayo', DATE '2026-06-01', 11500000.00),
(18, 'Teh Melati', DATE '2026-06-01', 5100000.00),
(19, 'Gula Pasir', DATE '2026-07-01', 3700000.00),
(20, 'Kopi Gayo', DATE '2026-07-01', 11100000.00),
(21, 'Teh Melati', DATE '2026-07-01', 5000000.00),
(22, 'Gula Pasir', DATE '2026-08-01', 4000000.00),
(23, 'Kopi Gayo', DATE '2026-08-01', 11900000.00),
(24, 'Teh Melati', DATE '2026-08-01', 5300000.00);
SELECT id, produk, bulan, pendapatan
FROM penjualan_bulanan
ORDER BY bulan, produk;Output yang Dihasilkan:
CREATE TABLE
INSERT 0 24
id | produk | bulan | pendapatan
----+------------+------------+-------------
1 | Gula Pasir | 2026-01-01 | 3200000.00
2 | Kopi Gayo | 2026-01-01 | 9200000.00
3 | Teh Melati | 2026-01-01 | 4100000.00
4 | Gula Pasir | 2026-02-01 | 3600000.00
5 | Kopi Gayo | 2026-02-01 | 9800000.00
6 | Teh Melati | 2026-02-01 | 4300000.00
7 | Gula Pasir | 2026-03-01 | 3100000.00
8 | Kopi Gayo | 2026-03-01 | 8900000.00
9 | Teh Melati | 2026-03-01 | 4600000.00
10 | Gula Pasir | 2026-04-01 | 3800000.00
11 | Kopi Gayo | 2026-04-01 | 10200000.00
12 | Teh Melati | 2026-04-01 | 4400000.00
13 | Gula Pasir | 2026-05-01 | 3500000.00
14 | Kopi Gayo | 2026-05-01 | 10800000.00
15 | Teh Melati | 2026-05-01 | 4800000.00
16 | Gula Pasir | 2026-06-01 | 3900000.00
17 | Kopi Gayo | 2026-06-01 | 11500000.00
18 | Teh Melati | 2026-06-01 | 5100000.00
19 | Gula Pasir | 2026-07-01 | 3700000.00
20 | Kopi Gayo | 2026-07-01 | 11100000.00
21 | Teh Melati | 2026-07-01 | 5000000.00
22 | Gula Pasir | 2026-08-01 | 4000000.00
23 | Kopi Gayo | 2026-08-01 | 11900000.00
24 | Teh Melati | 2026-08-01 | 5300000.00
(24 rows)Rata-rata Bergerak Tiga Bulan #
Query berikut menyaring rangkaian pendapatan Kopi Gayo, lalu menghitung rata-rata dari baris saat ini ditambah dua baris sebelumnya. Fungsi round membulatkan hasilnya ke dua angka desimal supaya kolom tetap enak dibaca:
-- rata-rata bergerak 3 bulan pada rangkaian pendapatan Kopi Gayo
SELECT bulan,
pendapatan,
round(avg(pendapatan) OVER (ORDER BY bulan
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW), 2) AS rata3_bulan
FROM penjualan_bulanan
WHERE produk = 'Kopi Gayo'
ORDER BY bulan;Output yang Dihasilkan:
bulan | pendapatan | rata3_bulan
------------+-------------+-------------
2026-01-01 | 9200000.00 | 9200000.00
2026-02-01 | 9800000.00 | 9500000.00
2026-03-01 | 8900000.00 | 9300000.00
2026-04-01 | 10200000.00 | 9633333.33
2026-05-01 | 10800000.00 | 9966666.67
2026-06-01 | 11500000.00 | 10833333.33
2026-07-01 | 11100000.00 | 11133333.33
2026-08-01 | 11900000.00 | 11500000.00
(8 rows)Bagian baru ada di klausa ROWS BETWEEN 2 PRECEDING AND CURRENT ROW. Kalimat tersebut adalah frame specification: ia membatasi himpunan baris yang dirata-ratakan menjadi “dua baris sebelum baris ini, plus baris ini” — total maksimal tiga baris. Untuk baris April, frame berisi Februari (9.800.000), Maret (8.900.000), dan April (10.200.000), sehingga rata-ratanya 9.633.333,33. Begitu query berpindah ke baris Mei, frame ikut bergeser: Februari keluar, Mei masuk.
Sekarang perhatikan tiga baris pertama hasilnya — di sinilah perilaku baris tepi (boundary) muncul:
- Januari — frame hanya berisi Januari sendiri, karena tidak ada baris sebelumnya; rata-ratanya 9.200.000, bukan hasil pembagian tiga.
- Februari — frame berisi dua baris (Januari dan Februari), sehingga rata-ratanya 9.500.000.
- Maret ke atas — frame selalu penuh tiga baris; mulai sini angka rata-rata benar-benar “tiga bulan”.
PostgreSQL tidak mengganti baris yang kurang dengan NULL atau menggeser jendela; fungsi agregat hanya menghitung dari baris yang tersedia di dalam frame. Konsekuensinya, rata-rata bergerak di awal rangkaian lebih sensitif terhadap lonjakan karena elemennya lebih sedikit.
Membandingkan Ukuran Jendela #
Ukuran frame adalah knob yang harus disetel sesuai kebutuhan: jendela pendek mengikuti data dengan cepat tetapi kurang meredam lonjakan; jendela panjang menghaluskan lebih agresif tetapi terlambat mendeteksi perubahan. Query berikut menempatkan tiga ukuran — 2, 3, dan 4 bulan — bersebelahan agar perbandingannya langsung terlihat:
-- perbandingan ukuran jendela: rata-rata bergerak 2, 3, dan 4 bulan
SELECT bulan,
pendapatan,
round(avg(pendapatan) OVER (ORDER BY bulan
ROWS BETWEEN 1 PRECEDING AND CURRENT ROW), 2) AS rata2_bulan,
round(avg(pendapatan) OVER (ORDER BY bulan
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW), 2) AS rata3_bulan,
round(avg(pendapatan) OVER (ORDER BY bulan
ROWS BETWEEN 3 PRECEDING AND CURRENT ROW), 2) AS rata4_bulan
FROM penjualan_bulanan
WHERE produk = 'Kopi Gayo'
ORDER BY bulan;Output yang Dihasilkan:
bulan | pendapatan | rata2_bulan | rata3_bulan | rata4_bulan
------------+-------------+-------------+-------------+-------------
2026-01-01 | 9200000.00 | 9200000.00 | 9200000.00 | 9200000.00
2026-02-01 | 9800000.00 | 9500000.00 | 9500000.00 | 9500000.00
2026-03-01 | 8900000.00 | 9350000.00 | 9300000.00 | 9300000.00
2026-04-01 | 10200000.00 | 9550000.00 | 9633333.33 | 9525000.00
2026-05-01 | 10800000.00 | 10500000.00 | 9966666.67 | 9925000.00
2026-06-01 | 11500000.00 | 11150000.00 | 10833333.33 | 10350000.00
2026-07-01 | 11100000.00 | 11300000.00 | 11133333.33 | 10900000.00
2026-08-01 | 11900000.00 | 11500000.00 | 11500000.00 | 11325000.00
(8 rows)Bandingkan reaksi ketiganya terhadap penurunan Maret (8.900.000 setelah 9.800.000 di Februari). rata2_bulan langsung jatuh ke 9.350.000 — paling responsif. rata3_bulan turun hanya ke 9.300.000 dan rata4_bulan ke 9.300.000 juga, karena keduanya masih ditopang bulan-bulan sebelumnya. Di ujung lain, saat pendapatan naik stabil mulai Mei, rata4_bulan tertinggal di 9.925.000 sementara rata2_bulan sudah melompat ke 10.500.000. Perhatikan juga baris-baris awal tiap kolom: rata2_bulan hanya punya satu baris tepi, rata3_bulan dua, dan rata4_bulan tiga — makin lebar jendelanya, makin banyak baris yang rata-ratanya dihitung dari elemen tidak penuh.
Rata-rata Bergerak per Produk #
Rangkaian tiap produk harus dihaluskan secara terpisah — mencampur Kopi Gayo dan Gula Pasir dalam satu frame menghasilkan angka tanpa makna. Kombinasi PARTITION BY produk dengan frame ROWS menangani hal itu sekaligus:
-- rata-rata bergerak 3 bulan per produk: PARTITION BY + frame ROWS
SELECT produk,
bulan,
pendapatan,
round(avg(pendapatan) OVER (PARTITION BY produk ORDER BY bulan
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW), 2) AS rata3_bulan
FROM penjualan_bulanan
ORDER BY produk, bulan;Output yang Dihasilkan:
produk | bulan | pendapatan | rata3_bulan
------------+------------+-------------+-------------
Gula Pasir | 2026-01-01 | 3200000.00 | 3200000.00
Gula Pasir | 2026-02-01 | 3600000.00 | 3400000.00
Gula Pasir | 2026-03-01 | 3100000.00 | 3300000.00
Gula Pasir | 2026-04-01 | 3800000.00 | 3500000.00
Gula Pasir | 2026-05-01 | 3500000.00 | 3466666.67
Gula Pasir | 2026-06-01 | 3900000.00 | 3733333.33
Gula Pasir | 2026-07-01 | 3700000.00 | 3700000.00
Gula Pasir | 2026-08-01 | 4000000.00 | 3866666.67
Kopi Gayo | 2026-01-01 | 9200000.00 | 9200000.00
Kopi Gayo | 2026-02-01 | 9800000.00 | 9500000.00
Kopi Gayo | 2026-03-01 | 8900000.00 | 9300000.00
Kopi Gayo | 2026-04-01 | 10200000.00 | 9633333.33
Kopi Gayo | 2026-05-01 | 10800000.00 | 9966666.67
Kopi Gayo | 2026-06-01 | 11500000.00 | 10833333.33
Kopi Gayo | 2026-07-01 | 11100000.00 | 11133333.33
Kopi Gayo | 2026-08-01 | 11900000.00 | 11500000.00
Teh Melati | 2026-01-01 | 4100000.00 | 4100000.00
Teh Melati | 2026-02-01 | 4300000.00 | 4200000.00
Teh Melati | 2026-03-01 | 4600000.00 | 4333333.33
Teh Melati | 2026-04-01 | 4400000.00 | 4433333.33
Teh Melati | 2026-05-01 | 4800000.00 | 4600000.00
Teh Melati | 2026-06-01 | 5100000.00 | 4766666.67
Teh Melati | 2026-07-01 | 5000000.00 | 4966666.67
Teh Melati | 2026-08-01 | 5300000.00 | 5133333.33
(24 rows)PARTITION BY produk membelah tabel menjadi tiga rangkaian terpisah, dan frame ROWS BETWEEN 2 PRECEDING AND CURRENT ROW berlaku ulang di setiap rangkaian: baris Januari tiap produk selalu menjadi titik awal frame-nya sendiri. Lihat blok Gula Pasir — rata-rata bergeraknya naik perlahan dari 3.200.000 ke 3.866.666,67 — lalu bandingkan dengan blok Kopi Gayo yang tumbuh dari 9.200.000 ke 11.500.000. Kedua tren berdiri berdampingan tanpa saling mencampur, dan tiap blok mengulang pola baris tepi yang sama: dua baris pertama rata-ratanya dihitung dari elemen yang lebih sedikit.
Agregat Lain pada Frame yang Sama #
Frame bergerak bukan milik avg saja — semua agregat biasa bisa memakainya. Query berikut menghitung sum, min, dan max atas jendela tiga bulan yang sama pada rangkaian Kopi Gayo:
-- agregat lain pada frame bergerak yang sama: sum, min, max 3 bulan
SELECT bulan,
pendapatan,
sum(pendapatan) OVER (ORDER BY bulan
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS total3_bulan,
min(pendapatan) OVER (ORDER BY bulan
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS terendah3_bulan,
max(pendapatan) OVER (ORDER BY bulan
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS tertinggi3_bulan
FROM penjualan_bulanan
WHERE produk = 'Kopi Gayo'
ORDER BY bulan;Output yang Dihasilkan:
bulan | pendapatan | total3_bulan | terendah3_bulan | tertinggi3_bulan
------------+-------------+--------------+-----------------+------------------
2026-01-01 | 9200000.00 | 9200000.00 | 9200000.00 | 9200000.00
2026-02-01 | 9800000.00 | 19000000.00 | 9200000.00 | 9800000.00
2026-03-01 | 8900000.00 | 27900000.00 | 8900000.00 | 9800000.00
2026-04-01 | 10200000.00 | 28900000.00 | 8900000.00 | 10200000.00
2026-05-01 | 10800000.00 | 29900000.00 | 8900000.00 | 10800000.00
2026-06-01 | 11500000.00 | 32500000.00 | 10200000.00 | 11500000.00
2026-07-01 | 11100000.00 | 33400000.00 | 10800000.00 | 11500000.00
2026-08-01 | 11900000.00 | 34500000.00 | 11100000.00 | 11900000.00
(8 rows)Tiap kolom menjawab pertanyaan analisis yang berbeda atas jendela tiga bulan yang identik: total3_bulan adalah pendapatan bergulir tiga bulan — angka yang biasa dibandingkan dengan target kuartalan berjalan; terendah3_bulan menunjukkan bulan terburuk dalam jendela itu (berguna untuk uji tekanan arus kas); max menunjukkan bulan terbaiknya. Perhatikan bagaimana nilai min bertahan di 8.900.000 dari Maret sampai Mei — Maret tetap berada di dalam frame hingga akhirnya tergeser keluar di Juni, saat min meloncat ke 10.200.000.
Kita kini memakai ROWS BETWEEN ... PRECEDING ... AND ... berulang kali dan baris tepinya sudah akrab. Namun beberapa pertanyaan masih menggantung: apa bedanya ROWS dengan RANGE yang selama ini diam-diam menjadi mode default frame? Apa arti UNBOUNDED FOLLOWING, dan mengapa frame bertemu ORDER BY di beberapa situasi tapi ditolak di situasi lain? Chapter berikutnya, Window Frame
, membedah spesifikasi frame secara menyeluruh — dua mode tersebut, sintaks lengkapnya, dan perilakunya saat nilai urut kembar.
Referensi #
- Window Function Expressions
— sintaks frame clause
ROWS/RANGE BETWEEN ...secara lengkap. - Window Functions — daftar fungsi jendela beserta contoh frame.
- Tutorial: Window Functions
— pengantar
OVERdan frame di tutorial resmi.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
buat-tabel.sql
Unduh berkas-- tabel latihan: pendapatan bulanan per produk Toko Maju Jaya, Januari-Agustus 2026
CREATE TABLE penjualan_bulanan (
id integer PRIMARY KEY,
produk text NOT NULL,
bulan date NOT NULL,
pendapatan numeric(12,2) NOT NULL
);
INSERT INTO penjualan_bulanan (id, produk, bulan, pendapatan) VALUES
( 1, 'Gula Pasir', DATE '2026-01-01', 3200000.00),
( 2, 'Kopi Gayo', DATE '2026-01-01', 9200000.00),
( 3, 'Teh Melati', DATE '2026-01-01', 4100000.00),
( 4, 'Gula Pasir', DATE '2026-02-01', 3600000.00),
( 5, 'Kopi Gayo', DATE '2026-02-01', 9800000.00),
( 6, 'Teh Melati', DATE '2026-02-01', 4300000.00),
( 7, 'Gula Pasir', DATE '2026-03-01', 3100000.00),
( 8, 'Kopi Gayo', DATE '2026-03-01', 8900000.00),
( 9, 'Teh Melati', DATE '2026-03-01', 4600000.00),
(10, 'Gula Pasir', DATE '2026-04-01', 3800000.00),
(11, 'Kopi Gayo', DATE '2026-04-01', 10200000.00),
(12, 'Teh Melati', DATE '2026-04-01', 4400000.00),
(13, 'Gula Pasir', DATE '2026-05-01', 3500000.00),
(14, 'Kopi Gayo', DATE '2026-05-01', 10800000.00),
(15, 'Teh Melati', DATE '2026-05-01', 4800000.00),
(16, 'Gula Pasir', DATE '2026-06-01', 3900000.00),
(17, 'Kopi Gayo', DATE '2026-06-01', 11500000.00),
(18, 'Teh Melati', DATE '2026-06-01', 5100000.00),
(19, 'Gula Pasir', DATE '2026-07-01', 3700000.00),
(20, 'Kopi Gayo', DATE '2026-07-01', 11100000.00),
(21, 'Teh Melati', DATE '2026-07-01', 5000000.00),
(22, 'Gula Pasir', DATE '2026-08-01', 4000000.00),
(23, 'Kopi Gayo', DATE '2026-08-01', 11900000.00),
(24, 'Teh Melati', DATE '2026-08-01', 5300000.00);
SELECT id, produk, bulan, pendapatan
FROM penjualan_bulanan
ORDER BY bulan, produk;
moving-agregat-lain.sql
Unduh berkas-- agregat lain pada frame bergerak yang sama: sum, min, max 3 bulan
SELECT bulan,
pendapatan,
sum(pendapatan) OVER (ORDER BY bulan
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS total3_bulan,
min(pendapatan) OVER (ORDER BY bulan
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS terendah3_bulan,
max(pendapatan) OVER (ORDER BY bulan
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS tertinggi3_bulan
FROM penjualan_bulanan
WHERE produk = 'Kopi Gayo'
ORDER BY bulan;
moving-average-dasar.sql
Unduh berkas-- rata-rata bergerak 3 bulan pada rangkaian pendapatan Kopi Gayo
SELECT bulan,
pendapatan,
round(avg(pendapatan) OVER (ORDER BY bulan
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW), 2) AS rata3_bulan
FROM penjualan_bulanan
WHERE produk = 'Kopi Gayo'
ORDER BY bulan;
moving-average-partisi.sql
Unduh berkas-- rata-rata bergerak 3 bulan per produk: PARTITION BY + frame ROWS
SELECT produk,
bulan,
pendapatan,
round(avg(pendapatan) OVER (PARTITION BY produk ORDER BY bulan
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW), 2) AS rata3_bulan
FROM penjualan_bulanan
ORDER BY produk, bulan;
perbandingan-jendela.sql
Unduh berkas-- perbandingan ukuran jendela: rata-rata bergerak 2, 3, dan 4 bulan
SELECT bulan,
pendapatan,
round(avg(pendapatan) OVER (ORDER BY bulan
ROWS BETWEEN 1 PRECEDING AND CURRENT ROW), 2) AS rata2_bulan,
round(avg(pendapatan) OVER (ORDER BY bulan
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW), 2) AS rata3_bulan,
round(avg(pendapatan) OVER (ORDER BY bulan
ROWS BETWEEN 3 PRECEDING AND CURRENT ROW), 2) AS rata4_bulan
FROM penjualan_bulanan
WHERE produk = 'Kopi Gayo'
ORDER BY bulan;