Fungsi agregat seperti COUNT dan SUM meringkas seluruh tabel menjadi satu angka. Pertanyaan bisnis yang sesungguhnya jarang sebesar itu — biasanya kita ingin “total per pelanggan” atau “jumlah pesanan per bulan”, bukan total keseluruhan. Klausa GROUP BY membelah tabel menjadi kelompok-kelompok lalu menjalankan agregat pada tiap kelompok, dan HAVING menyaring kelompok hasilnya. Kita akan memakai domain toko, dan karena JOIN belum dibahas, seluruh contoh cukup memakai satu tabel.

Data Contoh #

Tiga tabel inti domain toko dipakai pada chapter ini, plus tabel produk yang dirujuk oleh detail_pesanan:

sql
CREATE TABLE pelanggan (
  id INTEGER PRIMARY KEY,
  nama TEXT NOT NULL,
  kota TEXT
);
CREATE TABLE produk (
  id INTEGER PRIMARY KEY,
  nama TEXT NOT NULL,
  harga INTEGER NOT NULL,
  stok INTEGER
);
CREATE TABLE pesanan (
  id INTEGER PRIMARY KEY,
  pelanggan_id INTEGER REFERENCES pelanggan(id),
  tanggal TEXT NOT NULL
);
CREATE TABLE detail_pesanan (
  pesanan_id INTEGER REFERENCES pesanan(id),
  produk_id INTEGER REFERENCES produk(id),
  jumlah INTEGER NOT NULL,
  PRIMARY KEY (pesanan_id, produk_id)
);

INSERT INTO pelanggan (id, nama, kota) VALUES
  (1, 'Budi', 'Jakarta'), (2, 'Siti', 'Bandung'), (3, 'Ani', NULL),
  (4, 'Rina', 'Surabaya'), (5, 'Dewi', 'Jakarta');
INSERT INTO produk (id, nama, harga, stok) VALUES
  (1, 'Kopi Gayo', 45000, 20), (2, 'Teh Melati', 18000, 35),
  (3, 'Gula Aren', 25000, 15), (4, 'Sirup Vanila', 25000, NULL),
  (5, 'Kopi Toraja', 52000, 10), (6, 'Teh Hijau', 22000, 0),
  (7, 'Cangkir Keramik', 35000, 8);
INSERT INTO pesanan (id, pelanggan_id, tanggal) VALUES
  (1, 1, '2025-06-01'), (2, 2, '2025-06-03'), (3, 1, '2025-06-07'),
  (4, 3, '2025-06-10'), (5, 2, '2025-06-15'), (6, 1, '2025-07-02');
INSERT INTO detail_pesanan (pesanan_id, produk_id, jumlah) VALUES
  (1, 1, 2), (1, 3, 1), (2, 2, 3), (3, 1, 1),
  (3, 4, 2), (4, 5, 1), (5, 6, 2), (5, 3, 2), (6, 1, 1);

Memecah Tabel Menjadi Kelompok #

Query berikut menghitung pelanggan per kota. Kolom kota di belakang GROUP BY menjadi kunci pengelompokan, dan COUNT(*) dihitung ulang untuk tiap kelompok:

sql
SELECT kota, COUNT(*) AS jumlah
FROM pelanggan
GROUP BY kota
ORDER BY kota;

Output yang Dihasilkan:

text
|1
Bandung|1
Jakarta|2
Surabaya|1

Baris pertama tampil aneh — kolomnya kosong. Itulah kelompok NULL: engine SQLite memperlakukan semua kota yang NULL sebagai satu kelompok tersendiri, dan pada mode list nilai NULL dicetak sebagai field kosong. NULL tidak pernah “bubar” ke dalam kelompok kota mana pun.

Agregat mana pun dari Chapter Fungsi Agregat bisa mengisi kolom hasilnya. Query berikut merangkum tabel detail_pesanan per produk:

sql
SELECT produk_id, COUNT(*) AS kali_dipesan, SUM(jumlah) AS unit
FROM detail_pesanan
GROUP BY produk_id
ORDER BY produk_id;

Output yang Dihasilkan:

text
1|3|4
2|1|3
3|2|3
4|1|2
5|1|1
6|1|2

Produk 1 (Kopi Gayo) muncul di tiga baris detail dengan total empat unit; produk 7 tidak pernah dipesan sehingga tidak punya kelompok sama sekali.

