GROUP BY mengubah belasan baris menjadi ringkasan per kelompok, tetapi laporan nyata hampir selalu menuntut satu langkah lagi: menyaring ringkasan itu sendiri. Manajer tidak ingin melihat seluruh kota — hanya kota dengan transaksi mencapai target. Pelaporan tidak memuat seluruh kategori — hanya kategori dengan nilai penjualan di atas ambang tertentu. WHERE tidak bisa membantu di sini karena WHERE bekerja pada baris mentah, bukan pada hasil agregat; itulah pekerjaan klausa HAVING. Chapter ini membedah kapan WHERE dan HAVING masing-masing berlaku, bagaimana keduanya bekerja sama dalam satu query, dan jebakan sintaks yang paling sering menjatuhkan pemula.
Tabel Latihan #
Kita memakai tabel penjualan Toko Maju Jaya berisi dua belas transaksi sepanjang Januari–Februari 2026, tiga kategori produk, tiga kota, dan kolom metode_bayar yang sengaja memuat dua baris NULL (transaksi yang belum dikonfirmasi cara bayarnya):
-- tabel latihan: penjualan Toko Maju Jaya, Januari-Februari 2026
CREATE TABLE penjualan (
id integer PRIMARY KEY,
tanggal date NOT NULL,
kategori text NOT NULL,
produk text NOT NULL,
kota text NOT NULL,
jumlah integer NOT NULL,
harga_satuan numeric(12,2) NOT NULL,
metode_bayar text
);
INSERT INTO penjualan (id, tanggal, kategori, produk, kota, jumlah, harga_satuan, metode_bayar) VALUES
( 1, DATE '2026-01-05', 'Minuman', 'Kopi Gayo', 'Jakarta', 10, 85000.00, 'tunai'),
( 2, DATE '2026-01-08', 'Makanan', 'Biskuit', 'Jakarta', 20, 25000.00, 'transfer'),
( 3, DATE '2026-01-12', 'Alat Tulis', 'Pulpen', 'Bandung', 15, 8000.00, 'qris'),
( 4, DATE '2026-01-15', 'Minuman', 'Kopi Gayo', 'Bandung', 8, 85000.00, 'transfer'),
( 5, DATE '2026-01-20', 'Makanan', 'Biskuit', 'Surabaya', 12, 25000.00, NULL),
( 6, DATE '2026-01-25', 'Alat Tulis', 'Pulpen', 'Jakarta', 30, 8000.00, 'tunai'),
( 7, DATE '2026-02-02', 'Minuman', 'Kopi Gayo', 'Surabaya', 12, 85000.00, 'qris'),
( 8, DATE '2026-02-06', 'Makanan', 'Biskuit', 'Bandung', 25, 25000.00, 'transfer'),
( 9, DATE '2026-02-10', 'Alat Tulis', 'Pulpen', 'Surabaya', 6, 8000.00, NULL),
(10, DATE '2026-02-14', 'Minuman', 'Kopi Gayo', 'Jakarta', 5, 85000.00, 'qris'),
(11, DATE '2026-02-18', 'Makanan', 'Biskuit', 'Jakarta', 18, 25000.00, 'tunai'),
(12, DATE '2026-02-24', 'Alat Tulis', 'Pulpen', 'Bandung', 9, 8000.00, 'qris');
SELECT id, tanggal, kategori, produk, kota, jumlah, harga_satuan, metode_bayar
FROM penjualan
ORDER BY id;Output yang Dihasilkan:
CREATE TABLE
INSERT 0 12
id | tanggal | kategori | produk | kota | jumlah | harga_satuan | metode_bayar
----+------------+------------+-----------+----------+--------+--------------+--------------
1 | 2026-01-05 | Minuman | Kopi Gayo | Jakarta | 10 | 85000.00 | tunai
2 | 2026-01-08 | Makanan | Biskuit | Jakarta | 20 | 25000.00 | transfer
3 | 2026-01-12 | Alat Tulis | Pulpen | Bandung | 15 | 8000.00 | qris
4 | 2026-01-15 | Minuman | Kopi Gayo | Bandung | 8 | 85000.00 | transfer
5 | 2026-01-20 | Makanan | Biskuit | Surabaya | 12 | 25000.00 |
6 | 2026-01-25 | Alat Tulis | Pulpen | Jakarta | 30 | 8000.00 | tunai
7 | 2026-02-02 | Minuman | Kopi Gayo | Surabaya | 12 | 85000.00 | qris
8 | 2026-02-06 | Makanan | Biskuit | Bandung | 25 | 25000.00 | transfer
9 | 2026-02-10 | Alat Tulis | Pulpen | Surabaya | 6 | 8000.00 |
10 | 2026-02-14 | Minuman | Kopi Gayo | Jakarta | 5 | 85000.00 | qris
11 | 2026-02-18 | Makanan | Biskuit | Jakarta | 18 | 25000.00 | tunai
12 | 2026-02-24 | Alat Tulis | Pulpen | Bandung | 9 | 8000.00 | qris
(12 rows)Kalau Anda mengikuti section ini dari awal, tabel ini adalah kelanjutan pola latihan chapter GROUP BY ; strukturnya sengaja dibuat padat supaya setiap kelompok punya karakter berbeda — Jakarta dengan lima transaksi, Surabaya hanya tiga.
Inti Masalahnya: Menyaring Sebelum vs Sesudah Pengelompokan #
Aturan mainnya sudah kita cicipi lewat tur kilat GROUP BY di chapter Fungsi Agregat
; kini kita bedah tuntas. WHERE menyaring baris sebelum dikelompokkan, HAVING menyaring kelompok sesudah agregat dihitung. Konsekuensinya besar. WHERE tidak pernah melihat hasil count(*) atau sum(...) karena pada saat WHERE dievaluasi, kelompok belum terbentuk. Sebaliknya, HAVING justru dibuat untuk membandingkan hasil agregat dengan sebuah ambang.
| Aspek | WHERE | HAVING |
|---|---|---|
| Menyaring | baris mentah | kelompok hasil GROUP BY |
| Boleh memuat agregat? | tidak — error | ya, itu memang fungsinya |
| Boleh kolom biasa? | ya | ya, asalkan kolom grup |
| Dipakai untuk | kriteria baris (“transaksi Februari”) | kriteria ringkasan (“nilai di atas 1 juta”) |
Alur pemrosesan satu query SELECT di PostgreSQL dapat dipetakan seperti ini:
Diagram di atas menegaskan urutan yang tidak bisa ditukar: saringan baris selalu mendahului pembentukan kelompok, dan saringan kelompok baru berlaku setelah angka ringkasan tersedia. Sekarang kita buktikan dua sisi aturan itu dengan data nyata.
HAVING dengan Ekspresi Agregat #
Bentuk paling umum HAVING membandingkan agregat dengan sebuah nilai ambang. Tiga query berikut bertingkat dari count ke sum lalu avg:
-- HAVING dengan ekspresi agregat: saring kelompok berdasarkan hasil ringkasan
-- kelompok kota dengan minimal 4 transaksi
SELECT kota,
count(*) AS transaksi,
sum(jumlah) AS total_item
FROM penjualan
GROUP BY kota
HAVING count(*) >= 4
ORDER BY kota;
-- kelompok kategori dengan nilai penjualan di atas 1.000.000
SELECT kategori,
sum(jumlah * harga_satuan) AS total_nilai
FROM penjualan
GROUP BY kategori
HAVING sum(jumlah * harga_satuan) > 1000000
ORDER BY kategori;
-- kelompok kota dengan rata-rata item per transaksi minimal 15
SELECT kota,
round(avg(jumlah), 2) AS rata_item
FROM penjualan
GROUP BY kota
HAVING avg(jumlah) >= 15
ORDER BY kota;Output yang Dihasilkan:
kota | transaksi | total_item
---------+-----------+------------
Bandung | 4 | 57
Jakarta | 5 | 83
(2 rows)
kategori | total_nilai
----------+-------------
Makanan | 1875000.00
Minuman | 2975000.00
(2 rows)
kota | rata_item
---------+-----------
Jakarta | 16.60
(1 row)Mari kita bedah hasilnya:
- Query pertama: tiga kelompok kota terbentuk (Jakarta 5, Bandung 4, Surabaya 3 transaksi), lalu
HAVING count(*) >= 4hanya meloloskan Jakarta dan Bandung. Surabaya hilang dari hasil — bukan karena barisnya disaring, melainkan karena ringkasannya tidak memenuhi ambang. - Query kedua: ambang nilai memakai ekspresi lengkap
sum(jumlah * harga_satuan)— persis ekspresi yang ada di daftar SELECT. Makanan (1.875.000) dan Minuman (2.975.000) lolos; Alat Tulis (480.000) tersaring. - Query ketiga: agregat boleh dibungkus
round()di SELECT untuk tampilan, tetapi kondisiHAVINGmenulisavg(jumlah)apa adanya. Hanya Jakarta yang rata-rata itemnya (16,60) menyentuh angka 15.
Perhatikan satu detail sintaks: HAVING menulis ulang ekspresi agregat, bukan alias total_item. PostgreSQL memang tidak mengizinkan alias kolom output dipakai di HAVING — kebalikan dari kebiasaan MySQL. Alias rata_item di query ketiga aman karena HAVING memakai avg(jumlah) asli.
WHERE dan HAVING Bekerja Bersama #
Kedua klausa tidak saling menggantikan — query laporan yang serius memakai keduanya sekaligus. Tiga query berikut memperlihatkan pergantian posisi saringan yang sama-sama sah tetapi menjawab pertanyaan berbeda:
-- perbedaan alur: WHERE menyaring baris sebelum dikelompokkan,
-- HAVING menyaring kelompok sesudah agregat dihitung
-- (a) WHERE dulu: transaksi dengan jumlah >= 15, lalu dikelompokkan per kota
SELECT kota,
count(*) AS transaksi
FROM penjualan
WHERE jumlah >= 15
GROUP BY kota
ORDER BY kota;
-- (b) HAVING saja: kelompok kota dengan minimal 4 transaksi
SELECT kota,
count(*) AS transaksi
FROM penjualan
GROUP BY kota
HAVING count(*) >= 4
ORDER BY kota;
-- (c) keduanya sekaligus: hanya Februari, lalu kategori dengan nilai >= 1.200.000
SELECT kategori,
count(*) AS transaksi,
sum(jumlah * harga_satuan) AS total_nilai
FROM penjualan
WHERE tanggal >= DATE '2026-02-01'
GROUP BY kategori
HAVING sum(jumlah * harga_satuan) >= 1200000
ORDER BY kategori;Output yang Dihasilkan:
kota | transaksi
---------+-----------
Bandung | 2
Jakarta | 3
(2 rows)
kota | transaksi
---------+-----------
Bandung | 4
Jakarta | 5
(2 rows)
kategori | transaksi | total_nilai
----------+-----------+-------------
Minuman | 2 | 1445000.00
(1 row)Query (a) dan (b) memakai ambang berbeda dan hasilnya berbeda pula, tetapi pola keduanya adalah pelajaran intinya:
- Query (a):
WHERE jumlah >= 15membuang tujuh baris kecil sebelum pengelompokan, menyisakan lima transaksi besar. Surabaya lenyap sama sekali — kedua transaksinya (12 dan 6 item) tersaring di tahap baris, sehingga kelompok Surabaya tidak pernah terbentuk. Inilah wajah “kelompok yang mati muda”. - Query (b): semua dua belas baris ikut dikelompokkan, baru tiga kelompok kota disaring berdasarkan jumlah transaksinya. Surabaya kembali hilang — kali ini karena ringkasannya, bukan barisnya.
- Query (c): alur lengkapnya —
WHEREmemangkas data ke enam transaksi Februari,GROUP BY kategorimembentuk tiga kelompok, laluHAVING sum(...) >= 1200000hanya meloloskan Minuman (1.445.000). Makanan (1.075.000) dan Alat Tulis (120.000) tersaring setelah angkanya dihitung.
HAVING tanpa GROUP BY #
Query agregat tanpa GROUP BY memperlakukan seluruh tabel sebagai satu kelompok raksasa. HAVING tetap boleh menyandinginya — dan menjadi saklar yang memutuskan baris ringkasan tunggal itu tampil atau tidak:
-- HAVING tanpa GROUP BY: seluruh tabel menjadi satu kelompok
-- kondisi terpenuhi (12 > 10): baris ringkasan tunggal tetap tampil
SELECT count(*) AS transaksi,
sum(jumlah * harga_satuan) AS total_nilai
FROM penjualan
HAVING count(*) > 10;
-- kondisi gagal (12 > 100): hasil kosong, bukan angka nol
SELECT count(*) AS transaksi,
sum(jumlah * harga_satuan) AS total_nilai
FROM penjualan
HAVING count(*) > 100;Output yang Dihasilkan:
transaksi | total_nilai
-----------+-------------
12 | 5330000.00
(1 row)
transaksi | total_nilai
-----------+-------------
(0 rows)Query pertama meloloskan kelompok tunggal itu karena count(*) menghasilkan 12, sehingga ringkasan lengkap tampil. Query kedua memakai ambang yang mustahil dipenuhi — hasilnya nol baris, bukan satu baris bernilai nol. Pola ini berguna sebagai “pagar” laporan: misalnya query dasbor yang memilih menampilkan apa pun hanya ketika data hari itu sudah cukup banyak terkumpul, dan memilih diam ketika belum.
WHERE Tidak Bisa Memuat Agregat #
Cara tercepat memahami batas WHERE adalah melanggarnya. Query berikut mencoba menyaring kategori yang memiliki lebih dari satu transaksi langsung di WHERE — dan PostgreSQL menolaknya:
-- WHERE tidak boleh memuat fungsi agregat: error yang diharapkan
SELECT kategori,
count(*) AS transaksi
FROM penjualan
WHERE count(*) > 1
GROUP BY kategori
ORDER BY kategori;Output yang Dihasilkan:
psql:where-agregat-error.sql:7: ERROR: aggregate functions are not allowed in WHERE
LINE 4: WHERE count(*) > 1
^Pesan aggregate functions are not allowed in WHERE bukan sekadar aturan sintaks sebarangan: saat WHERE dievaluasi, count(*) belum punya makna karena kelompok belum terbentuk. Perbaikannya persis pola chapter ini — pindahkan kondisi ke HAVING count(*) > 1 setelah GROUP BY kategori, dan query berjalan seperti query-query sebelumnya.
Dengan HAVING, siklus dasar agregasi Anda lengkap: kelompokkan dengan GROUP BY, hitung dengan agregat, saring ringkasannya dengan HAVING. Namun ada satu keluhan yang belum terjawab — menghitung beberapa jenis subtotal sekaligus dalam satu query tanpa menulis UNION ALL. Chapter berikutnya, FILTER Clause
, menyelesaikan masalah itu dari sisi lain: bukan menyaring kelompok, melainkan menyeleksi baris mana yang ikut serta ke dalam setiap agregat.
Referensi #
- Tutorial: Aggregate Functions — bagian HAVING pada tutorial resmi.
- SELECT (SQL Reference) — sintaks lengkap klausa WHERE, GROUP BY, dan HAVING.
- Aggregate Functions — perilaku agregat terhadap NULL.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
buat-tabel.sql
Unduh berkas-- tabel latihan: penjualan Toko Maju Jaya, Januari-Februari 2026
CREATE TABLE penjualan (
id integer PRIMARY KEY,
tanggal date NOT NULL,
kategori text NOT NULL,
produk text NOT NULL,
kota text NOT NULL,
jumlah integer NOT NULL,
harga_satuan numeric(12,2) NOT NULL,
metode_bayar text
);
INSERT INTO penjualan (id, tanggal, kategori, produk, kota, jumlah, harga_satuan, metode_bayar) VALUES
( 1, DATE '2026-01-05', 'Minuman', 'Kopi Gayo', 'Jakarta', 10, 85000.00, 'tunai'),
( 2, DATE '2026-01-08', 'Makanan', 'Biskuit', 'Jakarta', 20, 25000.00, 'transfer'),
( 3, DATE '2026-01-12', 'Alat Tulis', 'Pulpen', 'Bandung', 15, 8000.00, 'qris'),
( 4, DATE '2026-01-15', 'Minuman', 'Kopi Gayo', 'Bandung', 8, 85000.00, 'transfer'),
( 5, DATE '2026-01-20', 'Makanan', 'Biskuit', 'Surabaya', 12, 25000.00, NULL),
( 6, DATE '2026-01-25', 'Alat Tulis', 'Pulpen', 'Jakarta', 30, 8000.00, 'tunai'),
( 7, DATE '2026-02-02', 'Minuman', 'Kopi Gayo', 'Surabaya', 12, 85000.00, 'qris'),
( 8, DATE '2026-02-06', 'Makanan', 'Biskuit', 'Bandung', 25, 25000.00, 'transfer'),
( 9, DATE '2026-02-10', 'Alat Tulis', 'Pulpen', 'Surabaya', 6, 8000.00, NULL),
(10, DATE '2026-02-14', 'Minuman', 'Kopi Gayo', 'Jakarta', 5, 85000.00, 'qris'),
(11, DATE '2026-02-18', 'Makanan', 'Biskuit', 'Jakarta', 18, 25000.00, 'tunai'),
(12, DATE '2026-02-24', 'Alat Tulis', 'Pulpen', 'Bandung', 9, 8000.00, 'qris');
SELECT id, tanggal, kategori, produk, kota, jumlah, harga_satuan, metode_bayar
FROM penjualan
ORDER BY id;
dasar-having.sql
Unduh berkas-- HAVING dengan ekspresi agregat: saring kelompok berdasarkan hasil ringkasan
-- kelompok kota dengan minimal 4 transaksi
SELECT kota,
count(*) AS transaksi,
sum(jumlah) AS total_item
FROM penjualan
GROUP BY kota
HAVING count(*) >= 4
ORDER BY kota;
-- kelompok kategori dengan nilai penjualan di atas 1.000.000
SELECT kategori,
sum(jumlah * harga_satuan) AS total_nilai
FROM penjualan
GROUP BY kategori
HAVING sum(jumlah * harga_satuan) > 1000000
ORDER BY kategori;
-- kelompok kota dengan rata-rata item per transaksi minimal 15
SELECT kota,
round(avg(jumlah), 2) AS rata_item
FROM penjualan
GROUP BY kota
HAVING avg(jumlah) >= 15
ORDER BY kota;
having-tanpa-group-by.sql
Unduh berkas-- HAVING tanpa GROUP BY: seluruh tabel menjadi satu kelompok
-- kondisi terpenuhi (12 > 10): baris ringkasan tunggal tetap tampil
SELECT count(*) AS transaksi,
sum(jumlah * harga_satuan) AS total_nilai
FROM penjualan
HAVING count(*) > 10;
-- kondisi gagal (12 > 100): hasil kosong, bukan angka nol
SELECT count(*) AS transaksi,
sum(jumlah * harga_satuan) AS total_nilai
FROM penjualan
HAVING count(*) > 100;
where-agregat-error.sql
Unduh berkas-- WHERE tidak boleh memuat fungsi agregat: error yang diharapkan
SELECT kategori,
count(*) AS transaksi
FROM penjualan
WHERE count(*) > 1
GROUP BY kategori
ORDER BY kategori;
where-vs-having.sql
Unduh berkas-- perbedaan alur: WHERE menyaring baris sebelum dikelompokkan,
-- HAVING menyaring kelompok sesudah agregat dihitung
-- (a) WHERE dulu: transaksi dengan jumlah >= 15, lalu dikelompokkan per kota
SELECT kota,
count(*) AS transaksi
FROM penjualan
WHERE jumlah >= 15
GROUP BY kota
ORDER BY kota;
-- (b) HAVING saja: kelompok kota dengan minimal 4 transaksi
SELECT kota,
count(*) AS transaksi
FROM penjualan
GROUP BY kota
HAVING count(*) >= 4
ORDER BY kota;
-- (c) keduanya sekaligus: hanya Februari, lalu kategori dengan nilai >= 1.200.000
SELECT kategori,
count(*) AS transaksi,
sum(jumlah * harga_satuan) AS total_nilai
FROM penjualan
WHERE tanggal >= DATE '2026-02-01'
GROUP BY kategori
HAVING sum(jumlah * harga_satuan) >= 1200000
ORDER BY kategori;