TL;DR
PIVOT di SQL adalah teknik mengubah nilai baris jadi nama kolom, biasanya buat bikin laporan matriks kayak penjualan per kota per bulan. Cara paling portabel adalah CASE WHEN di dalam agregat, dan itu jalan di hampir semua database. SQL Server punya operator PIVOT khusus, sementara PostgreSQL pakai fungsi crosstab dari extension tablefunc.
PIVOT di SQL adalah teknik ngubah nilai baris jadi nama kolom. Data kamu bentuknya panjang ke bawah, laporan yang diminta bentuknya lebar ke samping.
Kasus klasiknya: bos minta tabel penjualan dengan baris per kota dan kolom Jan sampai Des. Data kamu bentuknya satu baris per transaksi.
Ada tiga cara ngerjainnya, dan aku bakal jelasin ketiganya. Tapi satu di antaranya yang paling sering aku pakai, karena jalan di mana aja.
PIVOT adalah operasi yang muter data dari bentuk panjang (banyak baris, sedikit kolom) jadi bentuk lebar (sedikit baris, banyak kolom). Nilai unik dari satu kolom berubah jadi nama-nama kolom baru, dan angka agregatnya ngisi sel-selnya.
Contoh. Data mentahnya kayak gini:
| kota | bulan | omzet |
|---|---|---|
| Jakarta | 1 | 12.000.000 |
| Jakarta | 2 | 14.500.000 |
| Bandung | 1 | 8.200.000 |
| Bandung | 2 | 7.900.000 |
Setelah di-pivot:
| kota | jan | feb |
|---|---|---|
| Jakarta | 12.000.000 | 14.500.000 |
| Bandung | 8.200.000 | 7.900.000 |
Angkanya sama. Bentuknya beda. Dan bentuk kedua yang orang mau lihat di laporan.
Ini cara yang paling aku rekomendasiin. Jalan di semua database utama: PostgreSQL, MySQL, SQL Server, SQLite, BigQuery.
Polanya: SUM(CASE WHEN kondisi THEN nilai ELSE 0 END) buat tiap kolom yang mau kamu bikin.
SELECT
kota,
SUM(CASE WHEN EXTRACT(MONTH FROM tanggal) = 1 THEN total_harga ELSE 0 END) AS jan,
SUM(CASE WHEN EXTRACT(MONTH FROM tanggal) = 2 THEN total_harga ELSE 0 END) AS feb,
SUM(CASE WHEN EXTRACT(MONTH FROM tanggal) = 3 THEN total_harga ELSE 0 END) AS mar,
SUM(total_harga) AS total
FROM toko_berkah
WHERE EXTRACT(YEAR FROM tanggal) = 2026
GROUP BY kota
ORDER BY total DESC;
Cara bacanya per kolom. Kolom jan cuma ngejumlahin baris yang bulannya 1. Baris bulan lain dikasih 0, jadi nggak ikut kejumlah.
GROUP BY yang nentuin baris laporannya. CASE WHEN yang nentuin kolomnya. Dua-duanya kerja bareng.
Kolom total di ujung itu bonus: SUM tanpa kondisi, jadi dia jumlahin semuanya.
Kalau CASE WHEN masih asing buat kamu, ada penjelasan lengkapnya di halaman fungsi CASE WHEN.
Ganti SUM jadi COUNT, dan kamu dapat jumlah transaksi per bulan:
SELECT
kota,
COUNT(CASE WHEN EXTRACT(MONTH FROM tanggal) = 1 THEN 1 END) AS trx_jan,
COUNT(CASE WHEN EXTRACT(MONTH FROM tanggal) = 2 THEN 1 END) AS trx_feb
FROM toko_berkah
GROUP BY kota;
Perhatiin: di COUNT, aku nggak pakai ELSE 0. Soalnya COUNT ngitung nilai yang nggak NULL, jadi tanpa ELSE, baris yang nggak cocok jadi NULL dan nggak ikut kehitung. Kalau kamu tulis ELSE 0, semua baris kehitung dan hasilnya salah.
Ini jebakan yang sering bikin angka melenceng.
SQL Server punya sintaks khusus. Lebih pendek, tapi cuma jalan di SQL Server dan Oracle.
SELECT kota, [1] AS jan, [2] AS feb, [3] AS mar
FROM (
SELECT kota, MONTH(tanggal) AS bln, total_harga
FROM toko_berkah
) AS src
PIVOT (
SUM(total_harga) FOR bln IN ([1], [2], [3])
) AS pvt;
Tiga bagian yang perlu kamu ngerti. Subquery src nyiapin data yang bakal di-pivot. Cuma kolom yang kamu SELECT di sini yang bakal muncul.
Klausa PIVOT nentuin agregatnya (SUM) dan kolom mana yang nilainya jadi nama kolom (bln).
Bagian IN ([1], [2], [3]) nentuin nilai mana yang jadi kolom. Kamu harus sebut satu-satu. Ini kelemahannya.
Jebakan yang sering bikin bingung: kolom yang nggak kamu sebut di subquery tapi ada di tabel asli bakal ikut jadi grup diam-diam. Kalau hasilnya lebih banyak baris dari yang kamu harapin, cek subquery-nya. Detailnya di dokumentasi PIVOT SQL Server.
PostgreSQL nggak punya operator PIVOT. Dia punya crosstab, tapi itu ada di extension tablefunc yang harus di-install dulu:
CREATE EXTENSION IF NOT EXISTS tablefunc;
SELECT *
FROM crosstab(
'SELECT kota, EXTRACT(MONTH FROM tanggal)::int, SUM(total_harga)
FROM toko_berkah
GROUP BY 1, 2
ORDER BY 1, 2',
'SELECT generate_series(1, 3)'
) AS ct(kota text, jan numeric, feb numeric, mar numeric);
Jujur aja: aku jarang pakai ini. Sintaksnya ribet, query-nya ditulis sebagai string (jadi nggak dicek editor), dan kamu tetap harus sebut nama kolomnya di bagian AS ct(...).
Buat kebanyakan kasus di PostgreSQL, CASE WHEN lebih gampang dibaca dan di-maintain.
Ini query yang beneran dipakai buat laporan bulanan toko_berkah. Baris per kota, kolom per kuartal, plus persentase kontribusi.
WITH pivot_kuartal AS (
SELECT
kota,
SUM(CASE WHEN EXTRACT(QUARTER FROM tanggal) = 1 THEN total_harga ELSE 0 END) AS q1,
SUM(CASE WHEN EXTRACT(QUARTER FROM tanggal) = 2 THEN total_harga ELSE 0 END) AS q2,
SUM(CASE WHEN EXTRACT(QUARTER FROM tanggal) = 3 THEN total_harga ELSE 0 END) AS q3,
SUM(CASE WHEN EXTRACT(QUARTER FROM tanggal) = 4 THEN total_harga ELSE 0 END) AS q4,
SUM(total_harga) AS total
FROM toko_berkah
WHERE EXTRACT(YEAR FROM tanggal) = 2025
GROUP BY kota
)
SELECT
kota,
q1, q2, q3, q4,
total,
ROUND(total * 100.0 / SUM(total) OVER (), 1) AS kontribusi_persen,
ROUND((q4 - q1) * 100.0 / NULLIF(q1, 0), 1) AS growth_q1_ke_q4
FROM pivot_kuartal
ORDER BY total DESC;
Kolom terakhir yang paling menarik. Dia ngebandingin Q4 sama Q1, jadi kamu langsung lihat kota mana yang tumbuh dan mana yang nyusut sepanjang tahun.
NULLIF(q1, 0) di penyebutnya buat ngehindarin error kalau ada kota yang Q1-nya nol.
Hasil dari dataset ngulikdata: dari 12 kota, 4 kota punya growth Q1 ke Q4 negatif. Yang paling parah Solo, turun 31%. Tapi total omzet Solo setahun masih peringkat 6 dari 12, jadi kalau cuma lihat kolom total, dia kelihatan baik-baik aja.
Bentuk pivot yang bikin masalahnya kelihatan. Kolom total nyembunyiin trennya.
Kadang kamu dapat file Excel dari user yang formatnya udah lebar, dan kamu perlu balikin ke bentuk baris biar bisa dianalisis.
Cara paling portabel: UNION ALL.
SELECT kota, 'jan' AS bulan, jan AS omzet FROM laporan_lebar
UNION ALL
SELECT kota, 'feb', feb FROM laporan_lebar
UNION ALL
SELECT kota, 'mar', mar FROM laporan_lebar;
Panjang, tapi jelas dan jalan di mana aja.
Di PostgreSQL ada cara yang lebih ringkas:
SELECT
kota,
unnest(ARRAY['jan', 'feb', 'mar']) AS bulan,
unnest(ARRAY[jan, feb, mar]) AS omzet
FROM laporan_lebar;
1. Pakai ELSE 0 di dalam COUNT. Ini bikin semua baris kehitung, bukan cuma yang cocok kondisinya. Buat COUNT, biarin ELSE-nya kosong (jadi NULL). Buat SUM, ELSE 0 aman.
2. Berharap kolomnya dinamis. SQL harus tau nama kolomnya waktu query di-compile. Kamu nggak bisa bilang "buatin kolom otomatis dari nilai yang ada". Kalau butuh dinamis, pivot di layer visualisasi (Power BI, Looker Studio) atau pakai dynamic SQL. Dynamic SQL itu ribet dan rawan SQL injection.
3. Nggak filter periode dulu. Kalau tabelnya 5 tahun data dan kamu cuma butuh 2026, taruh WHERE-nya. Kalau nggak, database ngitung CASE WHEN buat semua baris yang bakal jadi 0 juga.
4. Bikin 30+ kolom pivot dalam satu query. Secara teknis bisa. Tapi query-nya jadi 200 baris, susah dibaca, dan lambat. Kalau kamu butuh 30 kolom, pertanyaannya: laporan ini beneran dibaca orang, atau cuma dump data?
5. Pivot padahal seharusnya pakai window function. Kalau yang kamu mau itu bandingin baris sama grupnya (misalnya kontribusi tiap transaksi ke total kota), PARTITION BY lebih tepat dan nggak bikin bentuk tabelnya berubah.
CASE WHEN di dalam fungsi agregat. Tulisannya: SUM(CASE WHEN bulan = 1 THEN total_harga ELSE 0 END) AS jan. Cara ini jalan di PostgreSQL, MySQL, SQL Server, SQLite, BigQuery, dan hampir semua database lain. Lebih panjang dari operator PIVOT khusus, tapi kamu nggak perlu hafal sintaks yang beda-beda tiap database.
Nggak bisa langsung. SQL butuh tau nama kolomnya waktu query di-compile. Solusinya ada dua: bikin dynamic SQL (query yang menghasilkan string query lain lalu dijalankan), atau pivot di layer visualisasi kayak Power BI atau Looker Studio yang emang dirancang buat itu (biasanya cara ini lebih baik).
GROUP BY meringkas data ke bawah, hasilnya satu baris per grup. PIVOT nyebar hasil agregat itu ke samping jadi kolom. Biasanya dua-duanya dipakai bareng: GROUP BY nentuin baris laporannya (per kota), CASE WHEN di dalam agregat nentuin kolomnya (per bulan). Jadi PIVOT bukan pengganti GROUP BY, tapi lapisan tambahan di atasnya.
Cara paling portabel pakai UNION ALL: satu SELECT per kolom yang mau diubah jadi baris. PostgreSQL punya cara lebih ringkas pakai UNNEST, SQL Server punya operator UNPIVOT. Ini sering dibutuhin waktu kamu terima file Excel dari user yang formatnya udah lebar, dan kamu mau masukin ke database dalam bentuk baris yang rapi.
Teknik CASE WHEN nggak jauh lebih lambat dari GROUP BY biasa, soalnya database cuma jalan sekali lewat data. Yang bikin lambat itu kalau kamu bikin puluhan kolom CASE WHEN sekaligus, atau kalau tabelnya nggak di-filter dulu. Taruh WHERE buat batasi periode sebelum agregasi, dan jangan bikin lebih dari 20 kolom hasil pivot dalam satu query.
Tiga cara bikin PIVOT: CASE WHEN (jalan di mana aja, ini yang aku pakai), operator PIVOT (SQL Server dan Oracle doang), dan crosstab (PostgreSQL, tapi ribet).
Pola CASE WHEN yang perlu kamu hafal: SUM(CASE WHEN kondisi THEN nilai ELSE 0 END) AS nama_kolom. Ganti SUM jadi COUNT? Buang ELSE-nya.
Dan inget: kolom pivot nggak bisa dinamis di SQL murni. Kalau butuh dinamis, itu kerjaan tool visualisasi.
Mau langsung latihan? Coba latihan CASE WHEN di NgulikSQL pakai dataset toko_berkah, terus bikin laporan kuartalan versi kamu sendiri.
Habis itu lanjut ke window function SQL, buat kasus yang kelihatan kayak butuh pivot, padahal sebenernya butuh PARTITION BY.
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.