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:

sql
DROP DATABASE IF EXISTS belajar_window;
CREATE DATABASE belajar_window;
USE belajar_window;

Output yang Dihasilkan:

text
Query OK, 0 rows affected, 1 warning (0.00 sec)

Query OK, 1 row affected (0.00 sec)

Database changed
sql
CREATE 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:

text
Query OK, 0 rows affected (0.00 sec)

Query OK, 8 rows affected (0.00 sec)
Records: 8  Duplicates: 0  Warnings: 0

Nilai 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:

sql
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:

text
+----+------------+--------+--------------+-----------+
| 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:

  1. Kolom per_baris (ROWS UNBOUNDED PRECEDING): frame menghitung baris fisik dari awal jendela. Baris id 1 menjumlahkan dirinya sendiri (5), baris id 2 menjumlahkan dua baris (5 + 7 = 12), baris id 3 tiga baris (15), baris id 4 empat baris (19), dan seterusnya hingga 47 di baris terakhir. Setiap baris menerima satu nilai lebih besar dari baris sebelumnya — akumulasi yang mulus, persis bentuk chapter running total.
  2. Kolom bawaan_range (tanpa frame tertulis): frame default yang diberikan ORDER BY adalah RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW. RANGE tidak menghitung baris, melainkan nilai: frame berisi semua baris yang nilainya lebih kecil atau sama dengan nilai baris saat ini menurut ORDER BY. Baris id 1 dan id 2 sama-sama bertanggal 2026-07-01, sehingga keduanya dianggap setara — istilah resminya peers — dan frame keduanya mencakup semua baris bertanggal ≤ 2026-07-01: keduanya menampilkan 12. Akumulasi melompat sekaligus per kelompok nilai: 12, 12, lalu 15, lalu 25, 25, lalu 33, lalu 47, 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:

sql
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:

text
+----+--------+---------------+
| 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:

text
ROWS BETWEEN <batas-awal> AND <batas-akhir>
BatasMakna
UNBOUNDED PRECEDINGawal jendela (partisi)
n PRECEDINGn baris / n satuan nilai sebelum baris sekarang
CURRENT ROWbaris sekarang
n FOLLOWINGn baris / n satuan nilai sesudah baris sekarang
UNBOUNDED FOLLOWINGakhir 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:

sql
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:

text
+----+------------+--------+------------------+------+
| 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 frameHasil per baris
ROWS UNBOUNDED PRECEDINGakumulasi dari awal jendela sampai sekarang
ROWS BETWEEN n PRECEDING AND CURRENT ROWjumlah/angka n+1 baris terakhir (moving average)
ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWINGjendela tetangga dua arah
ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWINGsisa deret dari sekarang ke akhir
RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROWakumulasi per kelompok nilai (frame default)
RANGE BETWEEN n PRECEDING AND n FOLLOWINGrentang nilai di sekitar baris sekarang
tanpa ORDER BY sama sekaliseluruh 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:

sql
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:

text
+----------+------------+--------+--------------+
| 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:

sql
-- @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:

text
Query OK, 0 rows affected (0.00 sec)

ERROR 1235 (42000): This version of MySQL doesn't yet support 'EXCLUDE'
Bye

Server 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:

sql
DROP DATABASE belajar_window;

Output yang Dihasilkan:

text
Query OK, 1 row affected (0.00 sec)

Referensi #

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.