Analisis deret waktu selalu bertanya hal yang sama: “berapa nilainya kemarin, dan berapa sekarang?”. Dua angka itu harus berdampingan dalam satu baris agar selisihnya bisa dihitung, padahal di tabel penjualan nilai kemarin tersimpan di baris lain. LAG() mengambil nilai dari baris sebelumnya dan LEAD() mengambil nilai dari baris berikutnya — keduanya memindahkan nilai antar baris tanpa join dan tanpa merubah jumlah baris hasil. Pada chapter ini kita memakai keduanya pada data penjualan harian: menghitung pertumbuhan hari ke hari, mengelola NULL yang muncul di baris tepi, memakai offset lebih dari satu, lalu membuktikan kenapa PARTITION BY wajib hadir agar nilai Bandung tidak bocor menjadi “kemarin”-nya Jakarta.
Menyiapkan Tabel Latihan #
Kita pakai database latihan belajar_window dengan tabel penjualan dua belas baris: empat transaksi untuk tiap kota, tanggal berjalan maju, satu produk andalan per kota:
DROP DATABASE IF EXISTS belajar_window;
CREATE DATABASE belajar_window;
USE belajar_window;Output yang Dihasilkan:
Query OK, 0 rows affected, 1 warning (0.00 sec)
Query OK, 1 row affected (0.00 sec)
Database changedCREATE 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-03-01', 'Jakarta', 'Kopi Gayo', 10, 55000.00),
('2026-03-01', 'Bandung', 'Teh Melati', 8, 25000.00),
('2026-03-02', 'Surabaya', 'Kopi Toraja', 12, 62000.00),
('2026-03-03', 'Jakarta', 'Kopi Gayo', 14, 55000.00),
('2026-03-03', 'Bandung', 'Teh Melati', 6, 25000.00),
('2026-03-04', 'Surabaya', 'Kopi Toraja', 15, 62000.00),
('2026-03-05', 'Jakarta', 'Kopi Gayo', 13, 55000.00),
('2026-03-05', 'Bandung', 'Teh Melati', 9, 25000.00),
('2026-03-06', 'Surabaya', 'Kopi Toraja', 18, 62000.00),
('2026-03-07', 'Jakarta', 'Kopi Gayo', 20, 55000.00),
('2026-03-07', 'Bandung', 'Teh Melati', 11, 25000.00),
('2026-03-08', 'Surabaya', 'Kopi Toraja', 16, 62000.00);Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)
Query OK, 12 rows affected (0.00 sec)
Records: 12 Duplicates: 0 Warnings: 0Seluruh tanggal ditulis sebagai literal sehingga urutan deret waktu pasti dan hasil setiap contoh dapat dibandingkan. Kita mulai dari satu kota saja supaya mekanisme dasarnya terlihat jernih sebelum berhadapan dengan tiga kota sekaligus.
LAG: Mengambil Nilai Baris Sebelumnya #
Query berikut menampilkan deret Jakarta berdampingan dengan nilainya pada baris sebelumnya:
SELECT tanggal, kota, jumlah,
LAG(jumlah) OVER (ORDER BY tanggal) AS penjualan_sebelumnya
FROM penjualan
WHERE kota = 'Jakarta'
ORDER BY tanggal;Output yang Dihasilkan:
+------------+---------+--------+----------------------+
| tanggal | kota | jumlah | penjualan_sebelumnya |
+------------+---------+--------+----------------------+
| 2026-03-01 | Jakarta | 10 | NULL |
| 2026-03-03 | Jakarta | 14 | 10 |
| 2026-03-05 | Jakarta | 13 | 14 |
| 2026-03-07 | Jakarta | 20 | 13 |
+------------+---------+--------+----------------------+
4 rows in set (0.00 sec)Deret Jakarta terbaca 10, 14, 13, 20, dan kolom penjualan_sebelumnya bergeser satu langkah: NULL, 10, 14, 13. Mari kita bedah mekanismenya: ORDER BY tanggal di dalam OVER menata baris-baris jendela menjadi deret waktu, lalu LAG(jumlah) menyalin nilai jumlah dari satu baris sebelum posisi baris saat ini. Baris pertama deret (2026-03-01) tidak punya baris sebelumnya, sehingga LAG mengembalikan NULL — bukan nol, bukan error, melainkan penanda bahwa pembanding tidak ada. Perhatikan pula bahwa klausa WHERE kota = 'Jakarta' berjalan lebih dulu: window function hanya melihat empat baris yang lolos saringan, persis perilaku urutan evaluasi yang kita buktikan di chapter ROW_NUMBER.
Selisih Harian dan Argumen Default #
Nilai yang dipindahkan LAG bisa langsung dipakai dalam ekspresi. Selisih hari ini dikurangi kemarin adalah resep klasik kolom pertumbuhan:
SELECT tanggal, jumlah,
LAG(jumlah) OVER (ORDER BY tanggal) AS sebelumnya,
jumlah - LAG(jumlah) OVER (ORDER BY tanggal) AS pertumbuhan
FROM penjualan
WHERE kota = 'Jakarta'
ORDER BY tanggal;Output yang Dihasilkan:
+------------+--------+------------+-------------+
| tanggal | jumlah | sebelumnya | pertumbuhan |
+------------+--------+------------+-------------+
| 2026-03-01 | 10 | NULL | NULL |
| 2026-03-03 | 14 | 10 | 4 |
| 2026-03-05 | 13 | 14 | -1 |
| 2026-03-07 | 20 | 13 | 7 |
+------------+--------+------------+-------------+
4 rows in set (0.00 sec)Pertumbuhan terbaca NULL, 4, -1, 7: naik empat unit pada 2026-03-03, turun satu unit pada 2026-03-05, lalu naik tujuh unit pada 2026-03-07. Baris pertama tetap NULL karena jumlah - NULL menghasilkan NULL — aritmetika NULL selalu NULL. LAG menerima tiga argumen — LAG(kolom, n, default) — dan argumen ketiga mengisi nilai pengganti ketika baris sumber tidak ada:
SELECT tanggal, jumlah,
jumlah - LAG(jumlah, 1, 0) OVER (ORDER BY tanggal) AS pertumbuhan
FROM penjualan
WHERE kota = 'Jakarta'
ORDER BY tanggal;Output yang Dihasilkan:
+------------+--------+-------------+
| tanggal | jumlah | pertumbuhan |
+------------+--------+-------------+
| 2026-03-01 | 10 | 10 |
| 2026-03-03 | 14 | 4 |
| 2026-03-05 | 13 | -1 |
| 2026-03-07 | 20 | 7 |
+------------+--------+-------------+
4 rows in set (0.00 sec)Dengan default 0, pertumbuhan baris pertama menjadi 10 — 10 - 0 — yang dalam bahasa bisnis berarti “hari pertama periode tumbuh penuh dari nol”. Argumen kedua 1 adalah offset: berapa baris yang dilewati. Offset yang sama berlaku dua arah pada LEAD.
LEAD: Mengambil Nilai Baris Berikutnya #
LEAD() bekerja simetris ke arah depan — nilainya berasal dari baris sesudah baris saat ini, dan kini baris terakhir deret yang kehilangan tetangga:
SELECT tanggal, jumlah,
LEAD(jumlah) OVER (ORDER BY tanggal) AS penjualan_besok
FROM penjualan
WHERE kota = 'Jakarta'
ORDER BY tanggal;Output yang Dihasilkan:
+------------+--------+-----------------+
| tanggal | jumlah | penjualan_besok |
+------------+--------+-----------------+
| 2026-03-01 | 10 | 14 |
| 2026-03-03 | 14 | 13 |
| 2026-03-05 | 13 | 20 |
| 2026-03-07 | 20 | NULL |
+------------+--------+-----------------+
4 rows in set (0.00 sec)Kolom penjualan_besok terbaca 14, 13, 20, NULL — baris terakhir 2026-03-07 tidak punya hari berikutnya di dalam data. Kombinasi LAG dan LEAD dalam satu query membuat setiap baris melihat tetangga kiri dan kanannya sekaligus; pola ini dipakai antara lain untuk mendeteksi perubahan arah tren. Offset depan juga diatur lewat argumen kedua, misalnya LEAD(jumlah, 2) mengambil nilai dua baris di depan dengan dua baris terakhir bernilai NULL.
PARTITION BY: Mencegah Kebocoran Antar Kota #
Seluruh contoh di atas menyaring satu kota dengan WHERE. Bagaimana bila laporan harus menampung ketiga kota sekaligus? Jawaban intuitif — menghapus WHERE dan membiarkan satu jendela global — menghasilkan laporan yang salah diam-diam:
SELECT tanggal, kota, jumlah,
LAG(jumlah) OVER (ORDER BY tanggal, kota) AS sebelumnya
FROM penjualan
ORDER BY tanggal, kota;Output yang Dihasilkan:
+------------+----------+--------+------------+
| tanggal | kota | jumlah | sebelumnya |
+------------+----------+--------+------------+
| 2026-03-01 | Bandung | 8 | NULL |
| 2026-03-01 | Jakarta | 10 | 8 |
| 2026-03-02 | Surabaya | 12 | 10 |
| 2026-03-03 | Bandung | 6 | 12 |
| 2026-03-03 | Jakarta | 14 | 6 |
| 2026-03-04 | Surabaya | 15 | 14 |
| 2026-03-05 | Bandung | 9 | 15 |
| 2026-03-05 | Jakarta | 13 | 9 |
| 2026-03-06 | Surabaya | 18 | 13 |
| 2026-03-07 | Bandung | 11 | 18 |
| 2026-03-07 | Jakarta | 20 | 11 |
| 2026-03-08 | Surabaya | 16 | 20 |
+------------+----------+--------+------------+
12 rows in set (0.00 sec)Lihat baris 2026-03-01 milik Jakarta: kolom sebelumnya berisi 8 — padahal nilai itu adalah penjualan Bandung pada tanggal yang sama, kota yang berbeda pasar dan tim. Nilai antar kota bocor menjadi “kemarin”-nya kota lain, dan sekali saja terjadi, seluruh kolom selisih di bawahnya ikut menyesatkan. Perbaikannya adalah membagi jendela per kota dengan PARTITION BY, sehingga LAG hanya melihat tetangga di dalam kotanya sendiri:
SELECT kota, tanggal, jumlah,
LAG(jumlah) OVER (PARTITION BY kota ORDER BY tanggal) AS sebelumnya,
jumlah - LAG(jumlah, 1, 0) OVER (PARTITION BY kota ORDER BY tanggal) AS pertumbuhan
FROM penjualan
ORDER BY kota, tanggal;Output yang Dihasilkan:
+----------+------------+--------+------------+-------------+
| kota | tanggal | jumlah | sebelumnya | pertumbuhan |
+----------+------------+--------+------------+-------------+
| Bandung | 2026-03-01 | 8 | NULL | 8 |
| Bandung | 2026-03-03 | 6 | 8 | -2 |
| Bandung | 2026-03-05 | 9 | 6 | 3 |
| Bandung | 2026-03-07 | 11 | 9 | 2 |
| Jakarta | 2026-03-01 | 10 | NULL | 10 |
| Jakarta | 2026-03-03 | 14 | 10 | 4 |
| Jakarta | 2026-03-05 | 13 | 14 | -1 |
| Jakarta | 2026-03-07 | 20 | 13 | 7 |
| Surabaya | 2026-03-02 | 12 | NULL | 12 |
| Surabaya | 2026-03-04 | 15 | 12 | 3 |
| Surabaya | 2026-03-06 | 18 | 15 | 3 |
| Surabaya | 2026-03-08 | 16 | 18 | -2 |
+----------+------------+--------+------------+-------------+
12 rows in set (0.00 sec)Dua belas baris keluar dengan tiga deret berjalan mandiri: Jakarta NULL/10, 4, -1, 7, Bandung NULL/8, -2, 3, 2, dan Surabaya NULL/12, 3, 3, -2. Tepi NULL kini muncul di baris pertama tiap kota — bukan hanya di baris pertama tabel — karena reset LAG mengikuti partisi. Aturan praktisnya mudah: setiap analisis yang membandingkan baris dengan tetangganya harus menyertakan PARTITION BY pada dimensi pembandingnya; tanpa itu, baris dari kelompok lain akan berpura-pura menjadi tetangga.
Membersihkan Database Latihan #
Rangkaian latihan selesai dan database latihan kita jatuhkan kembali:
DROP DATABASE belajar_window;Output yang Dihasilkan:
Query OK, 1 row affected (0.00 sec)Referensi #
- Window Function Descriptions — MySQL 8.4 Reference Manual
- Window Function Concepts and Syntax — MySQL 8.4 Reference Manual
Kini kita bisa menghadirkan nilai kemarin dan besok di samping nilai hari ini, lengkap dengan pertumbuhannya. Pertanyaan berikutnya menumpuk dari pola yang sama: bagaimana menghitung total semua hari sebelumnya sampai hari ini dalam satu kolom berjalan? Itu pekerjaan SUM() OVER (ORDER BY ...) — akumulasi berjalan yang kita bahas di chapter berikutnya.