- Pengenalan Window Functions
- Syntax Dasar & OVER Clause
- PARTITION BY — Membagi Window
- ROW_NUMBER() — Penomoran Baris
- RANK() & DENSE_RANK() — Peringkat
- LAG() & LEAD() — Akses Baris Lain
- NTILE() — Pembagian Kelompok
- Frame Clause — ROWS & RANGE
- Aggregate Functions sebagai Window
- Studi Kasus Nyata
- Quiz Pemahaman
1. Pengenalan Window Functions
Window Functions adalah salah satu fitur paling powerful dalam SQL modern. Berbeda dengan fungsi agregat biasa (seperti SUM(), COUNT()) yang mereduksi baris menjadi satu hasil, window functions melakukan perhitungan pada sekumpulan baris tanpa menggabungkannya — setiap baris tetap ada di hasil query.
Bayangkan Anda punya spreadsheet dan ingin menambah kolom "peringkat penjualan" di sebelah data penjualan. Window functions melakukan persis hal itu — menambah kolom hasil perhitungan ke setiap baris, berdasarkan "jendela" (window) data yang Anda definisikan.
Mengapa Window Functions Penting?
| Fitur | Aggregate Function | Window Function |
|---|---|---|
| Jumlah baris hasil | Dikurangi (per grup jadi 1 baris) | Tetap (semua baris dipertahankan) |
| Akses kolom lain | Hanya kolom di GROUP BY + agregat | Bisa akses kolom detail + agregat |
| Contoh penggunaan | Total penjualan per kota | Peringkat penjualan per kota, dengan detail penjualan |
| Syntax | GROUP BY | OVER (PARTITION BY ...) |
ROW_NUMBER(") OVER(ORDER BY gaji DESC") Dina Mar...
1
2
3
4
5
6
7
8
PARTITION BY departemen Each partition gets own...
/"Use Cases: Top N per group (WHERE rn=1) Paginat..."/
/"ROW_NUMBER selalu unik — tidak pernah tie. Guna..."/
ROW_NUMBER() selalu menghasilkan nomor unik (tidak pernah tie). Jika dua baris punya nilai sama, keduanya tetap dapat nomor berbeda. Urutannya tergantung database engine. Jika Anda ingin handling tie, gunakan RANK() atau DENSE_RANK().
5. RANK() & DENSE_RANK() — Peringkat
Berbeda dengan ROW_NUMBER(), RANK() dan DENSE_RANK() bisa menghasilkan peringkat yang sama untuk baris dengan nilai yang sama (tie/imbang).
Perbedaan RANK vs DENSE_RANK
| Aspek | RANK() | DENSE_RANK() |
|---|---|---|
| Saat tie | Sama, tapi loncat setelah tie | Sama, langsung lanjut berikutnya |
| Contoh: 1,1,? | 1, 1, 3 | 1, 1, 2 |
| Cocok untuk | Kompetisi (ada "juara kosong") | Ranking tanpa gap |
| Total rank unik | Bisa lebih sedikit (ada gap) | Sama dengan jumlah nilai unik |
6. LAG() & LEAD() — Akses Baris Lain
LAG() mengakses nilai dari baris sebelumnya, sedangkan LEAD() mengakses nilai dari baris berikutnya. Ini sangat berguna untuk perhitungan selisih, pertumbuhan, dan perbandingan antar periode.
Parameter LAG & LEAD
| Parameter | Deskripsi | Default |
|---|---|---|
| kolom | Kolom yang nilainya diakses | (wajib) |
| offset | Berapa baris ke belakang/maju | 1 |
| default | Nilai jika tidak ada baris (offset melewati batas) | NULL |
7. NTILE() — Pembagian Kelompok
NTILE(n) membagi baris menjadi n kelompok yang sama besar dan menetapkan nomor kelompok (1 sampai n) ke setiap baris. Berguna untuk membuat kuartil, persentil, atau kategori.
8. Frame Clause — ROWS & RANGE
Frame clause mendefinisikan secara tepat baris mana saja yang termasuk dalam window. Ini memungkinkan perhitungan seperti running total, moving average, dan cumulative sum.
n PRECEDING
n FOLLOWING
RUNNING TOTAL ROWS BETWEEN UNBOUNDED PRECEDING ...
/"Jan:45 Feb:97 Mar:135"/
Jan:45
Feb:97
Mar:135
MOVING AVERAGE ROWS BETWEEN 1 PRECEDING AND 1 F...
/"avg(prev + curr + next)"/
prev
next
/"ROWS = physical rows | RANGE = logical values..."/
ROWS vs RANGE
| Tipe | Deskripsi | Cocok Untuk |
|---|---|---|
| ROWS | Berdasarkan jumlah baris fisik | Moving average, running total |
| RANGE | Berdasarkan nilai ORDER BY | Window berdasarkan nilai/logika |
Jika Anda menulis ORDER BY di dalam OVER() tanpa frame clause, defaultnya adalah RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW. Ini berbeda dari ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW — perbedaannya terlihat saat ada nilai duplikat di kolom ORDER BY.
9. Aggregate Functions sebagai Window
Semua fungsi agregat standar — SUM(), AVG(), COUNT(), MIN(), MAX() — bisa digunakan sebagai window function dengan menambahkan OVER().
-- =============================================
-- Analisis Lengkap dengan Multiple Windows
-- =============================================
SELECT
nama,
departemen,
gaji,
-- Perbandingan dengan rata-rata departemen
AVG(gaji) OVER(PARTITION BY departemen) AS avg_dept,
gaji - AVG(gaji) OVER(PARTITION BY departemen) AS selisih_avg,
-- Perbandingan dengan rata-rata keseluruhan
AVG(gaji) OVER() AS avg_total,
-- Persentil dalam departemen
COUNT(*) OVER(PARTITION BY departemen) AS jumlah_dept,
ROUND(
COUNT(*) OVER(
PARTITION BY departemen
ORDER BY gaji DESC
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) * 100.0 / COUNT(*) OVER(PARTITION BY departemen), 0
) AS persentil_dept,
-- Gap dengan gaji tertinggi di departemen
MAX(gaji) OVER(PARTITION BY departemen) - gaji AS gap_max_dept,
-- Gap dengan gaji terendah di departemen
gaji - MIN(gaji) OVER(PARTITION BY departemen) AS gap_min_dept
FROM karyawan
ORDER BY departemen, gaji DESC;
-- =============================================
-- COUNT sebagai Window: Total per partisi
-- =============================================
SELECT
nama_sales,
wilayah,
bulan,
revenue,
COUNT(*) OVER(PARTITION BY wilayah) AS total_transaksi_wilayah,
COUNT(DISTINCT nama_sales) OVER(PARTITION BY wilayah) AS unique_sales_wilayah
FROM penjualan;
-- =============================================
-- Conditional Aggregation + Window Function
-- =============================================
SELECT
nama,
departemen,
gaji,
-- Persentase gaji terhadap total departemen
ROUND(
gaji * 100.0 / SUM(gaji) OVER(PARTITION BY departemen), 2
) AS pct_of_dept,
-- Kumulatif departemen (urut gaji DESC)
SUM(gaji) OVER(
PARTITION BY departemen
ORDER BY gaji DESC
) AS cumulative_dept
FROM karyawan
ORDER BY departemen, gaji DESC;
10. Studi Kasus Nyata
Studi Kasus 1: Analisis Penjualan E-Commerce
-- =============================================
-- Tabel: orders (pesanan e-commerce)
-- =============================================
-- Analisis: Top 3 produk per kategori berdasarkan revenue
WITH product_revenue AS (
SELECT
p.nama_produk,
p.kategori,
SUM(dp.subtotal) AS total_revenue,
SUM(dp.jumlah) AS total_qty,
ROW_NUMBER() OVER(
PARTITION BY p.kategori
ORDER BY SUM(dp.subtotal) DESC
) AS rank_in_category
FROM detail_pesanan dp
JOIN produk p ON dp.id_produk = p.id_produk
JOIN pesanan ps ON dp.id_pesanan = ps.id_pesanan
WHERE ps.status = 'selesai'
GROUP BY p.nama_produk, p.kategori
)
SELECT
nama_produk,
kategori,
total_revenue,
total_qty,
rank_in_category
FROM product_revenue
WHERE rank_in_category <= 3
ORDER BY kategori, rank_in_category;
-- =============================================
-- Analisis: Trend penjualan bulanan + pertumbuhan
-- =============================================
WITH monthly_sales AS (
SELECT
DATE_FORMAT(tanggal_pesanan, '%Y-%m') AS bulan,
COUNT(*) AS jumlah_order,
SUM(total) AS revenue,
AVG(total) AS avg_order_value
FROM pesanan
WHERE status != 'batal'
GROUP BY DATE_FORMAT(tanggal_pesanan, '%Y-%m')
)
SELECT
bulan,
jumlah_order,
revenue,
avg_order_value,
LAG(revenue) OVER(ORDER BY bulan) AS prev_revenue,
ROUND(
(revenue - LAG(revenue) OVER(ORDER BY bulan)) * 100.0
/ NULLIF(LAG(revenue) OVER(ORDER BY bulan), 0), 2
) AS revenue_growth_pct,
SUM(revenue) OVER(ORDER BY bulan) AS cumulative_revenue,
AVG(revenue) OVER(
ORDER BY bulan
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
) AS moving_avg_3m
FROM monthly_sales
ORDER BY bulan;
Studi Kasus 2: Analisis HR — Distribusi Gaji
-- =============================================
-- Dashboard HR: Lengkap dengan window functions
-- =============================================
WITH salary_analysis AS (
SELECT
nama,
departemen,
posisi,
gaji,
tanggal_masuk,
-- Peringkat dalam departemen
DENSE_RANK() OVER(
PARTITION BY departemen
ORDER BY gaji DESC
) AS rank_dept,
-- Quartile keseluruhan
NTILE(4) OVER(ORDER BY gaji DESC) AS quartile,
-- Running count berdasarkan tanggal masuk
ROW_NUMBER() OVER(
ORDER BY tanggal_masuk
) AS urutan_masuk,
-- Bandingkan dengan rata-rata
gaji - AVG(gaji) OVER() AS selisih_avg_global,
gaji - AVG(gaji) OVER(PARTITION BY departemen) AS selisih_avg_dept,
-- Persentase dari max gaji departemen
ROUND(
gaji * 100.0 / MAX(gaji) OVER(PARTITION BY departemen), 2
) AS pct_of_max_dept,
-- Total karyawan di departemen
COUNT(*) OVER(PARTITION BY departemen) AS total_anggota
FROM karyawan
)
SELECT
nama,
departemen,
posisi,
gaji,
rank_dept,
CASE quartile
WHEN 1 THEN 'Q1 (Top 25%)'
WHEN 2 THEN 'Q2 (25-50%)'
WHEN 3 THEN 'Q3 (50-75%)'
WHEN 4 THEN 'Q4 (Bottom 25%)'
END AS salary_quartile,
selisih_avg_dept,
pct_of_max_dept,
total_anggota
FROM salary_analysis
ORDER BY departemen, gaji DESC;
Studi Kasus 3: Cohort Analysis
-- =============================================
-- Cohort Analysis: Retensi pelanggan per bulan
-- =============================================
WITH customer_cohort AS (
SELECT
c.id_pelanggan,
DATE_FORMAT(c.tanggal_daftar, '%Y-%m') AS cohort_month,
DATE_FORMAT(p.tanggal_pesanan, '%Y-%m') AS order_month,
-- Hitung bulan ke-n sejak daftar
TIMESTAMPDIFF(MONTH,
c.tanggal_daftar,
p.tanggal_pesanan
) AS months_since_signup
FROM pelanggan c
JOIN pesanan p ON c.id_pelanggan = p.id_pelanggan
WHERE p.status != 'batal'
),
cohort_size AS (
SELECT
cohort_month,
COUNT(DISTINCT id_pelanggan) AS cohort_customers
FROM customer_cohort
GROUP BY cohort_month
)
SELECT
cc.cohort_month,
cs.cohort_customers,
cc.months_since_signup,
COUNT(DISTINCT cc.id_pelanggan) AS active_customers,
ROUND(
COUNT(DISTINCT cc.id_pelanggan) * 100.0 / cs.cohort_customers, 2
) AS retention_pct
FROM customer_cohort cc
JOIN cohort_size cs ON cc.cohort_month = cs.cohort_month
GROUP BY cc.cohort_month, cs.cohort_customers, cc.months_since_signup
ORDER BY cc.cohort_month, cc.months_since_signup;
Tips Performa Window Functions
| Tips | Penjelasan |
|---|---|
| Gunakan PARTITION BY yang terindeks | Database lebih cepat memproses partisi yang terindeks |
| Minimalkan jumlah window functions | Setiap OVER() = satu pass data. Gabungkan jika PARTITION BY dan ORDER BY sama |
| Gunakan WINDOW clause | Definisikan window sekali, pakai berulang kali — lebih clean dan efisien |
| Filter dulu, window sesudahnya | Gunakan WHERE sebelum window function untuk mengurangi data yang diproses |
| Hindari ORDER BY tanpa tujuan | ORDER BY di OVER() tanpa frame clause = default RANGE, yang bisa lambat |
11. Quiz: Uji Pemahamanmu!
Setelah membaca tutorial di atas, jawablah 5 pertanyaan berikut untuk menguji pemahamanmu tentang SQL Window Functions: