Klausa OVER adalah jendela yang menjadi nama segolongan fungsi ini: kotak penentu baris mana saja yang “terlihat” oleh sebuah fungsi saat menghitung angkanya. Di chapter sebelumnya kita memakai OVER (PARTITION BY produk) sebagai satu blok utuh tanpa membongkar isinya; kali ini kita bedah satu per satu. Mulai dari OVER() kosong yang memperlakukan seluruh tabel sebagai satu jendela, PARTITION BY yang membelah tabel menjadi jendela-jendela per kelompok, hingga ORDER BY di dalam OVER yang diam-diam mengubah agregat biasa menjadi hitungan kumulatif.
Tabel Latihan #
Kita lanjut memakai domain yang sama dengan chapter Konsep Window Function
: penjualan_bulanan berisi 16 baris penjualan bulanan per produk Toko Maju Jaya sepanjang semester I 2026, dengan kolom kanal yang membedakan penjualan toko dan online:
-- tabel latihan: penjualan 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,
kanal text NOT NULL,
jumlah integer NOT NULL
);
INSERT INTO penjualan_bulanan (id, produk, bulan, kanal, jumlah) VALUES
( 1, 'Kopi Gayo', DATE '2026-01-01', 'toko', 120),
( 2, 'Teh Melati', DATE '2026-01-01', 'toko', 80),
( 3, 'Kopi Gayo', DATE '2026-02-01', 'online', 135),
( 4, 'Teh Melati', DATE '2026-02-01', 'toko', 95),
( 5, 'Gula Pasir', DATE '2026-02-01', 'toko', 60),
( 6, 'Gula Pasir', DATE '2026-03-01', 'toko', 70),
( 7, 'Kopi Gayo', DATE '2026-03-01', 'toko', 128),
( 8, 'Teh Melati', DATE '2026-03-01', 'toko', 80),
( 9, 'Gula Pasir', DATE '2026-04-01', 'toko', 70),
(10, 'Kopi Gayo', DATE '2026-04-01', 'online', 150),
(11, 'Teh Melati', DATE '2026-04-01', 'toko', 110),
(12, 'Kopi Gayo', DATE '2026-05-01', 'online', 150),
(13, 'Gula Pasir', DATE '2026-05-01', 'toko', 55),
(14, 'Kopi Gayo', DATE '2026-06-01', 'online', 160),
(15, 'Teh Melati', DATE '2026-06-01', 'online', 130),
(16, 'Gula Pasir', DATE '2026-06-01', 'toko', 75);
SELECT id, produk, bulan, kanal, jumlah
FROM penjualan_bulanan
ORDER BY bulan, produk;Output yang Dihasilkan:
CREATE TABLE
INSERT 0 16
id | produk | bulan | kanal | jumlah
----+------------+------------+--------+--------
1 | Kopi Gayo | 2026-01-01 | toko | 120
2 | Teh Melati | 2026-01-01 | toko | 80
5 | Gula Pasir | 2026-02-01 | toko | 60
3 | Kopi Gayo | 2026-02-01 | online | 135
4 | Teh Melati | 2026-02-01 | toko | 95
6 | Gula Pasir | 2026-03-01 | toko | 70
7 | Kopi Gayo | 2026-03-01 | toko | 128
8 | Teh Melati | 2026-03-01 | toko | 80
9 | Gula Pasir | 2026-04-01 | toko | 70
10 | Kopi Gayo | 2026-04-01 | online | 150
11 | Teh Melati | 2026-04-01 | toko | 110
13 | Gula Pasir | 2026-05-01 | toko | 55
12 | Kopi Gayo | 2026-05-01 | online | 150
16 | Gula Pasir | 2026-06-01 | toko | 75
14 | Kopi Gayo | 2026-06-01 | online | 160
15 | Teh Melati | 2026-06-01 | online | 130
(16 rows)OVER() Kosong: Seluruh Tabel Menjadi Satu Jendela #
Bentuk paling sederhana dari klausa OVER adalah sepasang kurung kosong. Tanpa PARTITION BY dan tanpa ORDER BY, seluruh hasil query menjadi satu jendela tunggal — setiap fungsi agregat di dalamnya melihat semua baris:
-- OVER() kosong: seluruh tabel menjadi satu jendela besar
SELECT bulan,
produk,
kanal,
jumlah,
sum(jumlah) OVER () AS total_semester,
count(*) OVER () AS baris_semester,
round(avg(jumlah) OVER (), 1) AS rata_semester
FROM penjualan_bulanan
ORDER BY bulan, produk;Output yang Dihasilkan:
bulan | produk | kanal | jumlah | total_semester | baris_semester | rata_semester
------------+------------+--------+--------+----------------+----------------+---------------
2026-01-01 | Kopi Gayo | toko | 120 | 1668 | 16 | 104.3
2026-01-01 | Teh Melati | toko | 80 | 1668 | 16 | 104.3
2026-02-01 | Gula Pasir | toko | 60 | 1668 | 16 | 104.3
2026-02-01 | Kopi Gayo | online | 135 | 1668 | 16 | 104.3
2026-02-01 | Teh Melati | toko | 95 | 1668 | 16 | 104.3
2026-03-01 | Gula Pasir | toko | 70 | 1668 | 16 | 104.3
2026-03-01 | Kopi Gayo | toko | 128 | 1668 | 16 | 104.3
2026-03-01 | Teh Melati | toko | 80 | 1668 | 16 | 104.3
2026-04-01 | Gula Pasir | toko | 70 | 1668 | 16 | 104.3
2026-04-01 | Kopi Gayo | online | 150 | 1668 | 16 | 104.3
2026-04-01 | Teh Melati | toko | 110 | 1668 | 16 | 104.3
2026-05-01 | Gula Pasir | toko | 55 | 1668 | 16 | 104.3
2026-05-01 | Kopi Gayo | online | 150 | 1668 | 16 | 104.3
2026-06-01 | Gula Pasir | toko | 75 | 1668 | 16 | 104.3
2026-06-01 | Kopi Gayo | online | 160 | 1668 | 16 | 104.3
2026-06-01 | Teh Melati | online | 130 | 1668 | 16 | 104.3
(16 rows)Ketiga kolom window membawa nilai yang sama di keenam belas baris: total_semester 1668, baris_semester 16, dan rata_semester 104.3. Bandingkan dengan query agregat biasa — SELECT sum(jumlah), count(*) FROM penjualan_bulanan dengan GROUP BY kosong — yang menghasilkan angka-angka identik tetapi terpaksa meringkas seluruh tabel menjadi satu baris. Dengan OVER(), angka pembanding itu berdiri berdampingan dengan rincian bulan per bulan tanpa mengorbankan satu baris pun.
PARTITION BY: Membelah Tabel Menjadi Jendela #
PARTITION BY kolom membelah satu jendela besar menjadi banyak jendela kecil: satu jendela untuk setiap nilai berbeda pada kolom tersebut. Fungsi agregat lalu dihitung terpisah di tiap jendela, dan hasilnya ditempelkan hanya ke baris-baris milik jendela itu. Mari kita lihat perbandingan jumlah baris keluarannya secara berdampingan:
-- PARTITION BY membagi tabel menjadi jendela-jendela per kelompok
-- (a) GROUP BY: hasil 3 baris ringkasan, baris mentah hilang
SELECT produk,
sum(jumlah) AS total,
count(*) AS bulan_tercatat
FROM penjualan_bulanan
GROUP BY produk
ORDER BY produk;
-- (b) OVER (PARTITION BY produk): hasil tetap 16 baris,
-- tiap baris membawa total jendela produknya
SELECT bulan,
produk,
jumlah,
sum(jumlah) OVER (PARTITION BY produk) AS total_produk,
count(*) OVER (PARTITION BY produk) AS bulan_tercatat
FROM penjualan_bulanan
ORDER BY bulan, produk;Output yang Dihasilkan:
produk | total | bulan_tercatat
------------+-------+----------------
Gula Pasir | 330 | 5
Kopi Gayo | 843 | 6
Teh Melati | 495 | 5
(3 rows)
bulan | produk | jumlah | total_produk | bulan_tercatat
------------+------------+--------+--------------+----------------
2026-01-01 | Kopi Gayo | 120 | 843 | 6
2026-01-01 | Teh Melati | 80 | 495 | 5
2026-02-01 | Gula Pasir | 60 | 330 | 5
2026-02-01 | Kopi Gayo | 135 | 843 | 6
2026-02-01 | Teh Melati | 95 | 495 | 5
2026-03-01 | Gula Pasir | 70 | 330 | 5
2026-03-01 | Kopi Gayo | 128 | 843 | 6
2026-03-01 | Teh Melati | 80 | 495 | 5
2026-04-01 | Gula Pasir | 70 | 330 | 5
2026-04-01 | Kopi Gayo | 150 | 843 | 6
2026-04-01 | Teh Melati | 110 | 495 | 5
2026-05-01 | Gula Pasir | 55 | 330 | 5
2026-05-01 | Kopi Gayo | 150 | 843 | 6
2026-06-01 | Gula Pasir | 75 | 330 | 5
2026-06-01 | Kopi Gayo | 160 | 843 | 6
2026-06-01 | Teh Melati | 130 | 495 | 5
(16 rows)Query (a) menghasilkan 3 baris — total per produk — dan baris-baris mentahnya lenyap dari keluaran. Query (b) menghasilkan 16 baris: kolom bulan dan jumlah tetap rinci, sementara total_produk berganti nilai setiap kali produk berganti (330 untuk Gula Pasir, 843 untuk Kopi Gayo, 495 untuk Teh Melati). Angka-angka ini identik dengan hasil query (a); yang berubah hanyalah cara penyajiannya.
| Aspek | GROUP BY produk | OVER (PARTITION BY produk) |
|---|---|---|
| Baris keluaran | satu per kelompok (3 baris) | sama dengan baris masukan (16 baris) |
Kolom selain produk | hanya boleh agregat | bebas — bulan, jumlah, kolom apa pun |
| Angka agregat | jadi inti baris keluaran | jadi kolom tambahan di tiap baris |
| Cocok untuk | laporan ringkasan murni | rincian yang butuh angka pembanding |
PARTITION BY Lebih dari Satu Kolom #
Daftar di dalam PARTITION BY boleh memuat beberapa kolom, dipisah koma, persis seperti daftar di GROUP BY. Jendela lalu terbentuk per kombinasi nilai semua kolom tersebut. Kombinasi produk dan kanal membelah tabel kita menjadi lima jendela:
-- PARTITION BY dengan dua kolom: jendela per kombinasi produk + kanal
SELECT produk,
kanal,
bulan,
jumlah,
sum(jumlah) OVER (PARTITION BY produk, kanal) AS total_kombinasi,
count(*) OVER (PARTITION BY produk, kanal) AS baris_kombinasi
FROM penjualan_bulanan
ORDER BY produk, kanal, bulan;Output yang Dihasilkan:
produk | kanal | bulan | jumlah | total_kombinasi | baris_kombinasi
------------+--------+------------+--------+-----------------+-----------------
Gula Pasir | toko | 2026-02-01 | 60 | 330 | 5
Gula Pasir | toko | 2026-03-01 | 70 | 330 | 5
Gula Pasir | toko | 2026-04-01 | 70 | 330 | 5
Gula Pasir | toko | 2026-05-01 | 55 | 330 | 5
Gula Pasir | toko | 2026-06-01 | 75 | 330 | 5
Kopi Gayo | online | 2026-02-01 | 135 | 595 | 4
Kopi Gayo | online | 2026-04-01 | 150 | 595 | 4
Kopi Gayo | online | 2026-05-01 | 150 | 595 | 4
Kopi Gayo | online | 2026-06-01 | 160 | 595 | 4
Kopi Gayo | toko | 2026-01-01 | 120 | 248 | 2
Kopi Gayo | toko | 2026-03-01 | 128 | 248 | 2
Teh Melati | online | 2026-06-01 | 130 | 130 | 1
Teh Melati | toko | 2026-01-01 | 80 | 365 | 4
Teh Melati | toko | 2026-02-01 | 95 | 365 | 4
Teh Melati | toko | 2026-03-01 | 80 | 365 | 4
Teh Melati | toko | 2026-04-01 | 110 | 365 | 4
(16 rows)Ada dua hal yang layak dicermati dari hasilnya. Pertama, Kopi Gayo kini terbelah menjadi dua jendela sekaligus: empat baris kanal online dengan total 595, dan dua baris kanal toko dengan total 248 — produk yang sama boleh menjadi anggota beberapa jendela berbeda selama kombinasi kolomnya berbeda. Kedua, Teh Melati kanal online hanya berisi satu baris (Juni, 130 unit), sehingga agregat jendela satu baris itu menghasilkan nilai barisnya sendiri. Tidak ada aturan minimum anggota jendela; jendela dengan satu baris tetap jendela yang sah.
ORDER BY di Dalam OVER: Agregat Berubah Kumulatif #
Inilah bagian yang paling sering mengejutkan pemula: menambahkan ORDER BY di dalam OVER tidak sekadar mengurutkan — baginya, agregat berhenti melihat jendela penuh dan mulai melihat awal jendela sampai baris saat ini. sum berubah menjadi jumlah berjalan (running total), dan count menjadi penomoran 1, 2, 3:
-- ORDER BY di dalam OVER mengubah perilaku agregat menjadi kumulatif
-- (a) tanpa ORDER BY: agregat melihat seluruh jendela, total penuh berulang
SELECT bulan, produk, jumlah,
sum(jumlah) OVER (PARTITION BY produk) AS total_produk
FROM penjualan_bulanan
WHERE produk = 'Kopi Gayo'
ORDER BY bulan;
-- (b) dengan ORDER BY bulan: jumlah berjalan menumpuk dari awal partisi
SELECT bulan, produk, jumlah,
sum(jumlah) OVER (PARTITION BY produk ORDER BY bulan) AS total_berjalan,
count(*) OVER (PARTITION BY produk ORDER BY bulan) AS bulan_ke
FROM penjualan_bulanan
WHERE produk = 'Kopi Gayo'
ORDER BY bulan;
-- (c) ORDER BY di OVER juga fondasi fungsi ranking: nomor urut per produk
SELECT bulan, produk, jumlah,
row_number() OVER (PARTITION BY produk ORDER BY bulan) AS bulan_ke
FROM penjualan_bulanan
ORDER BY produk, bulan;Output yang Dihasilkan:
bulan | produk | jumlah | total_produk
------------+-----------+--------+--------------
2026-01-01 | Kopi Gayo | 120 | 843
2026-02-01 | Kopi Gayo | 135 | 843
2026-03-01 | Kopi Gayo | 128 | 843
2026-04-01 | Kopi Gayo | 150 | 843
2026-05-01 | Kopi Gayo | 150 | 843
2026-06-01 | Kopi Gayo | 160 | 843
(6 rows)
bulan | produk | jumlah | total_berjalan | bulan_ke
------------+-----------+--------+----------------+----------
2026-01-01 | Kopi Gayo | 120 | 120 | 1
2026-02-01 | Kopi Gayo | 135 | 255 | 2
2026-03-01 | Kopi Gayo | 128 | 383 | 3
2026-04-01 | Kopi Gayo | 150 | 533 | 4
2026-05-01 | Kopi Gayo | 150 | 683 | 5
2026-06-01 | Kopi Gayo | 160 | 843 | 6
(6 rows)
bulan | produk | jumlah | bulan_ke
------------+------------+--------+----------
2026-02-01 | Gula Pasir | 60 | 1
2026-03-01 | Gula Pasir | 70 | 2
2026-04-01 | Gula Pasir | 70 | 3
2026-05-01 | Gula Pasir | 55 | 4
2026-06-01 | Gula Pasir | 75 | 5
2026-01-01 | Kopi Gayo | 120 | 1
2026-02-01 | Kopi Gayo | 135 | 2
2026-03-01 | Kopi Gayo | 128 | 3
2026-04-01 | Kopi Gayo | 150 | 4
2026-05-01 | Kopi Gayo | 150 | 5
2026-06-01 | Kopi Gayo | 160 | 6
2026-01-01 | Teh Melati | 80 | 1
2026-02-01 | Teh Melati | 95 | 2
2026-03-01 | Teh Melati | 80 | 3
2026-04-01 | Teh Melati | 110 | 4
2026-06-01 | Teh Melati | 130 | 5
(16 rows)Mari kita bedah ketiganya:
- Query (a): enam baris Kopi Gayo (saringan
WHEREberjalan lebih dulu, jadi jendela hanya berisi baris yang lolos) semuanya menerimatotal_produk843 — total penuh partisi, tanpa peduli posisi baris. - Query (b): begitu
ORDER BY bulanmasuk keOVER, kolomtotal_berjalanmenumpuk nilainya — 120, 255, 383, 533, 683, 843 — karena tiap baris hanya melihat baris-baris sebelumnya ditambah dirinya sendiri. Kolombulan_kemenunjukkan efek yang sama padacount(*): 1, 2, 3, 4, 5, 6. - Query (c):
row_number()sama sekali tidak punya arti tanpaORDER BYdi dalamOVER— urutan adalah jantungnya. Di sini setiap produk mendapat penomoran bulan sendiri yang dimulai dari 1 (Kopi Gayo 1–6, Teh Melati dan Gula Pasir 1–5), dan penomoran itu reset setiap kali partisi berganti.
Satu catatan penutup tentang penentuan urutan: ORDER BY bulan pada contoh di atas aman karena setiap produk punya paling banyak satu baris per bulan. Bila dua baris bisa seri pada kolom pengurut, tambahkan kolom pemecah seri — misalnya ORDER BY jumlah DESC, id — agar hasilnya deterministik. Kita akan melihat betapa pentingnya kebiasaan ini di chapter ranking.
Dengan OVER, PARTITION BY, dan ORDER BY di tangan, Anda sudah bisa mengendalikan siapa yang dilihat fungsi (partition) dan sampai mana fungsi melihat (frame implisit). Yang belum bisa dilakukan adalah menghasilkan nilai non-agregat murni seperti nomor urut yang rapi — dan tepat itu pekerjaan ROW_NUMBER(), bintang chapter berikutnya.
Referensi #
- Tutorial: Window Functions
— uraian resmi
OVER,PARTITION BY, danORDER BYdi dalam jendela. - Window Functions (SQL Reference)
— sintaks
OVERbeserta frame default-nya. - SELECT (SQL Reference)
— grammar lengkap klausa
OVER,PARTITION BY, danWINDOW.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
buat-tabel.sql
Unduh berkas-- tabel latihan: penjualan 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,
kanal text NOT NULL,
jumlah integer NOT NULL
);
INSERT INTO penjualan_bulanan (id, produk, bulan, kanal, jumlah) VALUES
( 1, 'Kopi Gayo', DATE '2026-01-01', 'toko', 120),
( 2, 'Teh Melati', DATE '2026-01-01', 'toko', 80),
( 3, 'Kopi Gayo', DATE '2026-02-01', 'online', 135),
( 4, 'Teh Melati', DATE '2026-02-01', 'toko', 95),
( 5, 'Gula Pasir', DATE '2026-02-01', 'toko', 60),
( 6, 'Gula Pasir', DATE '2026-03-01', 'toko', 70),
( 7, 'Kopi Gayo', DATE '2026-03-01', 'toko', 128),
( 8, 'Teh Melati', DATE '2026-03-01', 'toko', 80),
( 9, 'Gula Pasir', DATE '2026-04-01', 'toko', 70),
(10, 'Kopi Gayo', DATE '2026-04-01', 'online', 150),
(11, 'Teh Melati', DATE '2026-04-01', 'toko', 110),
(12, 'Kopi Gayo', DATE '2026-05-01', 'online', 150),
(13, 'Gula Pasir', DATE '2026-05-01', 'toko', 55),
(14, 'Kopi Gayo', DATE '2026-06-01', 'online', 160),
(15, 'Teh Melati', DATE '2026-06-01', 'online', 130),
(16, 'Gula Pasir', DATE '2026-06-01', 'toko', 75);
SELECT id, produk, bulan, kanal, jumlah
FROM penjualan_bulanan
ORDER BY bulan, produk;
order-by-di-over.sql
Unduh berkas-- ORDER BY di dalam OVER mengubah perilaku agregat menjadi kumulatif
-- (a) tanpa ORDER BY: agregat melihat seluruh jendela, total penuh berulang
SELECT bulan, produk, jumlah,
sum(jumlah) OVER (PARTITION BY produk) AS total_produk
FROM penjualan_bulanan
WHERE produk = 'Kopi Gayo'
ORDER BY bulan;
-- (b) dengan ORDER BY bulan: jumlah berjalan menumpuk dari awal partisi
SELECT bulan, produk, jumlah,
sum(jumlah) OVER (PARTITION BY produk ORDER BY bulan) AS total_berjalan,
count(*) OVER (PARTITION BY produk ORDER BY bulan) AS bulan_ke
FROM penjualan_bulanan
WHERE produk = 'Kopi Gayo'
ORDER BY bulan;
-- (c) ORDER BY di OVER juga fondasi fungsi ranking: nomor urut per produk
SELECT bulan, produk, jumlah,
row_number() OVER (PARTITION BY produk ORDER BY bulan) AS bulan_ke
FROM penjualan_bulanan
ORDER BY produk, bulan;
over-kosong.sql
Unduh berkas-- OVER() kosong: seluruh tabel menjadi satu jendela besar
SELECT bulan,
produk,
kanal,
jumlah,
sum(jumlah) OVER () AS total_semester,
count(*) OVER () AS baris_semester,
round(avg(jumlah) OVER (), 1) AS rata_semester
FROM penjualan_bulanan
ORDER BY bulan, produk;
partition-by.sql
Unduh berkas-- PARTITION BY membagi tabel menjadi jendela-jendela per kelompok
-- (a) GROUP BY: hasil 3 baris ringkasan, baris mentah hilang
SELECT produk,
sum(jumlah) AS total,
count(*) AS bulan_tercatat
FROM penjualan_bulanan
GROUP BY produk
ORDER BY produk;
-- (b) OVER (PARTITION BY produk): hasil tetap 16 baris,
-- tiap baris membawa total jendela produknya
SELECT bulan,
produk,
jumlah,
sum(jumlah) OVER (PARTITION BY produk) AS total_produk,
count(*) OVER (PARTITION BY produk) AS bulan_tercatat
FROM penjualan_bulanan
ORDER BY bulan, produk;
partition-multi-kolom.sql
Unduh berkas-- PARTITION BY dengan dua kolom: jendela per kombinasi produk + kanal
SELECT produk,
kanal,
bulan,
jumlah,
sum(jumlah) OVER (PARTITION BY produk, kanal) AS total_kombinasi,
count(*) OVER (PARTITION BY produk, kanal) AS baris_kombinasi
FROM penjualan_bulanan
ORDER BY produk, kanal, bulan;