Sepanjang section ini kita menulis ROWS BETWEEN 2 PRECEDING AND CURRENT ROW dengan keyakinan penuh tanpa pernah membedah kata ROWS itu sendiri. Frame adalah sub-jendela: dari seluruh anggota jendela yang ditetapkan PARTITION BY, frame memilih bagian mana saja yang dilihat oleh fungsi pada posisi baris saat ini. MySQL menyediakan dua mode pemilihan: ROWS menghitung baris secara fisik, sedangkan RANGE menghitung berdasarkan nilai ORDER BY — dan keduanya hanya berbeda ketika nilai pengurutnya kembar. Pada chapter penutup ini kita buka semua kartu: membuktikan perbedaan ROWS vs RANGE dengan data yang memuat tanggal duplikat, memakai RANGE bersatuan nilai, memetakan keluarga bentuk frame, lalu menutup dengan frame default yang bekerja diam-diam serta status klausa EXCLUDE di MySQL 8.4.
Menyiapkan Tabel Latihan #
Kita pakai database latihan belajar_window dengan tabel penjualan delapan baris yang tanggalnya sengaja memuat kembaran: 2026-07-01, 2026-07-03, dan 2026-07-05 masing-masing dimiliki dua transaksi:
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-07-01', 'Jakarta', 'Kopi Gayo', 5, 55000.00),
('2026-07-01', 'Jakarta', 'Teh Melati', 7, 25000.00),
('2026-07-02', 'Bandung', 'Teh Melati', 3, 25000.00),
('2026-07-03', 'Jakarta', 'Kopi Gayo', 4, 55000.00),
('2026-07-03', 'Surabaya', 'Kopi Toraja', 6, 62000.00),
('2026-07-04', 'Bandung', 'Teh Melati', 8, 25000.00),
('2026-07-05', 'Jakarta', 'Kopi Gayo', 5, 55000.00),
('2026-07-05', 'Surabaya', 'Kopi Toraja', 9, 62000.00);Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)
Query OK, 8 rows affected (0.00 sec)
Records: 8 Duplicates: 0 Warnings: 0Nilai id berjalan searah tanggal sehingga urutan tampilan ORDER BY tanggal, id stabil dan deterministik. Baris-baris sekembar inilah bahan pembuktian utama kita — pada data dengan tanggal unik seperti chapter-chapter sebelumnya, ROWS dan RANGE tidak akan pernah ketahuan berbeda.
ROWS vs RANGE: Baris Fisik vs Nilai Logis #
Query berikut menjalankan dua akumulasi berjalan yang kode-kodenya nyaris identik — keduanya diurutkan menurut tanggal, keduanya dijumlahkan dari awal — hanya beda satu kata kunci:
SELECT id, tanggal, jumlah,
SUM(jumlah) OVER (ORDER BY tanggal) AS bawaan_range,
SUM(jumlah) OVER (ORDER BY tanggal ROWS UNBOUNDED PRECEDING) AS per_baris
FROM penjualan
ORDER BY tanggal, id;Output yang Dihasilkan:
+----+------------+--------+--------------+-----------+
| id | tanggal | jumlah | bawaan_range | per_baris |
+----+------------+--------+--------------+-----------+
| 1 | 2026-07-01 | 5 | 12 | 5 |
| 2 | 2026-07-01 | 7 | 12 | 12 |
| 3 | 2026-07-02 | 3 | 15 | 15 |
| 4 | 2026-07-03 | 4 | 25 | 19 |
| 5 | 2026-07-03 | 6 | 25 | 25 |
| 6 | 2026-07-04 | 8 | 33 | 33 |
| 7 | 2026-07-05 | 5 | 47 | 38 |
| 8 | 2026-07-05 | 9 | 47 | 47 |
+----+------------+--------+--------------+-----------+
8 rows in set (0.00 sec)Kedua kolom berhimpit sempurna di baris 2026-07-02 — sama-sama 15 — lalu berpisah dan berpisah lagi. Mari kita bedah perbedaannya baris demi baris pada tanggal kembar 2026-07-01:
- Kolom
per_baris(ROWS UNBOUNDED PRECEDING): frame menghitung baris fisik dari awal jendela. Barisid 1menjumlahkan dirinya sendiri (5), barisid 2menjumlahkan dua baris (5 + 7 = 12), barisid 3tiga baris (15), barisid 4empat baris (19), dan seterusnya hingga47di baris terakhir. Setiap baris menerima satu nilai lebih besar dari baris sebelumnya — akumulasi yang mulus, persis bentuk chapter running total. - Kolom
bawaan_range(tanpa frame tertulis): frame default yang diberikanORDER BYadalahRANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW.RANGEtidak menghitung baris, melainkan nilai: frame berisi semua baris yang nilainya lebih kecil atau sama dengan nilai baris saat ini menurutORDER BY. Barisid 1danid 2sama-sama bertanggal2026-07-01, sehingga keduanya dianggap setara — istilah resminya peers — dan frame keduanya mencakup semua baris bertanggal≤ 2026-07-01: keduanya menampilkan12. Akumulasi melompat sekaligus per kelompok nilai:12, 12, lalu15, lalu25, 25, lalu33, lalu47, 47.
Aturan singkatnya: ROWS bertanya “berapa baris sebelum posisi ini”, RANGE bertanya “berapa baris yang nilainya mendahului atau menyamai nilai baris ini”. Karena baris sekembar dianggap satu kelompok, RANGE selalu menghasilkan nilai yang sama untuk semua anggota kelompok — itulah kenapa kedua baris 2026-07-01 sama-sama 12 pada kolom bawaan_range namun 5 dan 12 pada kolom per_baris.
RANGE Bersatuan Nilai #
RANGE punya satu kemampuan yang tidak dimiliki ROWS: frame boleh bersatuan nilai dari kolom pengurut. Contoh berikut memakai frame RANGE BETWEEN 2 PRECEDING AND 2 FOLLOWING pada ORDER BY jumlah:
SELECT id, jumlah,
SUM(jumlah) OVER (ORDER BY jumlah
RANGE BETWEEN 2 PRECEDING AND 2 FOLLOWING) AS jendela_nilai
FROM penjualan
ORDER BY jumlah, id;Output yang Dihasilkan:
+----+--------+---------------+
| id | jumlah | jendela_nilai |
+----+--------+---------------+
| 3 | 3 | 17 |
| 4 | 4 | 23 |
| 1 | 5 | 30 |
| 7 | 5 | 30 |
| 5 | 6 | 35 |
| 2 | 7 | 40 |
| 6 | 8 | 30 |
| 8 | 9 | 24 |
+----+--------+---------------+
8 rows in set (0.00 sec)Untuk setiap baris, frame mencakup semua baris yang nilainya berada dalam rentang jumlah ± 2 — bukan “dua baris sebelum dan sesudah”, melainkan “semua yang nilainya dua angka lebih rendah hingga dua angka lebih tinggi”. Baris id 4 (jumlah 4) menjumlahkan baris-baris bernilai 3, 4, 5, 5 menjadi 17; baris id 2 (jumlah 7) menjumlahkan 5, 5, 6, 7, 8, 9 menjadi 40. Rentang semacam ini tidak mungkin dinyatakan dengan ROWS yang buta terhadap nilai — RANGE bersatuan nilai hanya valid untuk kolom pengurut bertipe angka atau tanggal, dan offset harus berupa literal yang sebanding. Pola klasiknya: “rata-rata transaksi bernilai serupa” atau “total penjualan dalam rentang ± 3 hari” tanpa perlu self-join.
Keluarga Bentuk Frame #
Bentuk lengkap frame ditulis dengan BETWEEN ... AND ..., dan kedua ujungnya bisa dipilih dari tiga jenis batas:
ROWS BETWEEN <batas-awal> AND <batas-akhir>| Batas | Makna |
|---|---|
UNBOUNDED PRECEDING | awal jendela (partisi) |
n PRECEDING | n baris / n satuan nilai sebelum baris sekarang |
CURRENT ROW | baris sekarang |
n FOLLOWING | n baris / n satuan nilai sesudah baris sekarang |
UNBOUNDED FOLLOWING | akhir jendela (partisi) |
Dua contoh berikut mempraktikkan bentuk yang paling sering dipakai: jendela tetangga yang melihat ke dua arah, dan sisa deret yang melihat ke depan:
SELECT id, tanggal, jumlah,
SUM(jumlah) OVER (ORDER BY tanggal, id
ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) AS jendela_tetangga,
SUM(jumlah) OVER (ORDER BY tanggal, id
ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS sisa
FROM penjualan
ORDER BY id;Output yang Dihasilkan:
+----+------------+--------+------------------+------+
| id | tanggal | jumlah | jendela_tetangga | sisa |
+----+------------+--------+------------------+------+
| 1 | 2026-07-01 | 5 | 12 | 47 |
| 2 | 2026-07-01 | 7 | 15 | 42 |
| 3 | 2026-07-02 | 3 | 14 | 35 |
| 4 | 2026-07-03 | 4 | 13 | 32 |
| 5 | 2026-07-03 | 6 | 18 | 28 |
| 6 | 2026-07-04 | 8 | 19 | 22 |
| 7 | 2026-07-05 | 5 | 22 | 14 |
| 8 | 2026-07-05 | 9 | 14 | 9 |
+----+------------+--------+------------------+------+
8 rows in set (0.00 sec)Kolom jendela_tetangga menjumlahkan maksimal tiga baris — satu sebelum, sekarang, satu sesudah: baris id 3 menjumlahkan 7 + 3 + 4 = 14, dan dua baris tepi (id 1 serta id 8) hanya menjumlahkan dua baris karena tetangga di luar sana tidak ada. Kolom sisa bekerja ke arah sebaliknya: dari baris sekarang sampai akhir jendela — baris pertama menampilkan 47 (total seluruh deret) dan baris terakhir menampilkan 9 (hanya dirinya). Kedua contoh memakai ORDER BY tanggal, id di dalam jendela supaya setiap baris punya posisi unik dan frame ROWS bekerja tanpa ambiguitas peers; gabungan kolom pengurut seperti ini adalah praktik aman ketika data memuat nilai kembar.
Bentuk pendek tanpa BETWEEN juga sah: ROWS UNBOUNDED PRECEDING yang kita pakai sejak chapter running total adalah kependekan dari ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW. Tabel berikut merangkum pasangan batas yang lazim beserta hasilnya:
| Bentuk frame | Hasil per baris |
|---|---|
ROWS UNBOUNDED PRECEDING | akumulasi dari awal jendela sampai sekarang |
ROWS BETWEEN n PRECEDING AND CURRENT ROW | jumlah/angka n+1 baris terakhir (moving average) |
ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING | jendela tetangga dua arah |
ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING | sisa deret dari sekarang ke akhir |
RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW | akumulasi per kelompok nilai (frame default) |
RANGE BETWEEN n PRECEDING AND n FOLLOWING | rentang nilai di sekitar baris sekarang |
tanpa ORDER BY sama sekali | seluruh partisi untuk semua baris |
Baris terakhir tabel layak ditegaskan: tanpa ORDER BY di dalam OVER, MySQL tidak punya acuan urutan sehingga frame apapun yang membatasi PRECEDING/FOLLOWING kehilangan makna — dan frame default pun melarut menjadi seluruh partisi. Selalu sertakan ORDER BY di dalam jendela sebelum menulis frame.
Frame di Dalam Partisi #
Frame hidup di dalam partisi dan direset oleh partisi. Contoh berikut menjumlahkan dua baris terakhir per kota:
SELECT kota, tanggal, jumlah,
SUM(jumlah) OVER (PARTITION BY kota ORDER BY tanggal, id
ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS dua_terakhir
FROM penjualan
ORDER BY kota, tanggal, id;Output yang Dihasilkan:
+----------+------------+--------+--------------+
| kota | tanggal | jumlah | dua_terakhir |
+----------+------------+--------+--------------+
| Bandung | 2026-07-02 | 3 | 3 |
| Bandung | 2026-07-04 | 8 | 11 |
| Jakarta | 2026-07-01 | 5 | 5 |
| Jakarta | 2026-07-01 | 7 | 12 |
| Jakarta | 2026-07-03 | 4 | 11 |
| Jakarta | 2026-07-05 | 5 | 9 |
| Surabaya | 2026-07-03 | 6 | 6 |
| Surabaya | 2026-07-05 | 9 | 15 |
+----------+------------+--------+--------------+
8 rows in set (0.00 sec)Bandung terbaca 3 lalu 11 — frame bandung tidak pernah melompat ke baris Jakarta meski jarak fisiknya dekat. Baris pertama tiap kota kembali menjumlahkan dirinya sendiri karena 1 PRECEDING tidak menemukan tetangga di dalam partisi kotanya. Kombinasi lengkap PARTITION BY + ORDER BY + frame adalah bentuk paling ekspresif dari window function: partisi memilih kelompok, urutan memilih arah, frame memilih cakupan.
EXCLUDE: Dibaca, Belum Didukung #
Standar SQL mendefinisikan satu lagi klausul frame: EXCLUDE, yang membuang bagian tertentu dari frame — EXCLUDE CURRENT ROW membuang baris sekarang, EXCLUDE GROUP membuang baris sekarang beserta peers-nya, EXCLUDE TIES membuang peers tanpa baris sekarang, dan EXCLUDE NO OTHERS adalah perilaku bawaan. Parser MySQL 8.4 mengenali kata kuncinya, namun implementasinya belum hadir:
-- @error
SELECT SUM(jumlah) OVER (ORDER BY id ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING EXCLUDE CURRENT ROW) AS tanpa_kini FROM penjualan;Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)
ERROR 1235 (42000): This version of MySQL doesn't yet support 'EXCLUDE'
ByeServer menolak dengan error yang jujur: fitur ini belum tersedia di versi MySQL ini. Kebutuhan membuang baris sekarang masih bisa dipenuhi lewat trik aritmetika — misalnya SUM(jumlah) OVER (...) - jumlah menghasilkan jumlah frame dikurangi nilai baris sekarang untuk frame berbasis ROWS yang memuat baris sekarang.
Membersihkan Database Latihan #
Rangkaian latihan selesai dan database latihan kita jatuhkan kembali:
DROP DATABASE belajar_window;Output yang Dihasilkan:
Query OK, 1 row affected (0.00 sec)Referensi #
- Window Functions — Frame Specification — MySQL 8.4 Reference Manual
- Window Function Concepts and Syntax — MySQL 8.4 Reference Manual
- Window Function Descriptions — MySQL 8.4 Reference Manual
Peta window function kini lengkap: OVER menentukan jendela, PARTITION BY membaginya, ORDER BY mengarahkannya, dan frame memilih cakupannya — dengan pilihan sadar antara ROWS yang berhitung baris dan RANGE yang berhitung nilai. Analisis lintas baris di SQL relasional ditutup di sini; kebutuhan data berikutnya bergerak ke arah struktur yang tidak lagi berbentuk tabel rapi — atribut yang berubah-ubah, katalog produk dengan varian tak tetap, log dengan field dinamis. Section berikutnya membahas JSON dan data semi-terstruktur, cara MySQL menyimpan dan meng-query data berbentuk dokumen.