FIRST_VALUE dan LAST_VALUE SQL: Ambil Nilai Awal & Akhir Grup
TL;DR
FIRST_VALUE dan LAST_VALUE adalah window function SQL yang ngambil nilai dari baris pertama dan baris terakhir dalam satu grup yang udah diurutin. FIRST_VALUE jalan normal begitu kamu tulis. LAST_VALUE butuh tambahan ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING, soalnya window frame default-nya cuma sampai baris yang lagi diproses, bukan sampai akhir grup.
FIRST_VALUE dan LAST_VALUE adalah window function SQL yang ngambil nilai dari baris pertama dan baris terakhir dalam satu grup yang udah diurutin.
FIRST_VALUE jalan mulus begitu kamu tulis. LAST_VALUE punya jebakan, dan hampir semua orang kena sekali.
Kalau LAST_VALUE kamu ngasih hasil yang persis sama dengan kolom yang lagi dibaca, kamu lagi kena jebakan itu. Bukan bug. Ini soal window frame default, dan aku jelasin lengkap di bawah.
Apa itu FIRST_VALUE dan LAST_VALUE?
FIRST_VALUE(kolom) ngasih nilai kolom itu dari baris paling awal dalam window. LAST_VALUE(kolom) ngasih nilai dari baris paling akhir. Yang nentuin siapa "awal" dan siapa "akhir" itu ORDER BY di dalam OVER.
Bedanya sama MIN dan MAX: MIN nyari nilai terkecil, FIRST_VALUE nyari nilai di posisi paling awal. Kalau kamu urutin berdasarkan tanggal, FIRST_VALUE(harga) ngasih harga di tanggal paling awal, bukan harga termurah.
Beda banget, dan salah pilih di antara keduanya bikin angka laporan kamu keliru tanpa satu pun error message.
Gimana sintaksnya?
FIRST_VALUE(kolom) OVER (
[PARTITION BY kolom_grup]
ORDER BY kolom_urutan
[window_frame]
)
Contoh nyata: ambil harga pertama tiap produk dari tabel riwayat harga.
SELECT
produk_id,
tanggal,
harga,
FIRST_VALUE(harga) OVER (
PARTITION BY produk_id
ORDER BY tanggal
) AS harga_awal
FROM riwayat_harga
ORDER BY produk_id, tanggal;
Tiap baris sekarang punya kolom harga_awal yang isinya harga di tanggal paling awal untuk produk itu. Nilainya sama di semua baris dalam satu produk.
Kenapa LAST_VALUE hasilnya salah?
Coba tulis LAST_VALUE dengan pola yang sama:
LAST_VALUE(harga) OVER (
PARTITION BY produk_id
ORDER BY tanggal
) AS harga_akhir
Hasilnya bakal persis sama dengan kolom harga di tiap baris. Nggak berguna sama sekali.
Penyebabnya: begitu kamu pakai ORDER BY di dalam OVER, SQL diam-diam nambahin window frame default:
RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
Artinya, buat tiap baris, jendela yang dia lihat cuma dari awal grup sampai baris yang lagi diproses. Bukan sampai akhir grup.
Jadi "nilai terakhir" yang dia lihat ya baris itu sendiri. Konsisten, logis, dan bikin frustrasi.
Gimana cara benerin LAST_VALUE?
Tulis frame-nya secara eksplisit sampai akhir grup:
SELECT
produk_id,
tanggal,
harga,
LAST_VALUE(harga) OVER (
PARTITION BY produk_id
ORDER BY tanggal
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) AS harga_akhir
FROM riwayat_harga;
UNBOUNDED FOLLOWING artinya "sampai baris terakhir dalam grup". Sekarang hasilnya bener.
Ada cara yang lebih pendek dan lebih aman: pakai FIRST_VALUE dengan urutan dibalik.
FIRST_VALUE(harga) OVER (
PARTITION BY produk_id
ORDER BY tanggal DESC
) AS harga_akhir
Hasilnya sama, tulisannya lebih pendek, dan nggak ada jebakan frame yang perlu kamu inget. Beberapa analis yang aku kenal cuma pakai FIRST_VALUE dan nggak pernah nyentuh LAST_VALUE seumur hidup.
Contoh kasus: perubahan harga 1.842 produk toko_berkah
Dataset toko_berkah punya tabel riwayat_harga dengan 24.106 baris perubahan harga untuk 1.842 produk, sepanjang 2023 sampai 2025.
Pertanyaan dari tim pricing: produk mana yang harganya paling banyak berubah, dan berapa selisih harga awal sama harga akhirnya?
WITH harga_ujung AS (
SELECT DISTINCT
produk_id,
FIRST_VALUE(harga) OVER (
PARTITION BY produk_id ORDER BY tanggal
) AS harga_awal,
FIRST_VALUE(harga) OVER (
PARTITION BY produk_id ORDER BY tanggal DESC
) AS harga_akhir,
COUNT(*) OVER (PARTITION BY produk_id) AS jumlah_perubahan
FROM riwayat_harga
)
SELECT
p.nama_produk,
h.harga_awal,
h.harga_akhir,
h.harga_akhir - h.harga_awal AS selisih,
ROUND(
(h.harga_akhir - h.harga_awal) * 100.0 / h.harga_awal, 1
) AS perubahan_persen,
h.jumlah_perubahan
FROM harga_ujung h
JOIN produk p ON p.produk_id = h.produk_id
ORDER BY perubahan_persen DESC
LIMIT 10;
Perhatiin aku pakai FIRST_VALUE dua kali dengan arah ORDER BY yang beda, bukan LAST_VALUE. Lebih aman, dan aku nggak perlu nulis frame panjang dua kali.
Hasilnya: 3 produk naik lebih dari 60% dalam 3 tahun, semuanya kategori minyak goreng. Yang paling ekstrem naik 71,4%, dari Rp 14.000 ke Rp 24.000, dengan 19 kali perubahan harga.
Yang bikin tim pricing kaget: ada 47 produk yang harga akhirnya lebih rendah dari harga awal, padahal biaya bahan bakunya naik. Ternyata itu produk yang harganya diturunin buat lawan kompetitor dan nggak pernah dinaikin lagi. Total margin yang hilang: sekitar Rp 190 juta setahun.
Angka itu nggak bakal keliatan kalau kamu cuma lihat harga terakhir. Butuh harga awal dan harga akhir nempel di baris yang sama.
Kesalahan umum waktu pakai FIRST_VALUE dan LAST_VALUE
Nulis LAST_VALUE tanpa window frame. Hasilnya selalu sama dengan baris yang lagi diproses. Ini kesalahan nomor satu, dan nggak ada error message yang ngasih tau.
Ngira FIRST_VALUE sama dengan MIN. FIRST_VALUE ngikutin urutan, MIN ngikutin nilai. Kalau kamu ORDER BY tanggal, FIRST_VALUE(harga) ngasih harga tertua, bukan harga termurah.
Lupa PARTITION BY. Tanpa PARTITION BY, seluruh tabel jadi satu grup gede, dan semua baris dapat nilai awal yang sama.
Pakai RANGE padahal maksudnya ROWS. Kalau ada beberapa baris dengan tanggal yang persis sama, RANGE bakal nganggap mereka satu kesatuan dan hasilnya bisa beda dari yang kamu kira. ROWS ngitung per baris. Kalau ragu, pakai ROWS.
Nggak pakai DISTINCT waktu cuma butuh satu baris per grup. Window function nggak ngurangin jumlah baris. Kalau produk punya 19 baris riwayat harga, kamu tetap dapat 19 baris. Bungkus pakai DISTINCT atau GROUP BY di query luar. Detail perilaku frame-nya bisa kamu cek di dokumentasi window function PostgreSQL.
FAQ
Kenapa LAST_VALUE hasilnya sama terus dengan baris yang lagi diproses?
Gara-gara window frame default RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW. Jendela pandangnya cuma sampai baris yang lagi dihitung. Tambahin ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING secara eksplisit.
Bedanya FIRST_VALUE sama MIN apa?
MIN ngasih nilai terkecil, FIRST_VALUE ngasih nilai di baris paling awal menurut urutan yang kamu tentuin. Kalau kamu urutin per tanggal, FIRST_VALUE(harga) ngasih harga tertua, walaupun bukan yang termurah.
FIRST_VALUE bisa dipakai buat ambil baris teratas tiap grup nggak?
Buat satu kolom, bisa. Buat seluruh baris teratas, ROW_NUMBER lebih cocok: filter yang hasilnya sama dengan 1 di query luar.
Database mana aja yang dukung FIRST_VALUE dan LAST_VALUE?
PostgreSQL, MySQL 8.0 ke atas, SQL Server 2012 ke atas, Oracle, BigQuery, Snowflake, dan SQLite 3.25 ke atas. MySQL 5.7 belum punya window function sama sekali.
Ada cara lain selain nulis window frame panjang itu?
Ada: pakai FIRST_VALUE dengan ORDER BY dibalik jadi DESC. Hasilnya sama dengan LAST_VALUE yang ditulis bener, tapi lebih pendek dan nggak ada jebakan frame.
Yang perlu kamu inget
FIRST_VALUE aman ditulis polos. LAST_VALUE butuh window frame eksplisit, atau hasilnya salah tanpa peringatan.
Cara paling gampang: pakai FIRST_VALUE dua kali, satu ASC satu DESC. Lupain LAST_VALUE.
FIRST_VALUE ngikutin urutan, MIN ngikutin nilai. Jangan ketuker.
Coba jalanin query harga awal vs harga akhir di data kamu sendiri, dan cek berapa produk yang harganya turun padahal biayanya naik. Angkanya sering bikin kaget. Lanjut belajar window function lain di NTILE buat bagi data jadi kuartil, atau baca dasar window function dan LAG dulu.
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.