Struk kasir, buku tabungan, dan dashboard omzet berbagi satu kebutuhan: kolom yang terus bertambah seiring baris berjalan. Setelah transaksi kelima, kasir ingin melihat jumlah uang yang sudah masuk sampai titik itu, bukan total akhir hari. Query agregat biasa hanya bisa memberi satu angka total untuk seluruh tabel, sementara LAG dari chapter sebelumnya hanya melihat satu tetangga. Fungsi SUM() OVER (ORDER BY kolom) menjembatani keduanya: agregasi dihitung kumulatif dari awal jendela hingga baris saat ini, sehingga setiap baris membawa total berjalan miliknya. Pada chapter ini kita bedah perilakunya, melihat apa yang berubah saat ORDER BY di dalam jendela dihapus, mereset akumulasi per kota, menulis bentuk eksplisitnya dengan ROWS UNBOUNDED PRECEDING, lalu menerapkannya pada kasus klasik saldo rekening tabungan.
Menyiapkan Tabel Latihan #
Kita pakai database latihan belajar_window dengan tabel penjualan sembilan baris — tiga transaksi per kota pada tanggal yang semuanya berbeda. Nilai jumlah sengaja kecil supaya akumulasi mudah dihitung manual:
DROP DATABASE IF EXISTS belajar_window;
CREATE DATABASE belajar_window;
USE belajar_window;Output yang Dihasilkan:
Query OK, 0 rows affected, 1 warning (0.00 sec)
Query OK, 1 row affected (0.00 sec)
Database changedCREATE TABLE penjualan (
id INT AUTO_INCREMENT PRIMARY KEY,
tanggal DATE NOT NULL,
kota VARCHAR(30) NOT NULL,
produk VARCHAR(50) NOT NULL,
jumlah INT NOT NULL,
harga DECIMAL(10,2) NOT NULL
);
INSERT INTO penjualan (tanggal, kota, produk, jumlah, harga) VALUES
('2026-04-01', 'Jakarta', 'Kopi Gayo', 5, 55000.00),
('2026-04-02', 'Bandung', 'Teh Melati', 3, 25000.00),
('2026-04-03', 'Surabaya', 'Kopi Toraja', 7, 62000.00),
('2026-04-04', 'Jakarta', 'Kopi Gayo', 8, 55000.00),
('2026-04-05', 'Bandung', 'Teh Melati', 6, 25000.00),
('2026-04-06', 'Surabaya', 'Kopi Toraja', 4, 62000.00),
('2026-04-07', 'Jakarta', 'Kopi Gayo', 12, 55000.00),
('2026-04-08', 'Bandung', 'Teh Melati', 9, 25000.00),
('2026-04-09', 'Surabaya', 'Kopi Toraja', 10, 62000.00);Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)
Query OK, 9 rows affected (0.00 sec)
Records: 9 Duplicates: 0 Warnings: 0Tanggal berjalan maju satu hari per baris tanpa satu pun yang kembar. Kondisi ini membuat akumulasi tampil mulus baris demi baris; nanti di chapter Window Frame kita akan sengaja memakai tanggal kembar untuk membongkar satu kejutan tersembunyi dari bentuk default query ini.
Akumulasi Berjalan dengan ORDER BY #
Satu kolom tambahan mengubah laporan flat menjadi laporan berjalan:
SELECT tanggal, kota, jumlah,
SUM(jumlah) OVER (ORDER BY tanggal) AS total_berjalan
FROM penjualan
ORDER BY tanggal;Output yang Dihasilkan:
+------------+----------+--------+----------------+
| tanggal | kota | jumlah | total_berjalan |
+------------+----------+--------+----------------+
| 2026-04-01 | Jakarta | 5 | 5 |
| 2026-04-02 | Bandung | 3 | 8 |
| 2026-04-03 | Surabaya | 7 | 15 |
| 2026-04-04 | Jakarta | 8 | 23 |
| 2026-04-05 | Bandung | 6 | 29 |
| 2026-04-06 | Surabaya | 4 | 33 |
| 2026-04-07 | Jakarta | 12 | 45 |
| 2026-04-08 | Bandung | 9 | 54 |
| 2026-04-09 | Surabaya | 10 | 64 |
+------------+----------+--------+----------------+
9 rows in set (0.00 sec)Kolom total_berjalan tumbuh mengikuti baris: 5, 8, 15, 23, 29, 33, 45, 54, 64, dan baris terakhir memang menghasilkan total seluruh sembilan transaksi. Mari kita bedah cara kerjanya baris demi baris: ORDER BY tanggal di dalam OVER menetapkan urutan jendela menurut tanggal, lalu untuk setiap baris, SUM(jumlah) menjumlahkan baris-baris dari awal jendela sampai posisi baris saat ini — baris 2026-04-03 menjumlahkan 5 + 3 + 7 menjadi 15, baris 2026-04-04 menjumlahkan 5 + 3 + 7 + 8 menjadi 23, dan seterusnya. Kumpulan baris yang dijumlahkan itulah yang disebut frame jendela — cakupan baris yang terlihat oleh fungsi pada satu posisi. Tanpa kita tulis apa-apa, ORDER BY di dalam jendela membuat frame tumbuh dari awal hingga baris sekarang; inilah mesin di balik semua akumulasi berjalan.
Tanpa ORDER BY: Total Jendela di Setiap Baris #
Apa yang terjadi bila ORDER BY di dalam jendela dihapus? Intuisi mungkin menduga hasil sama-sama kumulatif, hanya urutannya bebas. Realitanya berbeda total:
SELECT tanggal, kota, jumlah,
SUM(jumlah) OVER () AS total_semua
FROM penjualan
ORDER BY tanggal;Output yang Dihasilkan:
+------------+----------+--------+-------------+
| tanggal | kota | jumlah | total_semua |
+------------+----------+--------+-------------+
| 2026-04-01 | Jakarta | 5 | 64 |
| 2026-04-02 | Bandung | 3 | 64 |
| 2026-04-03 | Surabaya | 7 | 64 |
| 2026-04-04 | Jakarta | 8 | 64 |
| 2026-04-05 | Bandung | 6 | 64 |
| 2026-04-06 | Surabaya | 4 | 64 |
| 2026-04-07 | Jakarta | 12 | 64 |
| 2026-04-08 | Bandung | 9 | 64 |
| 2026-04-09 | Surabaya | 10 | 64 |
+------------+----------+--------+-------------+
9 rows in set (0.00 sec)Setiap baris kini menampilkan angka yang sama: 64, total seluruh jendela. Tanpa ORDER BY, frame tidak lagi “tumbuh” mengikuti posisi — frame menjadi seluruh jendela untuk semua baris, sehingga SUM melihat semuanya sejak baris pertama. Bedah perbedaannya dalam dua kalimat: dengan ORDER BY, fungsi menjumlahkan awal sampai sekarang (akumulasi berjalan); tanpa ORDER BY, fungsi menjumlahkan semuanya (total penuh). Jadi SUM(jumlah) OVER () identik maknanya dengan SELECT SUM(jumlah) agregat biasa — hanya saja nilainya ditempel di setiap baris alih-alih meruntuhkan tabel menjadi satu baris.
Partisi per Kota: Akumulasi yang Reset #
Seperti fungsi window lainnya, akumulasi berjalan mengikuti partisi. PARTITION BY kota membuat tiap kota memiliki lintasan akumulasinya sendiri:
SELECT kota, tanggal, jumlah,
SUM(jumlah) OVER (PARTITION BY kota ORDER BY tanggal) AS total_kota
FROM penjualan
ORDER BY kota, tanggal;Output yang Dihasilkan:
+----------+------------+--------+------------+
| kota | tanggal | jumlah | total_kota |
+----------+------------+--------+------------+
| Bandung | 2026-04-02 | 3 | 3 |
| Bandung | 2026-04-05 | 6 | 9 |
| Bandung | 2026-04-08 | 9 | 18 |
| Jakarta | 2026-04-01 | 5 | 5 |
| Jakarta | 2026-04-04 | 8 | 13 |
| Jakarta | 2026-04-07 | 12 | 25 |
| Surabaya | 2026-04-03 | 7 | 7 |
| Surabaya | 2026-04-06 | 4 | 11 |
| Surabaya | 2026-04-09 | 10 | 21 |
+----------+------------+--------+------------+
9 rows in set (0.00 sec)Tiga lintasan berjalan paralel: Jakarta 5 → 13 → 25, Bandung 3 → 9 → 18, dan Surabaya 7 → 11 → 21. Akumulasi setiap kota dimulai dari nol kembali di baris pertama kotanya — penjualan kota lain tidak pernah ikut dijumlahkan, padahal ketiganya hidup dalam satu query yang sama. Perhatikan juga bahwa jumlah dari ketiga nilai terakhir lintasan (25 + 18 + 21) tepat 64, konsisten dengan total global chapter sebelumnya — partisi membagi baris, bukan menggandakan angka. Pola gabungan PARTITION BY kota ORDER BY tanggal inilah yang dipakai untuk laporan omzet kumulatif per cabang, per produk, atau per region dalam satu tampilan.
Bentuk Eksplisit ROWS UNBOUNDED PRECEDING #
Frame yang bekerja diam-diam di belakang ORDER BY bisa ditulis secara terang-terangan:
SELECT tanggal, kota, jumlah,
SUM(jumlah) OVER (ORDER BY tanggal ROWS UNBOUNDED PRECEDING) AS total_berjalan
FROM penjualan
ORDER BY tanggal;Output yang Dihasilkan:
+------------+----------+--------+----------------+
| tanggal | kota | jumlah | total_berjalan |
+------------+----------+--------+----------------+
| 2026-04-01 | Jakarta | 5 | 5 |
| 2026-04-02 | Bandung | 3 | 8 |
| 2026-04-03 | Surabaya | 7 | 15 |
| 2026-04-04 | Jakarta | 8 | 23 |
| 2026-04-05 | Bandung | 6 | 29 |
| 2026-04-06 | Surabaya | 4 | 33 |
| 2026-04-07 | Jakarta | 12 | 45 |
| 2026-04-08 | Bandung | 9 | 54 |
| 2026-04-09 | Surabaya | 10 | 64 |
+------------+----------+--------+----------------+
9 rows in set (0.00 sec)Hasilnya identik dengan versi ringkas: 5, 8, 15, hingga 64. Frasa ROWS UNBOUNDED PRECEDING dibaca “dari baris tak terbatas di awal jendela, hingga baris sekarang” — bentuk lengkapnya adalah ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, dan bentuk pendeknya hanya memotong kata BETWEEN ... AND CURRENT ROW. Menuliskannya secara eksplisit membeli dua hal: kejelasan niat bagi pembaca query berikutnya, dan kendali penuh atas frame — misalnya mengganti UNBOUNDED menjadi 2 PRECEDING akan mengubah akumulasi menjadi jumlah tiga baris terakhir, cikal bakal rata-rata bergerak di chapter berikutnya.
Kasus Rekening Tabungan #
Kita tutup dengan kasus yang paling sering memakai akumulasi berjalan: saldo rekening. Tabel tabungan menyimpan mutasi harian — setoran positif, penarikan negatif:
CREATE TABLE tabungan (
id INT AUTO_INCREMENT PRIMARY KEY,
tanggal DATE NOT NULL,
keterangan VARCHAR(50) NOT NULL,
mutasi DECIMAL(12,2) NOT NULL
);
INSERT INTO tabungan (tanggal, keterangan, mutasi) VALUES
('2026-05-01', 'Setoran awal', 5000000.00),
('2026-05-03', 'Setoran gaji', 2500000.00),
('2026-05-06', 'Tarik tunai', -1000000.00),
('2026-05-11', 'Transfer masuk', 500000.00),
('2026-05-15', 'Belanja online', -1200000.00),
('2026-05-20', 'Setoran gaji', 2500000.00),
('2026-05-28', 'Tarik tunai', -300000.00);Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)
Query OK, 7 rows affected (0.00 sec)
Records: 7 Duplicates: 0 Warnings: 0Saldo setelah tiap mutasi adalah penjumlahan semua mutasi sebelumnya ditambah mutasi itu sendiri — persis definisi akumulasi berjalan:
SELECT tanggal, keterangan, mutasi,
SUM(mutasi) OVER (ORDER BY tanggal) AS saldo
FROM tabungan
ORDER BY tanggal;Output yang Dihasilkan:
+------------+----------------+-------------+------------+
| tanggal | keterangan | mutasi | saldo |
+------------+----------------+-------------+------------+
| 2026-05-01 | Setoran awal | 5000000.00 | 5000000.00 |
| 2026-05-03 | Setoran gaji | 2500000.00 | 7500000.00 |
| 2026-05-06 | Tarik tunai | -1000000.00 | 6500000.00 |
| 2026-05-11 | Transfer masuk | 500000.00 | 7000000.00 |
| 2026-05-15 | Belanja online | -1200000.00 | 5800000.00 |
| 2026-05-20 | Setoran gaji | 2500000.00 | 8300000.00 |
| 2026-05-28 | Tarik tunai | -300000.00 | 8000000.00 |
+------------+----------------+-------------+------------+
7 rows in set (0.00 sec)Saldo terbaca 5000000.00, 7500000.00, 6500000.00, 7000000.00, 5800000.00, 8300000.00, 8000000.00 — naik saat setoran masuk, turun saat penarikan keluar. Nilai terakhir kolom saldo adalah saldo akhir bulan, dan kita bisa membuktikannya dengan agregat biasa yang menjumlahkan seluruh mutasi:
SELECT SUM(mutasi) AS saldo_akhir
FROM tabungan;Output yang Dihasilkan:
+-------------+
| saldo_akhir |
+-------------+
| 8000000.00 |
+-------------+
1 row in set (0.00 sec)Kedua angka bertemu di 8000000.00: akumulasi berjalan pada baris terakhir selalu sama dengan total penuh, karena baris terakhir melihat seluruh jendela. Jumlah baris tidak berubah — tujuh mutasi menghasilkan tujuh baris laporan, sesuatu yang mustahil bagi GROUP BY manapun; setiap baris tetap membawa keterangan mutasinya sendiri sambil menampilkan saldo yang diperbarui.
Membersihkan Database Latihan #
Rangkaian latihan selesai dan database latihan kita jatuhkan kembali:
DROP DATABASE belajar_window;Output yang Dihasilkan:
Query OK, 2 rows affected (0.00 sec)Referensi #
- Window Function Concepts and Syntax — MySQL 8.4 Reference Manual
- Window Functions — Frame Specification — MySQL 8.4 Reference Manual
- Aggregate Function Descriptions — MySQL 8.4 Reference Manual
Akumulasi berjalan kini menjadi alat siap pakai, dari omzet kumulatif hingga saldo tabungan. Kolom tersebut menampung jumlah dari baris-baris terdahulu; pertanyaan analisis berikutnya meminta rata-rata dari beberapa baris terakhir untuk menghaluskan fluktuasi harian. Chapter berikutnya membangun rata-rata bergerak dengan AVG() dan frame ROWS BETWEEN 2 PRECEDING AND CURRENT ROW.