☀️Siang
Database

SQL Window Functions: Panduan Lengkap

Kuasai ROW_NUMBER, RANK, DENSE_RANK, LAG, LEAD, NTILE, NTILE, dan frame clauses — teknik SQL tingkat lanjut untuk analisis data

Artikel: Sql Window Functions Artikel: Sql Window Functions


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 hasilDikurangi (per grup jadi 1 baris)Tetap (semua baris dipertahankan)
Akses kolom lainHanya kolom di GROUP BY + agregatBisa akses kolom detail + agregat
Contoh penggunaanTotal penjualan per kotaPeringkat penjualan per kota, dengan detail penjualan
SyntaxGROUP BYOVER (PARTITION BY ...)
Diagram: ROW_NUMBER
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 vs RANK

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).

SQL — RANK vs DENSE_RANK

Nama Gaji ROW_NUM RANK DENSE_RANK Note

Dina 18jt 1 1 1

Gilang 17jt 2 2 2

Budi 15jt 3 3 3

Hana 14jt 4 4 4

Ali 8jt 6 6 6 tie!

Joko 8jt 7 6 6 tie!

Fitri 7.5jt 8 8 7

Indra 7.2jt 9 9 8

ROW_NUMBER 6, 7 Selalu unik, tidak ada tie Urut...

RANK 6, 8 (gap!) Tie = sama, lalu loncat Cocok...

DENSE_RANK 6, 7 (no gap) Tie = sama, lanjut be...

Perbedaan RANK vs DENSE_RANK

Aspek RANK() DENSE_RANK()
Saat tieSama, tapi loncat setelah tieSama, langsung lanjut berikutnya
Contoh: 1,1,?1, 1, 31, 1, 2
Cocok untukKompetisi (ada "juara kosong")Ranking tanpa gap
Total rank unikBisa 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.

Diagram

Andi — Jan Revenue: 45jt

Andi — Feb Revenue: 52jt

Andi — Mar Revenue: 38jt

LAG(revenue, 1, 0) Feb gets Jan value: 45jt

LEAD(revenue, 1, 0) Jan gets Feb value: 52jt

Result — LAG Example Bulan Revenue LAG (prev) S...

LEAD = look forward | LAG = look backward | ...

Parameter LAG & LEAD

Parameter Deskripsi Default
kolomKolom yang nilainya diakses(wajib)
offsetBerapa baris ke belakang/maju1
defaultNilai 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.

Diagram

Dina 18jt Gilang 17jt

Q1 — Top 25%

Budi 15jt Hana 14jt Eka 8.5jt

Q2 — 25-50%

Ali 8jt Joko 7.8jt

Q3 — 50-75%

Fitri 7.5jt Indra 7.2jt Citra 7jt

Q4 — Bottom 25%

NTILE membagi baris menjadi N kelompok sama bes...

NTILE(2) → Top/Bottom | NTILE(3) → Tinggi/Sed...



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.

SQL — Frame Clause
Diagram: Frame Clause
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
ROWSBerdasarkan jumlah baris fisikMoving average, running total
RANGEBerdasarkan nilai ORDER BYWindow berdasarkan nilai/logika
💡 Default Frame

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().

SQL — Aggregate sebagai Window Function
-- =============================================
-- 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

SQL — Studi Kasus 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

SQL — Studi Kasus HR
-- =============================================
-- 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

SQL — 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 terindeksDatabase lebih cepat memproses partisi yang terindeks
Minimalkan jumlah window functionsSetiap OVER() = satu pass data. Gabungkan jika PARTITION BY dan ORDER BY sama
Gunakan WINDOW clauseDefinisikan window sekali, pakai berulang kali — lebih clean dan efisien
Filter dulu, window sesudahnyaGunakan WHERE sebelum window function untuk mengurangi data yang diproses
Hindari ORDER BY tanpa tujuanORDER 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:

Pertanyaan 1: Apa perbedaan utama antara RANK() dan DENSE_RANK()?

a) RANK() lebih cepat dari DENSE_RANK()
b) DENSE_RANK() tidak punya gap setelah tie, RANK() punya gap
c) RANK() hanya bisa di PostgreSQL, DENSE_RANK() universal
d) Tidak ada perbedaan, keduanya identik

Pertanyaan 2: Fungsi apa yang digunakan untuk mengakses nilai baris sebelumnya?

a) LEAD()
b) PREVIOUS()
c) LAG()
d) BEFORE()

Pertanyaan 3: Mengapa window functions tidak bisa digunakan di clause WHERE?

a) Karena window functions hanya untuk PostgreSQL
b) Karena window functions dieksekusi setelah WHERE dalam urutan SQL
c) Karena window functions membutuhkan HAVING
d) Sebenarnya bisa, tidak ada batasan

Pertanyaan 4: Apa fungsi dari NTILE(4)?

a) Mengambil 4 baris pertama
b) Membagi baris menjadi 4 kelompok (quartile)
c) Menampilkan 4 kolom terakhir
d) Mengurutkan 4 baris secara descending

Pertanyaan 5: Untuk membuat moving average 3 bulan, frame clause yang tepat adalah?

a) ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
b) ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING
c) ROWS BETWEEN 3 PRECEDING AND 3 FOLLOWING
d) ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
🔍 Zoom
100%
🎨 Tema