TL;DR
UNPIVOT di SQL adalah operasi yang ngubah beberapa kolom jadi baris, ngerubah tabel dari format wide ke format long. Gunanya biar data gampang difilter, diagregasi, dan divisualisasi. Ada tiga cara: klausa UNPIVOT (SQL Server/Oracle), UNION ALL (jalan di semua database), dan CROSS JOIN LATERAL VALUES (paling ringkas di PostgreSQL). Di tabel laporan toko_berkah dengan 12 kolom bulan, UNPIVOT mangkas query agregasi dari 47 baris jadi 6 baris.
UNPIVOT di SQL adalah operasi yang ngubah beberapa kolom jadi baris. Tabel yang tadinya lebar (satu kolom per bulan) jadi panjang, dengan kolom bulan dan kolom penjualan.
Kenapa perlu? Coba kamu bikin query "total penjualan Q2" di tabel yang kolomnya jan, feb, mar sampai des.
SELECT produk, apr + mei + jun AS total_q2
FROM laporan_bulanan;
Jalan sih. Sampai bos minta "eh, sekalian bandingin sama Q2 tahun lalu, terus filter cuma bulan yang penjualannya di atas 10 juta."
Sekarang query kamu jadi neraka.
Di dataset toko_berkah, tabel laporannya punya 12 kolom bulan. Setelah aku UNPIVOT, query agregasi yang tadinya 47 baris jadi 6 baris.
Format wide nyimpen satu kategori per kolom. Enak dibaca manusia, susah diolah mesin.
| produk | jan | feb | mar |
|---|---|---|---|
| Beras Premium | 12.400.000 | 11.800.000 | 14.200.000 |
| Minyak Goreng | 8.100.000 | 9.300.000 | 7.900.000 |
Format long nyimpen satu observasi per baris. Susah dibaca manusia, gampang banget diolah mesin.
| produk | bulan | penjualan |
|---|---|---|
| Beras Premium | jan | 12.400.000 |
| Beras Premium | feb | 11.800.000 |
| Beras Premium | mar | 14.200.000 |
| Minyak Goreng | jan | 8.100.000 |
Di format long, kamu bisa WHERE bulan = 'mar'. Di format wide, kamu nggak bisa, karena "mar" itu nama kolom, bukan nilai.
Itu bedanya. Dan itu yang bikin UNPIVOT penting.
Kalau kamu pakai SQL Server atau Oracle, ada klausa bawaan. Ini cara paling bersih.
SELECT produk, bulan, penjualan
FROM laporan_bulanan
UNPIVOT (
penjualan FOR bulan IN (jan, feb, mar, apr, mei, jun,
jul, agu, sep, okt, nov, des)
) AS unpvt;
Cara bacanya:
penjualan = nama kolom baru buat nilainyabulan = nama kolom baru buat nama kolom lamanyaIN (...) = daftar kolom yang mau diubah jadi barisDua syarat yang sering bikin error:
IN (...) harus punya tipe data yang sama. Kalau jan itu INT dan feb itu DECIMAL, cast dulu.PostgreSQL, MySQL, dan SQLite nggak punya klausa UNPIVOT. Pakai UNION ALL. Ini jalan di semua database tanpa kecuali.
SELECT produk, 'jan' AS bulan, jan AS penjualan FROM laporan_bulanan
UNION ALL
SELECT produk, 'feb', feb FROM laporan_bulanan
UNION ALL
SELECT produk, 'mar', mar FROM laporan_bulanan
UNION ALL
SELECT produk, 'apr', apr FROM laporan_bulanan;
Polanya sama tiap blok: nama kolom jadi teks literal, isi kolomnya jadi nilai.
Verbose? Iya, 12 bulan berarti 12 blok. Tapi jalan di mana aja.
Kalau kamu belum familiar sama cara kerja UNION, cek dulu fungsi UNION dan UNION ALL.
Satu kelemahan penting: UNION ALL scan tabelnya sekali per blok. 12 blok = 12 kali full table scan. Di tabel besar, ini kerasa banget.
PostgreSQL punya trik yang jauh lebih pendek dan cuma scan tabel sekali: CROSS JOIN LATERAL (VALUES ...).
SELECT l.produk, v.bulan, v.penjualan
FROM laporan_bulanan l
CROSS JOIN LATERAL (VALUES
('jan', l.jan),
('feb', l.feb),
('mar', l.mar),
('apr', l.apr),
('mei', l.mei),
('jun', l.jun)
) AS v(bulan, penjualan)
WHERE v.penjualan IS NOT NULL;
Cara kerjanya: buat tiap baris di laporan_bulanan, PostgreSQL bikin 6 baris kecil dari daftar VALUES itu, terus ditempel.
Klausa WHERE v.penjualan IS NOT NULL di akhir bikin perilakunya sama kayak klausa UNPIVOT bawaan, baris kosong dibuang.
MySQL 8 juga bisa pakai pola yang mirip lewat JOIN ke subquery VALUES, walaupun sintaksnya sedikit beda.
Kondisi awal: tabel laporan_bulanan dengan 240 baris produk dan 12 kolom bulan. Total 2.880 sel angka yang "kepenjara" di dalam header kolom.
Permintaan dari owner: "Kasih tau bulan mana yang penjualannya di bawah rata-rata, per produk. Terus urutkan dari yang paling parah."
Di format wide, ini butuh 12 perbandingan manual per produk. Aku nyoba, sampai 47 baris query dan masih belum bener.
Di format long, jadi gini:
WITH long_data AS (
SELECT l.produk, v.bulan, v.penjualan
FROM laporan_bulanan l
CROSS JOIN LATERAL (VALUES
('jan', l.jan), ('feb', l.feb), ('mar', l.mar),
('apr', l.apr), ('mei', l.mei), ('jun', l.jun),
('jul', l.jul), ('agu', l.agu), ('sep', l.sep),
('okt', l.okt), ('nov', l.nov), ('des', l.des)
) AS v(bulan, penjualan)
WHERE v.penjualan IS NOT NULL
),
rata_rata AS (
SELECT produk, AVG(penjualan) AS avg_produk
FROM long_data
GROUP BY produk
)
SELECT
d.produk,
d.bulan,
d.penjualan,
ROUND(r.avg_produk, 0) AS rata_rata_produk,
ROUND((d.penjualan - r.avg_produk) / r.avg_produk * 100, 1) AS selisih_persen
FROM long_data d
JOIN rata_rata r ON d.produk = r.produk
WHERE d.penjualan < r.avg_produk
ORDER BY selisih_persen ASC
LIMIT 20;
Hasilnya nunjukin pola yang nggak kelihatan di format wide: Minyak Goreng anjlok 38% di bawah rata-ratanya sendiri di bulan Juli. Bukan cuma satu produk. 6 dari 10 produk kebutuhan pokok kena penurunan besar di Juli.
Setelah dicek, Juli 2025 itu minggu setelah Lebaran. Stok belum masuk lagi.
Insight yang muncul cuma gara-gara datanya dibalik bentuknya.
Buat mainin agregasi kayak gini, kamu bakal butuh GROUP BY dan AVG. Konsep CTE-nya ada di glossary CTE.
Klausa UNPIVOT nolak kalau kolom yang mau digabung tipe datanya campur. Cast semuanya dulu:
SELECT produk,
CAST(jan AS DECIMAL(15,2)) AS jan,
CAST(feb AS DECIMAL(15,2)) AS feb
FROM laporan_bulanan
Kalau produk baru launching di bulan Agustus, sel jan sampai jul-nya NULL. Klausa UNPIVOT buang baris itu. Kadang itu yang kamu mau, kadang enggak. Kalau kamu lagi ngitung berapa bulan produk itu nol penjualan, kamu butuh baris itu tetap ada.
12 blok UNION ALL = 12 kali scan tabel. Di tabel 5 juta baris, query yang harusnya 2 detik bisa jadi 40 detik. Pindah ke CROSS JOIN LATERAL kalau bisa.
Kalau kamu ORDER BY bulan dan bulannya teks, urutannya jadi alfabetis: agu, apr, des, feb... Bikin kolom bantu berisi angka bulan:
CROSS JOIN LATERAL (VALUES
(1, 'jan', l.jan),
(2, 'feb', l.feb),
(3, 'mar', l.mar)
) AS v(no_bulan, bulan, penjualan)
Terus ORDER BY no_bulan.
PIVOT ngubah baris jadi kolom, dari data panjang jadi tabel lebar yang enak dibaca manusia. UNPIVOT kebalikannya, ngubah kolom jadi baris biar data gampang diolah mesin. Patokannya: kalau kamu lagi nyiapin laporan buat dibaca orang, PIVOT. Kalau kamu lagi nyiapin data buat dianalisa atau divisualisasi, UNPIVOT dulu.
SQL Server dan Oracle punya klausa UNPIVOT bawaan. PostgreSQL, MySQL, dan SQLite nggak punya. Buat database yang nggak punya, pakai UNION ALL. Ini jalan di semua database tanpa kecuali. Kalau kamu di PostgreSQL, ada cara yang lebih ringkas pakai CROSS JOIN LATERAL VALUES atau fungsi unnest().
Klausa UNPIVOT bawaan otomatis buang baris yang nilainya NULL. Jadi kalau produk A nggak ada penjualan di bulan Maret dan selnya NULL, baris Maret buat produk A nggak muncul di hasil. Kalau kamu pakai UNION ALL, baris NULL-nya tetap ikut. Tambahin WHERE ... IS NOT NULL kalau mau perilaku yang sama.
Soalnya tiap baris jadi satu observasi yang lengkap: produk, bulan, angka. Kamu bisa langsung GROUP BY bulan, filter WHERE bulan = 'mei', atau lempar ke chart tanpa nyusun ulang. Di format wide, nambah satu bulan baru artinya kamu harus edit semua query yang nyebut nama kolom bulan satu-satu.
Buat tabel kecil sampai sedang, dampaknya kecil. Yang bikin lambat biasanya UNION ALL, soalnya dia scan tabel sekali per kolom. 12 kolom bulan berarti 12 kali full scan. Klausa UNPIVOT dan CROSS JOIN LATERAL cuma scan sekali, jadi lebih cepat. Kalau tabelnya jutaan baris dan sering dipakai, mending simpan hasilnya sebagai view atau tabel terpisah.
Tiga hal yang perlu kamu inget:
Sintaks lengkap klausa UNPIVOT ada di dokumentasi resmi Microsoft T-SQL.
Kalau data kamu masih di Excel atau CSV dan belum masuk database, unpivot bisa dilakuin lebih cepet lewat Power Query. Caranya ada di panduan Power Query di Power BI.
Mau latihan nulis query UNPIVOT sendiri sampai lancar? Coba latihan interaktifnya di NgulikSQL.
Mau praktek langsung? Mulai latihan SQL gratis
Latihan interaktif, langsung di browser.
Analisa per kuartal, hari kerja, atau musim jadi ribet kalau tiap query ngitung ulang atribut tanggal. Tabel kalender nyimpen semua atribut itu sekali, biar tinggal di-JOIN.
Laporan penjualan harian sering bolong di hari tanpa transaksi. Ini cara bikin deret tanggal lengkap di SQL biar tiap hari muncul, walau nilainya nol.
Struktur organisasi tersimpan sebagai kolom id_atasan yang saling nunjuk. Ini cara narik rantai jabatan, hitung total bawahan, dan span of control cuma pakai SQL.