Laporan keuangan jarang berhenti pada angka satu periode. Manajer Toko Maju Jaya ingin melihat pendapatan setiap bulan dan berapa jumlahnya sejak awal tahun pada saat yang sama: kolom pendapatan Januari berdiri di samping kolom akumulasi Januari–bulan berjalan. Dengan GROUP BY, angka kumulatif seperti itu harus dihitung dengan subquery atau dijumlahkan ulang di aplikasi; dengan window function, cukup satu agregat sum yang diberi klausa OVER berisi ORDER BY. Chapter ini membahas pola running total — akumulasi berjalan yang bertambah setiap baris — mulai dari satu rangkaian data, varian per partisi, hingga kombinasi dengan grand total dan penerapannya untuk memantau pencapaian target semester.
Tabel Latihan #
Kita lanjut memakai domain penjualan bulanan Toko Maju Jaya: tabel penjualan_bulanan berisi pendapatan tiga produk sepanjang semester I 2026. Kolom bulan menyimpan tanggal pertama tiap bulan, dan satu bulan bisa memuat lebih dari satu baris karena setiap produk punya barisnya sendiri:
-- tabel latihan: pendapatan bulanan per produk Toko Maju Jaya, semester I 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);
SELECT id, produk, bulan, pendapatan
FROM penjualan_bulanan
ORDER BY bulan, produk;Output yang Dihasilkan:
CREATE TABLE
INSERT 0 18
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
(18 rows)Running Total pada Satu Rangkaian #
Bentuk paling sederhana dari running total muncul pada satu rangkaian data yang sudah terurut waktu. Query berikut menyaring pendapatan Kopi Gayo saja, lalu menghitung akumulasi berjalan dengan sum(pendapatan) OVER (ORDER BY bulan) — plus count(*) dengan jendela yang sama sebagai penghitung urutan bulan:
-- running total pada satu rangkaian: pendapatan Kopi Gayo Januari-Juni
SELECT bulan,
pendapatan,
sum(pendapatan) OVER (ORDER BY bulan) AS total_berjalan,
count(*) OVER (ORDER BY bulan) AS bulan_ke
FROM penjualan_bulanan
WHERE produk = 'Kopi Gayo'
ORDER BY bulan;Output yang Dihasilkan:
bulan | pendapatan | total_berjalan | bulan_ke
------------+-------------+----------------+----------
2026-01-01 | 9200000.00 | 9200000.00 | 1
2026-02-01 | 9800000.00 | 19000000.00 | 2
2026-03-01 | 8900000.00 | 27900000.00 | 3
2026-04-01 | 10200000.00 | 38100000.00 | 4
2026-05-01 | 10800000.00 | 48900000.00 | 5
2026-06-01 | 11500000.00 | 60400000.00 | 6
(6 rows)Mekanismenya bekerja seperti ini: klausa OVER (ORDER BY bulan) memerintahkan PostgreSQL mengurutkan baris-baris jendela berdasarkan bulan, lalu untuk setiap baris menghitung sum atas baris-baris tersebut mulai dari paling awal sampai baris yang sedang diproses. Baris Februari menjumlahkan Januari dan Februari (19.000.000); baris Maret menjumlahkan Januari sampai Maret (27.900.000); dan seterusnya sampai baris Juni yang totalnya 60.400.000 rupiah. Perhatikan dua hal penting: kolom pendapatan asli tetap utuh di samping hasil akumulasinya, dan count(*) OVER (ORDER BY bulan) menghasilkan pola yang sama — 1, 2, 3, … — sehingga kolom itu berfungsi sebagai nomor urut bulan sekaligus bukti bahwa count juga mengikuti aturan akumulasi berjalan.
Mari kita bedah query tersebut baris per baris:
- Baris 3 (
sum(pendapatan) OVER (ORDER BY bulan)) — agregatsumdipakai sebagai window function;ORDER BY bulandi dalamOVERmembuat frame penjumlahan tumbuh mengikuti urutan waktu, menghasilkan akumulasi berjalan. - Baris 4 (
count(*) OVER (ORDER BY bulan)) — pola yang sama dengan agregat berbeda: penghitung baris yang ikut berjalan dari 1 sampai 6. - Baris 6 (
WHERE produk = 'Kopi Gayo') — penyaringan dilakukan sebelum window function bekerja, sehingga jendela hanya berisi enam baris Kopi Gayo. - Baris 7 (
ORDER BY bulan) —ORDER BYquery luar mengatur urutan tampilan hasil; urutan ini terpisah dari urutan di dalamOVER, meskipun keduanya kebetulan sama.
Running Total per Produk dengan PARTITION BY #
Laporan nyata jarang berisi satu produk saja. Tanpa PARTITION BY, sum(...) OVER (ORDER BY bulan) pada tabel penuh akan mencampur pendapatan semua produk dalam satu akumulasi. Menambahkan PARTITION BY produk memisahkan rangkaian tiap produk: akumulasi dimulai dari nol setiap kali window function pindah ke partisi produk berikutnya:
-- running total per produk: PARTITION BY mereset akumulasi di tiap partisi
SELECT produk,
bulan,
pendapatan,
sum(pendapatan) OVER (PARTITION BY produk ORDER BY bulan) AS total_berjalan,
count(*) OVER (PARTITION BY produk ORDER BY bulan) AS bulan_ke
FROM penjualan_bulanan
ORDER BY produk, bulan;Output yang Dihasilkan:
produk | bulan | pendapatan | total_berjalan | bulan_ke
------------+------------+-------------+----------------+----------
Gula Pasir | 2026-01-01 | 3200000.00 | 3200000.00 | 1
Gula Pasir | 2026-02-01 | 3600000.00 | 6800000.00 | 2
Gula Pasir | 2026-03-01 | 3100000.00 | 9900000.00 | 3
Gula Pasir | 2026-04-01 | 3800000.00 | 13700000.00 | 4
Gula Pasir | 2026-05-01 | 3500000.00 | 17200000.00 | 5
Gula Pasir | 2026-06-01 | 3900000.00 | 21100000.00 | 6
Kopi Gayo | 2026-01-01 | 9200000.00 | 9200000.00 | 1
Kopi Gayo | 2026-02-01 | 9800000.00 | 19000000.00 | 2
Kopi Gayo | 2026-03-01 | 8900000.00 | 27900000.00 | 3
Kopi Gayo | 2026-04-01 | 10200000.00 | 38100000.00 | 4
Kopi Gayo | 2026-05-01 | 10800000.00 | 48900000.00 | 5
Kopi Gayo | 2026-06-01 | 11500000.00 | 60400000.00 | 6
Teh Melati | 2026-01-01 | 4100000.00 | 4100000.00 | 1
Teh Melati | 2026-02-01 | 4300000.00 | 8400000.00 | 2
Teh Melati | 2026-03-01 | 4600000.00 | 13000000.00 | 3
Teh Melati | 2026-04-01 | 4400000.00 | 17400000.00 | 4
Teh Melati | 2026-05-01 | 4800000.00 | 22200000.00 | 5
Teh Melati | 2026-06-01 | 5100000.00 | 27300000.00 | 6
(18 rows)Baca hasilnya per blok produk. Untuk Gula Pasir, kolom total_berjalan naik dari 3.200.000, 6.800.000, sampai 21.100.000 rupiah di bulan Juni; begitu blok berganti ke Kopi Gayo, akumulasi reset dan mulai lagi dari 9.200.000. Kolom bulan_ke juga kembali ke 1 di setiap awal partisi — angka 1 sampai 6 berulang tiga kali. Inilah kombinasi yang paling sering dipakai untuk running total: PARTITION BY menentukan rangkaian mana yang diakumulasi, ORDER BY menentukan arah waktu akumulasinya.
Running Total dan Grand Total dalam Satu Query #
Pertanyaan berikutnya yang biasanya menyertai running total: berapa persen kontribusi tiap bulan terhadap keseluruhan semester? Jawabannya membutuhkan dua angka sekaligus — total berjalan dan grand total. Kedua-duanya bisa dihitung dalam satu query dengan dua spesifikasi jendela yang berbeda: OVER (ORDER BY bulan) untuk akumulasi, dan OVER () untuk total seluruh partisi:
-- total bulanan, running total, dan grand total dalam satu query
WITH bulanan AS (
SELECT bulan,
sum(pendapatan) AS total_bulan
FROM penjualan_bulanan
GROUP BY bulan
)
SELECT bulan,
total_bulan,
sum(total_bulan) OVER (ORDER BY bulan) AS total_berjalan,
sum(total_bulan) OVER () AS total_keseluruhan,
round(total_bulan / sum(total_bulan) OVER () * 100, 1) AS persen_dari_total
FROM bulanan
ORDER BY bulan;Output yang Dihasilkan:
bulan | total_bulan | total_berjalan | total_keseluruhan | persen_dari_total
------------+-------------+----------------+-------------------+-------------------
2026-01-01 | 16500000.00 | 16500000.00 | 108800000.00 | 15.2
2026-02-01 | 17700000.00 | 34200000.00 | 108800000.00 | 16.3
2026-03-01 | 16600000.00 | 50800000.00 | 108800000.00 | 15.3
2026-04-01 | 18400000.00 | 69200000.00 | 108800000.00 | 16.9
2026-05-01 | 19100000.00 | 88300000.00 | 108800000.00 | 17.6
2026-06-01 | 20500000.00 | 108800000.00 | 108800000.00 | 18.8
(6 rows)Tiga teknik bekerja bersama di query ini:
- CTE
bulanan— data mentah berisi tiga baris per bulan, jadi total bulanan dihitung dulu denganGROUP BY bulanmenghasilkan enam baris ringkasan; window function kemudian bekerja di atas ringkasan itu. sum(total_bulan) OVER (ORDER BY bulan)— running total di tingkat bulanan: 16.500.000, 34.200.000, …, 108.800.000.sum(total_bulan) OVER ()— jendela tanpaORDER BYdan tanpaPARTITION BYmencakup seluruh baris hasil, sehingga setiap baris menerima grand total 108.800.000. Grand total lalu dipakai sebagai penyebutpersen_dari_total— kolom yang menunjukkan kontribusi tiap bulan, dari 15,2% di Januari hingga 18,8% di Juni.
Perhatikan pola pentingnya: satu kolom grand total yang sama bisa dipakai berulang kali dalam daftar SELECT tanpa menghitung ulang subquery. OVER () adalah cara paling ringkas menempelkan angka keseluruhan pada setiap baris.
Use Case: Progres Terhadap Target Semester #
Running total menjadi sangat berguna begitu dibandingkan dengan angka patokan. Misalkan manajemen menetapkan target pendapatan semester I sebesar Rp105.000.000. Dengan membagi total berjalan terhadap target tersebut, setiap baris bulanan langsung menunjukkan persentase pencapaian dan statusnya:
-- use case: progres pencapaian target pendapatan semester (Rp105.000.000)
WITH bulanan AS (
SELECT bulan,
sum(pendapatan) AS total_bulan
FROM penjualan_bulanan
GROUP BY bulan
)
SELECT bulan,
total_bulan,
sum(total_bulan) OVER (ORDER BY bulan) AS total_berjalan,
round(sum(total_bulan) OVER (ORDER BY bulan) / 105000000 * 100, 1) AS progres_target_pct,
CASE WHEN sum(total_bulan) OVER (ORDER BY bulan) >= 105000000
THEN 'target tercapai'
ELSE 'belum tercapai'
END AS status_target
FROM bulanan
ORDER BY bulan;Output yang Dihasilkan:
bulan | total_bulan | total_berjalan | progres_target_pct | status_target
------------+-------------+----------------+--------------------+-----------------
2026-01-01 | 16500000.00 | 16500000.00 | 15.7 | belum tercapai
2026-02-01 | 17700000.00 | 34200000.00 | 32.6 | belum tercapai
2026-03-01 | 16600000.00 | 50800000.00 | 48.4 | belum tercapai
2026-04-01 | 18400000.00 | 69200000.00 | 65.9 | belum tercapai
2026-05-01 | 19100000.00 | 88300000.00 | 84.1 | belum tercapai
2026-06-01 | 20500000.00 | 108800000.00 | 103.6 | target tercapai
(6 rows)Hasilnya membentuk kurva progres yang naik: 15,7% pada Januari, 48,4% pada Maret, 84,1% pada Mei — sampai Juni menutup semester di angka 103,6% dengan status target tercapai. Pola yang sama berlaku untuk kasus lain yang bentuknya serupa: saldo berjalan pada mutasi rekening dihitung dengan menjumlahkan setoran dan penarikan berurutan tanggal, dan progres produksi dihitung dengan mengakumulasi unit yang selesai. Semuanya satu resep: agregat berurutan sum(...) OVER (ORDER BY kolom_waktu) dibandingkan dengan patokan di sisi kanannya.
Sebagai rangkuman, varian running total yang sudah kita bahas:
| Kebutuhan | Pola jendela |
|---|---|
| Akumulasi satu rangkaian | sum(x) OVER (ORDER BY waktu) |
| Akumulasi per kelompok, reset tiap kelompok | sum(x) OVER (PARTITION BY kelompok ORDER BY waktu) |
| Grand total di setiap baris | sum(x) OVER () |
| Persentase progres | sum(x) OVER (ORDER BY waktu) / patokan * 100 |
Running total menghitung akumulasi penuh sejak awal rangkaian. Banyak analisis data penjualan justru butuh versi yang lebih halus: rata-rata atas jendela waktu pendek yang ikut bergeser — misalnya rata-rata tiga bulan terakhir — untuk membaca tren tanpa terpengaruh lonjakan satu bulan. Chapter berikutnya, Moving Average
, menghadirkan alat itu lewat klausa ROWS BETWEEN n PRECEDING AND CURRENT ROW.
Referensi #
- Window Function Expressions
— sintaks
OVERdan perilaku agregat sebagai window function. - Window Functions — daftar fungsi jendela dan contoh pemakaiannya.
- Tutorial: Window Functions
— pengantar running total dan
rank()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, semester I 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);
SELECT id, produk, bulan, pendapatan
FROM penjualan_bulanan
ORDER BY bulan, produk;
grand-total.sql
Unduh berkas-- total bulanan, running total, dan grand total dalam satu query
WITH bulanan AS (
SELECT bulan,
sum(pendapatan) AS total_bulan
FROM penjualan_bulanan
GROUP BY bulan
)
SELECT bulan,
total_bulan,
sum(total_bulan) OVER (ORDER BY bulan) AS total_berjalan,
sum(total_bulan) OVER () AS total_keseluruhan,
round(total_bulan / sum(total_bulan) OVER () * 100, 1) AS persen_dari_total
FROM bulanan
ORDER BY bulan;
running-total-dasar.sql
Unduh berkas-- running total pada satu rangkaian: pendapatan Kopi Gayo Januari-Juni
SELECT bulan,
pendapatan,
sum(pendapatan) OVER (ORDER BY bulan) AS total_berjalan,
count(*) OVER (ORDER BY bulan) AS bulan_ke
FROM penjualan_bulanan
WHERE produk = 'Kopi Gayo'
ORDER BY bulan;
running-total-partisi.sql
Unduh berkas-- running total per produk: PARTITION BY mereset akumulasi di tiap partisi
SELECT produk,
bulan,
pendapatan,
sum(pendapatan) OVER (PARTITION BY produk ORDER BY bulan) AS total_berjalan,
count(*) OVER (PARTITION BY produk ORDER BY bulan) AS bulan_ke
FROM penjualan_bulanan
ORDER BY produk, bulan;
use-case-target.sql
Unduh berkas-- use case: progres pencapaian target pendapatan semester (Rp105.000.000)
WITH bulanan AS (
SELECT bulan,
sum(pendapatan) AS total_bulan
FROM penjualan_bulanan
GROUP BY bulan
)
SELECT bulan,
total_bulan,
sum(total_bulan) OVER (ORDER BY bulan) AS total_berjalan,
round(sum(total_bulan) OVER (ORDER BY bulan) / 105000000 * 100, 1) AS progres_target_pct,
CASE WHEN sum(total_bulan) OVER (ORDER BY bulan) >= 105000000
THEN 'target tercapai'
ELSE 'belum tercapai'
END AS status_target
FROM bulanan
ORDER BY bulan;