GROUP BY, HAVING & FILTER: Laporan per Kelompok
Mengelompokkan baris dan menghitung ringkasan per kelompok dengan GROUP BY, menyaring kelompok dengan HAVING, serta agregat bersyarat dengan FILTER khas PostgreSQL.
Analogi: GROUP BY itu seperti memilah cucian ke keranjang berbeda (putih, warna, gelap), baru kemudian menghitung isi tiap keranjang. WHERE menyaring baju sebelum dimasukkan keranjang, HAVING menyaring keranjang setelah terisi ("hanya tampilkan keranjang yang beratnya di atas 3 kg").
GROUP BY: satu ringkasan per kelompok
Agregat tanpa GROUP BY meringkas seluruh tabel menjadi satu angka. Dengan GROUP BY, tabel dibagi menjadi kelompok, lalu setiap kelompok diringkas sendiri-sendiri.
SELECT kota, COUNT(*) AS jumlah
FROM pelanggan
GROUP BY kota;
kota | jumlah
------------+--------
Jakarta | 3
Bandung | 2
Surabaya | 1
...
Analogi: bayangkan kartu pelanggan ditaruh ke dalam kotak per kota. GROUP BY = memilah kartu ke dalam kotak. COUNT = menghitung isi tiap kotak.
Aturan emas GROUP BY
Setiap kolom di SELECT harus salah satu dari dua: (1) ada di GROUP BY, atau (2) dibungkus fungsi agregat. Kalau tidak, PostgreSQL menolak dengan error column "..." must appear in the GROUP BY clause or be used in an aggregate function.
SELECT kategori_id, COUNT(*) AS jumlah_buku, SUM(stok) AS total_stok
FROM buku
GROUP BY kategori_id
ORDER BY kategori_id;
HAVING: menyaring kelompok
WHERE menyaring baris sebelum dikelompokkan. HAVING menyaring kelompok setelah dihitung:
SELECT kategori_id, COUNT(*) AS jumlah_buku
FROM buku
GROUP BY kategori_id
HAVING COUNT(*) > 1; -- hanya kategori yang punya lebih dari 1 buku
| WHERE | HAVING | |
|---|---|---|
| Dikerjakan | sebelum GROUP BY | sesudah GROUP BY |
| Menyaring | baris | kelompok |
| Boleh pakai agregat? | tidak | ya |
Urutan lengkap penulisan: SELECT ... FROM ... WHERE ... GROUP BY ... HAVING ... ORDER BY ... LIMIT.
Mengelompokkan lebih dari satu kolom
SELECT kota, status_kirim, COUNT(*) FROM ... GROUP BY kota, status_kirim;
Mengelompokkan berdasarkan hasil fungsi
Gabungkan dengan DATE_TRUNC untuk laporan bulanan:
SELECT DATE_TRUNC('month', tanggal) AS bulan, COUNT(*) AS jumlah_pesanan
FROM pesanan
GROUP BY DATE_TRUNC('month', tanggal)
ORDER BY bulan;
FILTER: agregat bersyarat (khas PostgreSQL)
Ingin menghitung beberapa kategori sekaligus dalam satu baris? Gunakan FILTER (WHERE ...) pada agregat:
SELECT COUNT(*) FILTER (WHERE status = 'lunas') AS lunas,
COUNT(*) FILTER (WHERE status = 'menunggu') AS menunggu,
COUNT(*) FILTER (WHERE status = 'batal') AS batal
FROM pesanan;
Lebih rapi daripada SUM(CASE WHEN ... THEN 1 ELSE 0 END) dan sering dipakai untuk dashboard.
Kasus nyata: kategori buku mana yang paling laris dan mana yang stoknya menumpuk
SELECT k.nama AS kategori,
COUNT(*) AS jumlah_judul,
SUM(b.stok) AS total_stok
FROM buku b
JOIN kategori k ON k.id = b.kategori_id
GROUP BY k.nama
HAVING SUM(b.stok) > 50
ORDER BY total_stok DESC;Laporan seperti ini membantu tim pembelian memutuskan kategori mana yang perlu dipromosikan agar stoknya berputar.
Kesalahan yang sering terjadi
- Menaruh kolom di SELECT yang tidak ada di
GROUP BYdan bukan agregat. PostgreSQL akan menolaknya. - Memakai
HAVINGuntuk syarat baris biasa. Lebih efisien memakaiWHEREbila memungkinkan. - Lupa bahwa
GROUP BYmemperlakukan NULL sebagai satu kelompok sendiri.