LAG dan LEAD SQL: Bandingkan Baris Sekarang dengan Sebelumnya
TL;DR
LAG dan LEAD adalah window function SQL yang narik nilai dari baris lain dalam hasil query yang sama: LAG ngambil dari baris sebelumnya, LEAD dari baris berikutnya. Sintaksnya LAG(kolom, offset, default) OVER (PARTITION BY grup ORDER BY urutan). Dipakai buat ngitung selisih dan pertumbuhan antar periode tanpa perlu self-join.
LAG dan LEAD adalah window function SQL yang narik nilai dari baris lain dalam hasil query yang sama — LAG dari baris sebelumnya, LEAD dari baris berikutnya.
Ini yang jawab pertanyaan paling sering ditanya bos: "omzet bulan ini naik berapa persen dari bulan lalu?"
Tanpa LAG, kamu harus self-join tabel ke dirinya sendiri, bikin kondisi join yang ribet, dan berdoa nggak ada bulan yang bolong. Dengan LAG, satu baris kode.
Apa itu LAG dan LEAD di SQL?
LAG dan LEAD adalah window function — fungsi yang ngeliat baris lain tanpa ngegabungin baris. Hasil query-mu tetap punya jumlah baris yang sama; cuma nambah kolom yang isinya nilai dari tetangga.
Sintaksnya:
LAG(kolom, offset, nilai_default) OVER (
PARTITION BY grup
ORDER BY urutan
)
Tiga argumen, dua di antaranya opsional:
- kolom — nilai apa yang mau diambil dari baris tetangga.
- offset — mundur/maju berapa baris. Default 1.
- nilai_default — dipakai kalau baris tetangganya nggak ada. Default NULL.
Bagian OVER (...) wajib. Di dalamnya, ORDER BY itu yang nentuin siapa "sebelumnya". Tanpa ORDER BY, SQL nggak tau urutan barisnya, dan hasilnya nggak bisa diandalkan.
Gimana cara ngitung growth bulan ke bulan pakai LAG?
Tabel omzet_bulanan dari toko_berkah:
bulan omzet
2026-01 42500000
2026-02 38200000
2026-03 51800000
2026-04 49100000
2026-05 58400000
Query-nya:
SELECT
bulan,
omzet,
LAG(omzet) OVER (ORDER BY bulan) AS omzet_bulan_lalu,
omzet - LAG(omzet) OVER (ORDER BY bulan) AS selisih,
ROUND(
100.0 * (omzet - LAG(omzet) OVER (ORDER BY bulan))
/ LAG(omzet) OVER (ORDER BY bulan), 1
) AS growth_persen
FROM omzet_bulanan
ORDER BY bulan;
Hasilnya:
| bulan | omzet | omzet_bulan_lalu | selisih | growth_persen |
|---|---|---|---|---|
| 2026-01 | 42.500.000 | NULL | NULL | NULL |
| 2026-02 | 38.200.000 | 42.500.000 | −4.300.000 | −10,1 |
| 2026-03 | 51.800.000 | 38.200.000 | +13.600.000 | +35,6 |
| 2026-04 | 49.100.000 | 51.800.000 | −2.700.000 | −5,2 |
| 2026-05 | 58.400.000 | 49.100.000 | +9.300.000 | +18,9 |
Baris Januari NULL semua — wajar, nggak ada bulan sebelumnya buat dibandingin.
Lonjakan Maret +35,6% itu Ramadan. Turun lagi April setelah lebaran. Pola musiman kayak gini nggak keliatan kalau kamu cuma lihat angka omzet mentahnya.
Query-nya jadi berulang-ulang nulis LAG(omzet) OVER (ORDER BY bulan) tiga kali. Rapiin pakai CTE:
WITH dengan_lag AS (
SELECT
bulan,
omzet,
LAG(omzet) OVER (ORDER BY bulan) AS omzet_lalu
FROM omzet_bulanan
)
SELECT
bulan,
omzet,
omzet_lalu,
omzet - omzet_lalu AS selisih,
ROUND(100.0 * (omzet - omzet_lalu) / omzet_lalu, 1) AS growth_persen
FROM dengan_lag
ORDER BY bulan;
Lebih enak dibaca, dan kalau mau ganti logikanya cukup edit satu tempat. Kalau CTE masih asing, cek glossary CTE.
Kenapa harus pakai PARTITION BY?
Sekarang datanya per kota:
kota bulan omzet
Jakarta 2026-01 28000000
Jakarta 2026-02 31000000
Surabaya 2026-01 14500000
Surabaya 2026-02 17200000
Kalau kamu pakai LAG(omzet) OVER (ORDER BY bulan) doang, baris pertama Surabaya bakal ngambil nilai dari baris terakhir Jakarta. Growth-nya jadi ngawur total.
PARTITION BY yang benerin ini:
SELECT
kota,
bulan,
omzet,
LAG(omzet) OVER (PARTITION BY kota ORDER BY bulan) AS omzet_bulan_lalu
FROM omzet_kota
ORDER BY kota, bulan;
PARTITION BY bikin LAG mulai ulang dari nol tiap ganti kota. Baris pertama tiap kota balikin NULL — itu yang bener.
Aturan praktis: kalau datamu punya dimensi grup (kota, produk, pelanggan), hampir selalu butuh PARTITION BY. Lupa PARTITION BY adalah bug paling sering di window function, dan yang paling susah kedeteksi — soalnya query-nya jalan, angkanya keluar, tapi salah.
Kapan pakai LEAD?
LEAD narik dari baris berikutnya. Kepake buat ngitung jarak ke kejadian selanjutnya.
Contoh: berapa hari jeda antar belanja tiap pelanggan toko_berkah?
SELECT
pelanggan_id,
tanggal,
LEAD(tanggal) OVER (
PARTITION BY pelanggan_id
ORDER BY tanggal
) AS transaksi_berikutnya,
LEAD(tanggal) OVER (
PARTITION BY pelanggan_id
ORDER BY tanggal
) - tanggal AS jarak_hari
FROM transaksi
ORDER BY pelanggan_id, tanggal;
Hasilnya buat pelanggan P-0042:
| tanggal | transaksi_berikutnya | jarak_hari |
|---|---|---|
| 2026-03-02 | 2026-03-09 | 7 |
| 2026-03-09 | 2026-03-17 | 8 |
| 2026-03-17 | 2026-04-25 | 39 |
| 2026-04-25 | NULL | NULL |
Pola belanjanya mingguan, terus tiba-tiba bolong 39 hari. Itu sinyal pelanggan hampir hilang.
Baris terakhir NULL — belum ada transaksi berikutnya. Kalau kamu filter WHERE transaksi_berikutnya IS NULL (lewat subquery), kamu dapat daftar transaksi terakhir tiap pelanggan. Itu bahan mentah buat analisis churn.
Gimana cara pakai offset lebih dari 1?
Argumen kedua bikin kamu bisa loncat lebih jauh. Buat bandingin sama periode yang sama tahun lalu — 12 bulan ke belakang:
SELECT
bulan,
omzet,
LAG(omzet, 12) OVER (ORDER BY bulan) AS omzet_tahun_lalu,
ROUND(
100.0 * (omzet - LAG(omzet, 12) OVER (ORDER BY bulan))
/ LAG(omzet, 12) OVER (ORDER BY bulan), 1
) AS yoy_persen
FROM omzet_bulanan
ORDER BY bulan;
Ini year-over-year growth. Lebih jujur dari month-over-month buat bisnis musiman, soalnya dia bandingin Ramadan sama Ramadan, bukan Ramadan sama bulan biasa.
Syaratnya: datanya harus lengkap tanpa bulan bolong. LAG ngitung mundur 12 baris, bukan 12 bulan. Kalau ada bulan yang nggak ada barisnya, offset-nya meleset diam-diam.
Argumen ketiga buat ganti NULL:
LAG(omzet, 1, 0) OVER (ORDER BY bulan)
Baris pertama keluar 0, bukan NULL. Tapi hati-hati — kalau kamu pakai nilai itu sebagai pembagi, kamu dapat division by zero. Kadang NULL justru lebih aman.
Contoh kasus: nemuin produk yang lagi meluncur turun
Di dataset toko_berkah, 82 produk, data omzet 6 bulan terakhir. Pertanyaannya: produk mana yang turun 2 bulan berturut-turut?
WITH bulanan AS (
SELECT
produk,
bulan,
SUM(total) AS omzet
FROM transaksi
GROUP BY produk, bulan
),
dengan_lag AS (
SELECT
produk,
bulan,
omzet,
LAG(omzet, 1) OVER (PARTITION BY produk ORDER BY bulan) AS m1,
LAG(omzet, 2) OVER (PARTITION BY produk ORDER BY bulan) AS m2
FROM bulanan
)
SELECT
produk,
bulan,
m2 AS dua_bulan_lalu,
m1 AS bulan_lalu,
omzet AS bulan_ini
FROM dengan_lag
WHERE omzet < m1
AND m1 < m2
AND bulan = '2026-05'
ORDER BY (m2 - omzet) DESC;
Hasilnya: 7 produk dari 82. Yang paling parah Minyak Goreng 2L — turun dari 8,4 juta ke 6,1 juta ke 4,2 juta. Anjlok 50% dalam dua bulan.
Pas dicek, ternyata kompetitor di ujung jalan lagi jual minyak lebih murah 3.000 per botol. Tanpa query ini, pemilik toko baru sadar 3 bulan kemudian pas stok numpuk.
Perhatiin: filter omzet < m1 ditaruh di query luar, bukan di WHERE query dalam. Itu bukan gaya-gayaan — window function nggak bisa dipakai di WHERE yang sama.
Kesalahan umum LAG dan LEAD
1. Lupa ORDER BY di dalam OVER. Tanpa ORDER BY, "baris sebelumnya" nggak punya arti. Beberapa database ngasih error; yang lain diam-diam ngasih hasil ngaco.
2. Lupa PARTITION BY. Baris pertama tiap grup nyolong nilai dari grup sebelumnya. Query jalan, angka salah.
3. Pakai LAG di WHERE. Nggak bisa. Window function dieksekusi setelah WHERE. Bungkus pakai CTE atau subquery, filter di luar.
4. Nganggap offset = periode. LAG(omzet, 12) mundur 12 baris, bukan 12 bulan. Kalau ada bulan bolong, hasilnya bergeser. Pastiin datanya lengkap atau bikin tabel kalender dulu.
5. Bagi dengan hasil LAG tanpa cek NULL atau nol. Growth persen di baris pertama pasti error atau NULL. Bungkus pakai NULLIF: / NULLIF(omzet_lalu, 0).
6. Pakai MySQL 5.7. Window function baru ada di MySQL 8.0. Cek versi kamu pakai SELECT VERSION(). Detail lengkapnya di dokumentasi window function PostgreSQL yang jadi acuan standar SQL.
FAQ
Apa beda LAG dan LEAD di SQL?
LAG narik nilai dari baris sebelumnya dalam urutan yang kamu tentuin di ORDER BY. LEAD narik dari baris berikutnya. Keduanya sintaksnya identik, cuma arahnya kebalik. LAG dipakai buat ngitung pertumbuhan dari periode lalu; LEAD dipakai buat ngitung jarak ke kejadian berikutnya, misalnya berapa hari sampai pelanggan belanja lagi.
Kenapa hasil LAG saya NULL di baris pertama?
Itu perilaku normal. Baris pertama nggak punya baris sebelumnya, jadi LAG balikin NULL. Kalau kamu mau ganti jadi angka lain, isi argumen ketiga: LAG(omzet, 1, 0) bikin baris pertama keluar 0, bukan NULL. Hati-hati kalau kamu bagi pakai nilai itu — pembagian dengan nol bikin error di beberapa database.
Apakah LAG dan LEAD ada di MySQL?
Ada, tapi mulai MySQL 8.0. Versi 5.7 ke bawah nggak punya window function sama sekali, jadi kamu harus pakai self-join atau variabel. PostgreSQL udah dukung sejak versi 8.4, SQL Server sejak 2012, BigQuery dan Snowflake dukung penuh. Cek versi MySQL kamu pakai SELECT VERSION().
Bisa nggak pakai LAG di klausa WHERE?
Nggak bisa langsung. Window function dieksekusi setelah WHERE, jadi WHERE belum tau hasil LAG-nya. Solusinya: bungkus query-nya jadi subquery atau CTE, lalu filter di query luar. Pola ini juga berlaku buat semua window function lain kayak ROW_NUMBER dan RANK.
Apa gunanya PARTITION BY di LAG?
PARTITION BY bikin LAG mulai ulang dari awal di tiap grup. Kalau kamu ngitung growth per kota dan nggak pakai PARTITION BY kota, baris pertama Surabaya bakal ngambil nilai dari baris terakhir Jakarta — angkanya jadi ngawur. Dengan PARTITION BY kota, tiap kota punya urutannya sendiri dan baris pertamanya balikin NULL.
Penutup
LAG buat ngitung perubahan dari masa lalu. LEAD buat ngukur jarak ke masa depan. Dua-duanya butuh ORDER BY di dalam OVER, dan hampir selalu butuh PARTITION BY kalau datamu punya grup.
Latihan yang paling cepet nempel: ambil tabel penjualan di kerjaanmu, bikin growth bulan ke bulan, terus lihat bulan mana yang paling anjlok. Sepuluh menit, dan kamu bakal nemu sesuatu.
Lanjut baca panduan window function SQL buat kenalan sama ROW_NUMBER dan RANK, atau cek fungsi SUM kalau kamu perlu ngerapiin agregatnya duluan.
Mau praktek langsung? Mulai latihan SQL gratis
Latihan interaktif, langsung di browser.
Artikel Terkait
7 SQL Client Gratis Terbaik buat Analis Data (2026)
Nggak perlu bayar buat nulis query. Ini 7 SQL client gratis yang beneran dipakai analis, plus kelebihan dan kekurangannya masing-masing.
SQL Style Guide: Cara Nulis Query yang Enak Dibaca Tim
Sepuluh aturan SQL style guide yang bikin query kamu kebaca sama tim — dan sama kamu sendiri 6 bulan lagi. Lengkap dengan contoh sebelum-sesudah.
Take-Home Test SQL Data Analyst: Contoh Soal + Cara Ngerjainnya
Contoh soal take-home test SQL yang beneran dipakai perusahaan, dikerjain step by step — dari baca soal, nulis query, sampai nyusun insight yang bikin recruiter nengok.