Klausa WHERE menghentikan baris yang tidak memenuhi syarat sebelum agregasi berjalan, sehingga WHERE SUM(jumlah) > 5 tidak pernah sah — nilai SUM(jumlah) baru ada setelah pengelompokan selesai. MySQL menyediakan klausa HAVING khusus untuk menyaring hasil agregat: satu-satunya tempat where-condition boleh membandingkan fungsi agregat. Pada chapter ini kita membuktikan pembagian tugas WHERE dan HAVING, mempelajari urutan eksekusi logis yang menjelaskan mengapa keduanya tidak bisa saling menggantikan, lalu merangkai kombinasi lengkap WHERE–GROUP BY–HAVING–ORDER BY yang menjadi pola standar laporan harian.

Menyiapkan Tabel Latihan #

Kita lanjutkan memakai tabel penjualan yang sama dengan chapter GROUP BY: empat belas transaksi penjualan kopi di empat kota, dengan kolom pelanggan yang boleh NULL. Konsistensi data antar chapter membuat setiap angka hasil agregasi mudah dibandingkan dengan hasil chapter sebelumnya tanpa membuka ulang skrip:

sql
DROP DATABASE IF EXISTS belajar_agregasi;
CREATE DATABASE belajar_agregasi;
USE belajar_agregasi;

Output yang Dihasilkan:

text
Query OK, 0 rows affected, 1 warning (0.00 sec)

Query OK, 1 row affected (0.00 sec)

Database changed
sql
CREATE TABLE penjualan (
  id INT AUTO_INCREMENT PRIMARY KEY,
  tanggal DATE NOT NULL,
  kota VARCHAR(50) NOT NULL,
  produk VARCHAR(50) NOT NULL,
  jumlah INT NOT NULL,
  harga DECIMAL(10,2) NOT NULL,
  pelanggan VARCHAR(50) NULL
);

INSERT INTO penjualan (tanggal, kota, produk, jumlah, harga, pelanggan) VALUES
  ('2026-01-05', 'Jakarta',    'Kopi Gayo',   2, 55000.00, 'Budi'),
  ('2026-01-05', 'Bandung',    'Teh Melati',  3, 25000.00, 'Siti'),
  ('2026-01-06', 'Jakarta',    'Teh Melati',  1, 25000.00, NULL),
  ('2026-01-07', 'Surabaya',   'Kopi Toraja', 5, 62000.00, 'Andi'),
  ('2026-01-08', 'Bandung',    'Gula Aren',   4, 15000.00, NULL),
  ('2026-01-09', 'Jakarta',    'Gula Aren',   2, 15000.00, 'Rina'),
  ('2026-01-10', 'Yogyakarta', 'Kopi Gayo',   3, 55000.00, NULL),
  ('2026-01-11', 'Surabaya',   'Teh Melati',  2, 25000.00, 'Budi'),
  ('2026-01-12', 'Yogyakarta', 'Kopi Toraja', 1, 62000.00, NULL),
  ('2026-01-13', 'Jakarta',    'Kopi Toraja', 4, 62000.00, 'Siti'),
  ('2026-01-14', 'Bandung',    'Kopi Gayo',   1, 55000.00, NULL),
  ('2026-01-15', 'Surabaya',   'Gula Aren',   6, 15000.00, 'Rina'),
  ('2026-01-16', 'Jakarta',    'Kopi Gayo',   1, 55000.00, 'Dewi'),
  ('2026-01-17', 'Bandung',    'Teh Melati',  2, 25000.00, NULL);

Output yang Dihasilkan:

text
Query OK, 0 rows affected (0.00 sec)

Query OK, 14 rows affected (0.00 sec)
Records: 14  Duplicates: 0  Warnings: 0

Menyaring Hasil Agregat #

Pertanyaan yang tidak bisa dijawab WHERE misalnya berbunyi: kota mana yang total unit terjualnya lebih dari lima? Nilai SUM(jumlah) dihitung per kelompok kota, sehingga penyaringnya harus diletakkan setelah GROUP BY. Klausa HAVING mengisi posisi tersebut:

sql
SELECT kota, SUM(jumlah) AS total_unit
FROM penjualan
GROUP BY kota
HAVING SUM(jumlah) > 5
ORDER BY kota;

Output yang Dihasilkan:

text
+----------+------------+
| kota     | total_unit |
+----------+------------+
| Bandung  |         10 |
| Jakarta  |         10 |
| Surabaya |         13 |
+----------+------------+
3 rows in set (0.00 sec)

Hasilnya hanya memuat kota dengan total unit di atas lima. Bandung dengan total 10 unit, Jakarta dengan 10 unit, dan Surabaya dengan 13 unit lolos saringan, sedangkan Yogyakarta dengan hanya 4 unit tersaring keluar. Angka-angka ini bisa kita hitung ulang dari empat belas baris data secara manual — kebiasaan yang sebaiknya dipertahankan saat memverifikasi laporan agregasi.

