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):

sql buat-tabel.sql
-- 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:

text
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.

AspekWHEREHAVING
Menyaringbaris mentahkelompok hasil GROUP BY
Boleh memuat agregat?tidak — errorya, itu memang fungsinya
Boleh kolom biasa?yaya, asalkan kolom grup
Dipakai untukkriteria 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:

text
  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:

  1. Query pertama: tiga kelompok kota terbentuk (Jakarta 5, Bandung 4, Surabaya 3 transaksi), lalu HAVING count(*) >= 4 hanya meloloskan Jakarta dan Bandung. Surabaya hilang dari hasil — bukan karena barisnya disaring, melainkan karena ringkasannya tidak memenuhi ambang.
  2. 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.
  3. Query ketiga: agregat boleh dibungkus round() di SELECT untuk tampilan, tetapi kondisi HAVING menulis avg(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:

text
  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:

  1. Query (a): WHERE jumlah >= 15 membuang 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”.
  2. 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.
  3. Query (c): alur lengkapnya — WHERE memangkas data ke enam transaksi Februari, GROUP BY kategori membentuk tiga kelompok, lalu HAVING sum(...) >= 1200000 hanya 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:

text
 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:

text
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 #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

buat-tabel.sql

Unduh berkas
sql
-- 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
sql
-- 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
sql
-- 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
sql
-- 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
sql
-- 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;