Aturan Kolom Non-Agregat #

Perhatikan bahwa setiap kolom di SELECT query di atas adalah salah satu dari dua jenis: masuk GROUP BY, atau dibungkus agregat. Aturan praktis ini wajib: kolom yang tidak dikelompokkan dan tidak diagregasi (“bare column”) membuat hasilnya tidak terdefinisi dengan baik.

SQLite sebenarnya membiarkan query semacam itu jalan — mesin database lain menolaknya dengan galat sintaks. Lihat nama pada query berikut:

sql
SELECT kota, nama, COUNT(*) AS jumlah
FROM pelanggan
GROUP BY kota
ORDER BY kota;

Output yang Dihasilkan:

text
|Ani|1
Bandung|Siti|1
Jakarta|Budi|2
Surabaya|Rina|1

Untuk kelompok Jakarta yang beranggotakan Budi dan Dewi, engine SQLite menampilkan Budi — padahal boleh saja Dewi yang muncul.

Mengelompokan dengan Beberapa Kolom #

Satu kunci pengelompokan bisa dibentuk dari lebih dari satu kolom — cukup cantumkan semuanya di GROUP BY. Query berikut menghitung pesanan per pelanggan per bulan dengan memotong tujuh karakter pertama tanggal (bagian YYYY-MM):

sql
SELECT pelanggan_id, substr(tanggal, 1, 7) AS bulan, COUNT(*) AS jumlah_pesanan
FROM pesanan
GROUP BY pelanggan_id, substr(tanggal, 1, 7)
ORDER BY pelanggan_id, bulan;

Output yang Dihasilkan:

text
1|2025-06|2
1|2025-07|1
2|2025-06|2
3|2025-06|1

Budi (pelanggan 1) mendapat dua baris: dua pesanan di Juni dan satu di Juli. Ekspresi seperti substr() juga sah sebagai kunci GROUP BY — yang penting ekspresi di SELECT dan di GROUP BY sama persis.

Menyaring Kelompok dengan HAVING #

Setelah kelompok terbentuk, HAVING memutuskan kelompok mana yang boleh tampil. Misalnya kita hanya ingin pesanan yang totalnya sedikit banyak — minimal tiga unit:

sql
SELECT pesanan_id, SUM(jumlah) AS total_unit
FROM detail_pesanan
GROUP BY pesanan_id
HAVING SUM(jumlah) >= 3
ORDER BY pesanan_id;

Output yang Dihasilkan:

text
1|3
2|3
3|3
5|4

Tanpa HAVING, enam kelompok tampil (pesanan 4 dan 6 hanya berisi satu unit). Syarat pada HAVING boleh memakai agregat — hal yang tidak boleh dilakukan WHERE.

WHERE Sebelum, HAVING Sesudah #

Keduanya menyaring, tetapi pada tahap yang berbeda: WHERE menyaring baris mentah sebelum pengelompokan, HAVING menyaring kelompok hasil agregasi setelahnya. Perbedaan urutan ini mengubah hasil secara nyata:

sql
SELECT pesanan_id, SUM(jumlah) AS total_unit
FROM detail_pesanan
WHERE produk_id IN (1, 3)
GROUP BY pesanan_id
ORDER BY pesanan_id;

Output yang Dihasilkan:

text
1|3
3|1
5|2
6|1
sql
SELECT pesanan_id, SUM(jumlah) AS total_unit
FROM detail_pesanan
WHERE produk_id IN (1, 3)
GROUP BY pesanan_id
HAVING SUM(jumlah) >= 2
ORDER BY pesanan_id;

Output yang Dihasilkan:

text
1|3
5|2

Baris detail produk selain 1 dan 3 dibuang lebih dulu oleh WHERE, sehingga jumlahnya tidak pernah ikut dijumlah; HAVING lalu menyisakan kelompok yang totalnya minimal dua. Seandainya syarat total kita pindah ke WHERE, query akan gagal karena SUM(jumlah) belum ada saat baris mentah dibaca.

AspekWHEREHAVING
MenyaringBaris mentahKelompok hasil agregasi
Boleh memakai agregatTidakYa
DievaluasiSebelum GROUP BYSetelah GROUP BY

Kombinasi GROUP BY dengan penggabungan tabel dibuka pada Chapter Menggabungkan Tabel dengan JOIN — di sana laporan “total belanja per pelanggan” akhirnya bisa dibuat utuh.