WHERE dan HAVING Bekerja pada Tahap Berbeda #

Kekuatan sesungguhnya muncul saat keduanya dipakai bersamaan. Skenario berikutnya: dari transaksi produk kopi saja (bukan teh atau gula), kota mana yang total unitnya melebihi lima? WHERE memangkas baris berdasarkan produk sebelum pengelompokan; HAVING kemudian menyaring kelompok berdasarkan hasil SUM:

sql
SELECT kota, SUM(jumlah) AS total_unit_kopi
FROM penjualan
WHERE produk LIKE 'Kopi%'
GROUP BY kota
HAVING SUM(jumlah) > 5
ORDER BY kota;

Output yang Dihasilkan:

text
+---------+-----------------+
| kota    | total_unit_kopi |
+---------+-----------------+
| Jakarta |               7 |
+---------+-----------------+
1 row in set (0.00 sec)

Perhatikan bahwa Jakarta kini totalnya turun menjadi 7 unit karena transaksi Teh Melati dan Gula Aren di Jakarta sudah dibuang lebih dulu oleh WHERE. Bandung hilang seluruhnya — satu-satunya transaksi kopinya hanya 1 unit — meskipun total keseluruhan Bandung tadi 10 unit. Perbedaan kedua angka ini membuktikan bahwa WHERE dan HAVING menyaring objek yang berbeda: baris mentah versus kelompok hasil agregat.

Urutan eksekusi logis menjelaskan pembagian tugas tersebut. Meskipun kita menulis klausa dalam urutan SELECT–FROM–WHERE–GROUP BY–HAVING–ORDER BY, MySQL memprosesnya sebagai FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY:

TahapKlausaObjek yang disaring
1FROMTabel sumber
2WHEREBaris individual
3GROUP BYPembentukan kelompok
4HAVINGKelompok hasil agregat
5SELECTKolom/ekspresi akhir
6ORDER BYBaris hasil akhir

Tabel ini juga menjelaskan dua aturan penulisan lain: alias kolom yang dibuat di SELECT belum ada ketika WHERE dievaluasi, tetapi sudah tersedia saat ORDER BY dievaluasi. Perilaku HAVING berada di tengah — dan di titik ini MySQL memberi kelonggaran.

Alias Agregat di HAVING #

Standar SQL mewajibkan HAVING menulis ulang ekspresi agregat secara penuh, dan MySQL mendukung gaya tersebut seperti contoh sebelumnya. Sebagai tambahan yang tidak standar, MySQL juga mengizinkan HAVING merujuk alias yang dideklarasikan di SELECT:

sql
SELECT kota, COUNT(*) AS jumlah_transaksi
FROM penjualan
GROUP BY kota
HAVING jumlah_transaksi >= 4
ORDER BY kota;

Output yang Dihasilkan:

text
+---------+------------------+
| kota    | jumlah_transaksi |
+---------+------------------+
| Bandung |                4 |
| Jakarta |                5 |
+---------+------------------+
2 rows in set (0.00 sec)

Query ini menyaring kota dengan sedikitnya empat transaksi. Alias jumlah_transaksi merujuk COUNT(*) — perbandingan >= 4 berlaku pada hasil hitung tersebut. Kelonggaran ini nyaman dipakai, namun ingat bahwa portabilitasnya rendah: database lain umumnya menolak alias di HAVING, sehingga kode yang akan dipindahkan antar database sebaiknya tetap menulis ekspresi agregat secara penuh.

HAVING Tanpa GROUP BY #

HAVING juga bisa berdiri tanpa GROUP BY. Pada query agregat tanpa pengelompokan, seluruh tabel menjadi satu kelompok tunggal, dan HAVING menyaring kelompok tunggal tersebut — hasilnya baik satu baris (kondisi terpenuhi) maupun nol baris (tidak terpenuhi):

sql
SELECT COUNT(*) AS total_transaksi, SUM(jumlah) AS total_unit
FROM penjualan
HAVING COUNT(*) > 10;

Output yang Dihasilkan:

text
+-----------------+------------+
| total_transaksi | total_unit |
+-----------------+------------+
|              14 |         37 |
+-----------------+------------+
1 row in set (0.00 sec)

Kondisi COUNT(*) > 10 bernilai benar karena tabel memuat empat belas baris, sehingga satu baris hasil agregat tetap tampil. Pola ini berguna sebagai gerbang sederhana: misalnya sebuah query dashboard hanya menampilkan angka bila data hari ini sudah terisi minimal sejumlah ambang tertentu, dan diam total bila belum.

Referensi #

Dengan HAVING, kotak peralatan laporan kita lengkap: WHERE memilih baris, GROUP BY mengelompokkan, HAVING menyaring kelompok, dan ORDER BY merapikan hasil. Bab berikutnya mengasah satu pola lanjutan yang kerap menggantikan laporan berkolom-kolom: agregasi dengan kondisi di dalam fungsi menggunakan CASE WHEN.