GROUP BY membagi baris hasil query ke dalam kelompok, lalu setiap kelompok diringkas menjadi satu baris hasil. Topik ini membahas semantik pengelompokan itu, aturan pemilihan kolom pada SELECT, urutan evaluasi GROUP BY terhadap WHERE, serta catatan khusus tentang mode strict. Dataset peminjaman dan anggota yang sama dipakai di seluruh contoh, sehingga setiap keluaran dapat dihitung ulang langsung dari datanya. Setiap blok SQL memuat pembuatan tabelnya sendiri, sehingga dapat dijalankan langsung dengan sqlite3 :memory:.
Satu baris hasil per grup#
Klausa GROUP BY menerima daftar kolom atau ekspresi, lalu baris-baris yang nilainya sama ditaruh dalam satu grup. Fungsi agregasi pada SELECT kemudian dihitung per grup, bukan atas seluruh tabel, sehingga satu grup menghasilkan tepat satu baris hasil. Query berikut menghitung banyaknya peminjaman dan total denda untuk setiap anggota.
CREATE TABLE anggota (
id INTEGER PRIMARY KEY,
nama TEXT NOT NULL
);
CREATE TABLE peminjaman (
id INTEGER PRIMARY KEY,
id_anggota INTEGER,
id_buku INTEGER,
tanggal_pinjam TEXT,
denda REAL
);
INSERT INTO anggota (id, nama) VALUES
(1, 'Ali'), (2, 'Budi'), (3, 'Citra'),
(4, 'Dewi'), (5, 'Eka'), (6, 'Farhan');
INSERT INTO peminjaman (id, id_anggota, id_buku, tanggal_pinjam, denda) VALUES
(1, 1, 1, '2026-03-02', 0.5),
(2, 1, 3, '2026-03-04', 1.0),
(3, 2, 2, '2026-03-05', NULL),
(4, 3, 1, '2026-03-09', 0.5),
(5, 3, 4, '2026-03-12', 2.0),
(6, 4, 5, '2026-03-16', NULL),
(7, 5, 3, '2026-03-19', 1.0),
(8, 5, 6, '2026-03-23', NULL);
SELECT id_anggota, COUNT(*), SUM(denda)
FROM peminjaman
GROUP BY id_anggota
ORDER BY id_anggota;1|2|1.5
2|1|
3|2|2.5
4|1|
5|2|1.0Keluaran memuat lima baris karena data memuat lima nilai id_anggota yang berbeda. Ali, Citra, dan Eka masing-masing meminjam dua kali, sedangkan Budi dan Dewi sekali. Baris Budi dan Dewi menampilkan kolom total denda kosong karena SUM atas grup yang seluruh dendanya NULL menghasilkan NULL, sedangkan Farhan tidak muncul sama sekali karena tidak ada barisnya pada tabel peminjaman. Tanpa ORDER BY, urutan baris hasil GROUP BY tidak dijamin oleh bahasa SQL meskipun sqlite3 pada praktiknya menampilkan kelompok dengan urutan yang rapi, sehingga tambahkan ORDER BY eksplisit setiap kali urutan laporan penting.
Diagram di atas merangkum alur tersebut. Delapan baris mentah dipilah menurut nilai id_anggota menjadi lima grup, lalu agregat dihitung di dalam setiap grup, dan hasil akhirnya berupa lima baris. Ukuran hasil query ber-GROUP BY ditentukan oleh banyaknya grup, bukan oleh banyaknya baris masukan.
Kolom yang boleh dipilih#
Aturan yang aman untuk SELECT pada query ber-GROUP BY berbunyi sebagai berikut. Kolom non-agregat yang dipilih harus menjadi kunci pengelompokan, sedangkan kolom lain hanya boleh muncul di dalam fungsi agregasi. Aturan ini menjaga kejelasan karena satu grup berisi banyak baris, sedangkan baris hasil hanya satu, sehingga nilai sebuah kolom non-kunci tidak pasti mewakili grupnya. Standar SQL menegakkan aturan tersebut sebagai error, sedangkan SQLite menerimanya dengan perilaku yang dibahas pada bagian berikutnya.
CREATE TABLE anggota (
id INTEGER PRIMARY KEY,
nama TEXT NOT NULL
);
CREATE TABLE peminjaman (
id INTEGER PRIMARY KEY,
id_anggota INTEGER,
id_buku INTEGER,
tanggal_pinjam TEXT,
denda REAL
);
INSERT INTO anggota (id, nama) VALUES
(1, 'Ali'), (2, 'Budi'), (3, 'Citra'),
(4, 'Dewi'), (5, 'Eka'), (6, 'Farhan');
INSERT INTO peminjaman (id, id_anggota, id_buku, tanggal_pinjam, denda) VALUES
(1, 1, 1, '2026-03-02', 0.5),
(2, 1, 3, '2026-03-04', 1.0),
(3, 2, 2, '2026-03-05', NULL),
(4, 3, 1, '2026-03-09', 0.5),
(5, 3, 4, '2026-03-12', 2.0),
(6, 4, 5, '2026-03-16', NULL),
(7, 5, 3, '2026-03-19', 1.0),
(8, 5, 6, '2026-03-23', NULL);
SELECT a.nama, COUNT(*) AS jumlah, SUM(p.denda) AS total_denda
FROM peminjaman p
JOIN anggota a ON a.id = p.id_anggota
GROUP BY a.nama
ORDER BY a.nama;Ali|2|1.5
Budi|1|
Citra|2|2.5
Dewi|1|
Eka|2|1.0Query di atas memilih a.nama yang juga menjadi kunci pengelompokannya, sehingga setiap nama terikat jelas dengan agregatnya. Join dengan tabel anggota dilakukan agar laporan menampilkan nama yang mudah dibaca, bukan id yang harus diterka. Klausa AS memberi judul pada kolom hasil, dan baris Budi serta Dewi tetap menampilkan total kosong karena seluruh dendanya NULL.
Bahaya kolom di luar kunci pengelompokan#
SQLite memperbolehkan kolom non-agregat yang bukan kunci pengelompokan untuk tetap dipilih. Kolom seperti itu disebut bare column dalam dokumentasi SQLite, dan nilainya diambil dari satu baris arbitrer di dalam grup. Perilaku ini bersifat teknis, bukan jaminan semantik. Query berikut sengaja menampilkan id_buku yang bukan kuncinya.
CREATE TABLE anggota (
id INTEGER PRIMARY KEY,
nama TEXT NOT NULL
);
CREATE TABLE peminjaman (
id INTEGER PRIMARY KEY,
id_anggota INTEGER,
id_buku INTEGER,
tanggal_pinjam TEXT,
denda REAL
);
INSERT INTO anggota (id, nama) VALUES
(1, 'Ali'), (2, 'Budi'), (3, 'Citra'),
(4, 'Dewi'), (5, 'Eka'), (6, 'Farhan');
INSERT INTO peminjaman (id, id_anggota, id_buku, tanggal_pinjam, denda) VALUES
(1, 1, 1, '2026-03-02', 0.5),
(2, 1, 3, '2026-03-04', 1.0),
(3, 2, 2, '2026-03-05', NULL),
(4, 3, 1, '2026-03-09', 0.5),
(5, 3, 4, '2026-03-12', 2.0),
(6, 4, 5, '2026-03-16', NULL),
(7, 5, 3, '2026-03-19', 1.0),
(8, 5, 6, '2026-03-23', NULL);
SELECT id_anggota, id_buku, COUNT(*)
FROM peminjaman
GROUP BY id_anggota
ORDER BY id_anggota;1|1|2
2|2|1
3|1|2
4|5|1
5|3|2Perhatikan baris pertama. Kelompok id_anggota 1 memuat dua peminjaman, yaitu ke buku 1 dan buku 3, tetapi keluaran hanya menampilkan salah satunya tanpa menandai pilihannya. Nilai itu bukan rata-rata maupun nilai pertama yang dijanjikan, melainkan nilai dari baris arbitrer, sehingga laporan yang menampilkan kolom semacam ini mudah menyesatkan pembacanya.
Perilaku khusus min() dan max()#
Ada satu pengecualian yang terdokumentasi dan justru berguna. Apabila query memakai tepat satu fungsi min() atau max() pada kolom, kolom non-agregat yang dipilih diambil dari baris tempat nilai minimum atau maksimum itu dicapai. Pola ini menjadi cara singkat untuk mengambil baris terbaik dari setiap grup tanpa subquery.
CREATE TABLE anggota (
id INTEGER PRIMARY KEY,
nama TEXT NOT NULL
);
CREATE TABLE peminjaman (
id INTEGER PRIMARY KEY,
id_anggota INTEGER,
id_buku INTEGER,
tanggal_pinjam TEXT,
denda REAL
);
INSERT INTO anggota (id, nama) VALUES
(1, 'Ali'), (2, 'Budi'), (3, 'Citra'),
(4, 'Dewi'), (5, 'Eka'), (6, 'Farhan');
INSERT INTO peminjaman (id, id_anggota, id_buku, tanggal_pinjam, denda) VALUES
(1, 1, 1, '2026-03-02', 0.5),
(2, 1, 3, '2026-03-04', 1.0),
(3, 2, 2, '2026-03-05', NULL),
(4, 3, 1, '2026-03-09', 0.5),
(5, 3, 4, '2026-03-12', 2.0),
(6, 4, 5, '2026-03-16', NULL),
(7, 5, 3, '2026-03-19', 1.0),
(8, 5, 6, '2026-03-23', NULL);
SELECT id_anggota, MAX(denda), id_buku, tanggal_pinjam
FROM peminjaman
GROUP BY id_anggota
ORDER BY id_anggota;1|1.0|3|2026-03-04
2||2|2026-03-05
3|2.0|4|2026-03-12
4||5|2026-03-16
5|1.0|3|2026-03-19Pada keluaran di atas, kolom id_buku dan tanggal_pinjam diambil dari baris yang dendanya paling besar di grup masing-masing. Denda terbesar Ali adalah 1.0 yang berasal dari peminjaman buku 3 pada 2026-03-04, dan pasangan itulah yang tampil. Grup Budi dan Dewi menampilkan nilai dari baris arbitrer karena nilai MAX-nya NULL sehingga tidak ada baris yang dicapai. Apabila beberapa baris memiliki nilai ekstrem yang sama, baris yang dipilih kembali bersifat arbitrer, sehingga tambahkan syarat pembeda bila keunikan hasil penting.
Urutan evaluasi terhadap WHERE#
GROUP BY dievaluasi setelah WHERE. Baris yang gagal memenuhi syarat WHERE dibuang lebih dahulu, baru sisanya dikelompokkan dan diagregasi. Akibatnya, agregat pada SELECT hanya menghitung baris yang lolos penyaringan, dan grup yang seluruh barisnya terbuang tidak muncul sama sekali pada hasil.
CREATE TABLE anggota (
id INTEGER PRIMARY KEY,
nama TEXT NOT NULL
);
CREATE TABLE peminjaman (
id INTEGER PRIMARY KEY,
id_anggota INTEGER,
id_buku INTEGER,
tanggal_pinjam TEXT,
denda REAL
);
INSERT INTO anggota (id, nama) VALUES
(1, 'Ali'), (2, 'Budi'), (3, 'Citra'),
(4, 'Dewi'), (5, 'Eka'), (6, 'Farhan');
INSERT INTO peminjaman (id, id_anggota, id_buku, tanggal_pinjam, denda) VALUES
(1, 1, 1, '2026-03-02', 0.5),
(2, 1, 3, '2026-03-04', 1.0),
(3, 2, 2, '2026-03-05', NULL),
(4, 3, 1, '2026-03-09', 0.5),
(5, 3, 4, '2026-03-12', 2.0),
(6, 4, 5, '2026-03-16', NULL),
(7, 5, 3, '2026-03-19', 1.0),
(8, 5, 6, '2026-03-23', NULL);
SELECT id_anggota, COUNT(*)
FROM peminjaman
WHERE denda IS NOT NULL
GROUP BY id_anggota
ORDER BY id_anggota;1|2
3|2
5|1Syarat denda IS NOT NULL membuang tiga baris sebelum pengelompokan. Budi dan Dewi kehilangan satu-satunya barisnya sehingga tidak membentuk grup, sedangkan Eka hanya menghitung satu peminjaman. Urutan lengkapnya adalah FROM, WHERE, GROUP BY, lalu pemilihan kolom beserta agregasinya, dan akhirnya ORDER BY. Klausa HAVING menyisipkan penyaringan grup di antara GROUP BY dan pemilihan hasil.
Mode strict tidak mengubah semantik#
Tabel strict membatasi tipe dan nilai yang boleh disimpan sehingga data kotor tertangkap lebih awal pada saat penulisan. Batasan itu bekerja pada tingkat penyimpanan, sedangkan semantik GROUP BY bekerja pada tingkat query, sehingga keduanya tidak saling menyentuh. Query berikut mengulang contoh kolom arbitrer pada tabel yang dibuat dengan kata kunci STRICT.
CREATE TABLE anggota (
id INTEGER PRIMARY KEY,
nama TEXT NOT NULL
) STRICT;
CREATE TABLE peminjaman (
id INTEGER PRIMARY KEY,
id_anggota INTEGER,
id_buku INTEGER,
tanggal_pinjam TEXT,
denda REAL
) STRICT;
INSERT INTO anggota (id, nama) VALUES
(1, 'Ali'), (2, 'Budi'), (3, 'Citra'),
(4, 'Dewi'), (5, 'Eka'), (6, 'Farhan');
INSERT INTO peminjaman (id, id_anggota, id_buku, tanggal_pinjam, denda) VALUES
(1, 1, 1, '2026-03-02', 0.5),
(2, 1, 3, '2026-03-04', 1.0),
(3, 2, 2, '2026-03-05', NULL),
(4, 3, 1, '2026-03-09', 0.5),
(5, 3, 4, '2026-03-12', 2.0),
(6, 4, 5, '2026-03-16', NULL),
(7, 5, 3, '2026-03-19', 1.0),
(8, 5, 6, '2026-03-23', NULL);
SELECT id_anggota, id_buku, COUNT(*)
FROM peminjaman
GROUP BY id_anggota
ORDER BY id_anggota;1|1|2
2|2|1
3|1|2
4|5|1
5|3|2Keluarannya persis sama dengan versi tabel biasa, termasuk nilai id_buku yang diambil dari baris arbitrer. Mode strict tidak mengubah kolom mana yang boleh dipilih dan tidak memberi makna tambahan pada kolom non-kunci. Disiplin tipe data dan disiplin menulis query pengelompokan adalah dua praktik yang harus dijaga masing-masing.
Kesalahan umum#
Mengharapkan nilai bermakna dari kolom non-kunci. SQLite menerimanya tanpa pesan error, sehingga kesalahannya baru terasa ketika laporan dibandingkan dengan data aslinya. Pindahkan kolom tersebut ke dalam kunci pengelompokan atau ke dalam fungsi agregasi agar nilainya terdefinisi.
Mengandalkan urutan keluaran GROUP BY yang tampak rapi. Urutan tersebut bukan jaminan bahasa, sehingga tampilan laporan dapat berubah antarversi atau antaralat. Tambahkan ORDER BY pada kolom kunci setiap kali urutan pembacaan penting.
Lupa bahwa grup yang seluruh isinya NULL menghasilkan agregat NULL. Laporan yang membuang baris kosong secara diam-diam akan kehilangan anggota seperti Budi dan Dewi. Bungkus agregat dengan COALESCE bila nol adalah makna yang benar.
Ringkasan#
- GROUP BY membagi baris menjadi grup dan setiap grup menghasilkan tepat satu baris hasil.
- Kolom non-agregat yang dipilih sebaiknya hanya kunci pengelompokan, sedangkan SQLite mengambil kolom lain dari baris arbitrer tanpa peringatan.
- Dengan satu fungsi min() atau max(), kolom non-agregat diambil dari baris tempat nilai ekstrem itu dicapai, sesuai perilaku yang terdokumentasi.
- WHERE dievaluasi sebelum pengelompokan, dan mode strict tidak mengubah semantik query ber-GROUP BY.
Referensi#
- SELECT — SQLite — Dokumentasi resmi klausa SELECT dan GROUP BY, termasuk perilaku bare column pada agregasi.
- STRICT Tables — SQLite — Dokumentasi tabel strict beserta batasan tipe dan nilainya.