Foto oleh panumas nikhomkhai via Pexels

GROUP BY digunakan ketika kita perlu meringkas data berdasarkan kategori tertentu, misalnya total penjualan per kota atau jumlah pesanan per pelanggan. Tanpa GROUP BY, setiap baris data mentah harus diproses satu per satu di sisi aplikasi untuk mendapatkan ringkasan semacam ini — pekerjaan yang jauh lebih efisien dilakukan langsung di database.

Artikel ini membahas cara kerja GROUP BY beserta aturan penting yang sering membingungkan pemula, terutama terkait kolom apa saja yang boleh muncul di SELECT ketika GROUP BY dipakai.

Sintaks Dasar GROUP BY

Menghitung jumlah customer per kota

SELECT City, COUNT(*) AS JumlahCustomer FROM Customers GROUP BY City; -- Expected output: -- City | JumlahCustomer -- Jakarta | 120 -- Surabaya | 45

GROUP BY mengelompokkan baris-baris yang memiliki nilai sama pada kolom yang disebutkan, lalu setiap kelompok diringkas menjadi satu baris hasil menggunakan fungsi agregat seperti COUNT, SUM, atau AVG.

Aturan Penting: Kolom di SELECT Harus Ada di GROUP BY

Kolom non-agregat wajib disertakan di GROUP BY

-- SALAH, akan error SELECT City, Province, COUNT(*) FROM Customers GROUP BY City; -- BENAR SELECT City, Province, COUNT(*) FROM Customers GROUP BY City, Province; -- Expected error pada versi salah: -- Column 'Customers.Province' is invalid in the select list -- because it is not contained in either an aggregate function -- or the GROUP BY clause.

SQL Server akan menolak query jika ada kolom di SELECT yang bukan hasil fungsi agregat namun tidak disebutkan di GROUP BY, karena SQL Server tidak tahu nilai mana yang harus dipilih untuk mewakili kelompok tersebut jika satu City punya banyak Province berbeda.

GROUP BY dengan Banyak Kolom Agregat

Ringkasan lengkap per pelanggan

SELECT CustomerId, COUNT(*) AS JumlahOrder, SUM(TotalAmount) AS TotalBelanja, AVG(TotalAmount) AS RataRataBelanja FROM Orders GROUP BY CustomerId; -- Expected: satu baris ringkasan untuk SETIAP CustomerId yang unik

Satu query GROUP BY bisa menghasilkan banyak kolom ringkasan sekaligus, menjadikannya cara paling efisien membuat laporan dashboard dibanding menjalankan beberapa query terpisah untuk setiap metrik.

GROUP BY pada Ekspresi, Bukan Hanya Kolom Mentah

GROUP BY berdasarkan hasil ekspresi, bukan kolom asli

SELECT YEAR(OrderDate) AS Tahun, MONTH(OrderDate) AS Bulan, SUM(TotalAmount) AS TotalPenjualan FROM Orders GROUP BY YEAR(OrderDate), MONTH(OrderDate); -- Expected: total penjualan diringkas per bulan per tahun, -- bukan per tanggal mentah yang unik

GROUP BY tidak terbatas pada kolom mentah — ekspresi seperti hasil fungsi YEAR() dan MONTH() juga bisa dipakai, sangat berguna untuk laporan ringkasan per periode waktu tanpa perlu kolom tambahan khusus di table.

Menggunakan GROUP BY dengan Lebih dari Satu Kolom

GROUP BY dengan kombinasi dua kolom

SELECT City, Category, COUNT(*) AS Jumlah FROM Customers c JOIN Orders o ON o.CustomerID = c.CustomerID JOIN Products p ON p.ProductID = o.ProductID GROUP BY City, Category; -- Expected: satu baris untuk setiap kombinasi unik -- City dan Category, bukan hanya per City saja

Saat GROUP BY diberi lebih dari satu kolom, SQL Server akan memperlakukan setiap kombinasi unik dari kolom-kolom tersebut sebagai satu kelompok tersendiri. Ini berbeda dengan anggapan keliru bahwa menambahkan kolom kedua hanya menambah detail di dalam kelompok yang sudah ada — sebenarnya jumlah barisnya bisa jauh lebih banyak karena setiap kombinasi dihitung terpisah.

Urutan kolom dalam GROUP BY tidak memengaruhi hasil pengelompokan secara logis, namun tetap memengaruhi urutan default penyajian data sebelum ORDER BY diterapkan. Praktik yang aman adalah selalu menambahkan ORDER BY eksplisit setelah GROUP BY multi-kolom agar urutan tampilan tidak bergantung pada perilaku internal mesin database yang bisa berubah antar versi.

Aturan Kolom pada SELECT Saat Memakai GROUP BY

Error umum saat kolom non-agregat tidak disertakan di GROUP BY

-- SALAH: ProductName bukan kolom agregat dan tidak ada di GROUP BY -- SELECT Category, ProductName, SUM(UnitPrice) FROM Products GROUP BY Category; -- Error: ProductName invalid in select list because it is not -- contained in either an aggregate function or GROUP BY clause -- BENAR SELECT Category, SUM(UnitPrice) AS TotalHarga, COUNT(*) AS JumlahProduk FROM Products GROUP BY Category;

Ketika sebuah query memakai GROUP BY, SQL Server mewajibkan setiap kolom yang muncul di klausa SELECT untuk berupa salah satu dari dua hal saja: kolom yang juga disebutkan di GROUP BY, atau kolom yang dibungkus di dalam fungsi agregat seperti SUM, COUNT, AVG, MIN, atau MAX. Aturan ini masuk akal secara logis karena GROUP BY meringkas banyak baris menjadi satu baris per kelompok, sehingga SQL Server perlu tahu persis bagaimana cara meringkas setiap kolom yang ditampilkan.

Mencoba menampilkan kolom seperti ProductName tanpa fungsi agregat padahal GROUP BY hanya dilakukan pada Category akan menghasilkan error, karena dalam satu kelompok Category yang sama bisa terdapat banyak ProductName berbeda, dan SQL Server tidak memiliki cara otomatis untuk memutuskan ProductName mana yang harus ditampilkan mewakili kelompok tersebut.

GROUP BY sendiri secara logis dieksekusi setelah WHERE namun sebelum SELECT dan ORDER BY dalam urutan pemrosesan query, artinya kondisi WHERE menyaring baris mentah terlebih dahulu sebelum pengelompokan terjadi, sementara penyaringan terhadap hasil agregat setelah pengelompokan harus memakai HAVING, bukan WHERE.

GROUPING SETS dan ROLLUP untuk Subtotal Bertingkat

ROLLUP menghasilkan subtotal dan grand total otomatis

-- ROLLUP menghasilkan subtotal berjenjang: per kota, lalu total keseluruhan SELECT Kota, Kategori, SUM(TotalHarga) AS TotalPenjualan FROM dbo.Penjualan GROUP BY ROLLUP(Kota, Kategori); -- Hasil, contoh (baris dengan NULL adalah baris subtotal/grand total): -- Kota Kategori TotalPenjualan -- Jakarta Elektronik 50000000 -- Jakarta Fashion 20000000 -- Jakarta NULL 70000000 -- subtotal Jakarta -- NULL NULL 150000000 -- grand total seluruh kota

GROUPING SETS adalah perluasan GROUP BY yang memungkinkan satu query menghasilkan beberapa level agregasi sekaligus dalam satu hasil, menggantikan kebutuhan menulis beberapa query terpisah yang digabung dengan UNION ALL. Dengan GROUPING SETS (Kota, Kategori), (Kota), () misalnya, SQL Server akan menghasilkan baris agregasi per kombinasi Kota-Kategori, per Kota saja, dan satu baris grand total, semuanya dalam satu kali pemindaian data.

ROLLUP adalah bentuk singkat dari GROUPING SETS untuk kasus hierarki berjenjang yang umum, seperti Kota lalu Kategori di dalamnya, yang secara otomatis menghasilkan subtotal pada setiap level hierarki ditambah satu grand total di baris terakhir. Baris subtotal ini ditandai dengan nilai NULL pada kolom yang sedang diringkas, namun NULL ini bisa membingungkan jika data asli memang mengandung NULL, sehingga fungsi GROUPING() disediakan untuk membedakan NULL hasil ROLLUP dari NULL data asli.

Pemakaian ROLLUP dan GROUPING SETS sangat relevan untuk laporan keuangan atau laporan manajemen yang membutuhkan tampilan bertingkat seperti subtotal per cabang dan total perusahaan dalam satu laporan, tanpa perlu memproses ulang data di sisi aplikasi. Dibanding menggabungkan beberapa query manual dengan UNION ALL, pendekatan ini lebih efisien karena SQL Server dapat mengoptimalkan seluruh level agregasi dalam satu rencana eksekusi, bukan menjalankan pemindaian table berulang kali untuk setiap level.

GROUP BY dengan Ekspresi Tanggal untuk Agregasi per Bulan

GROUP BY berdasarkan ekspresi tanggal

-- Mengelompokkan total penjualan per tahun dan bulan SELECT YEAR(TanggalPesan) AS Tahun, MONTH(TanggalPesan) AS Bulan, SUM(TotalHarga) AS TotalPenjualan FROM dbo.Pesanan GROUP BY YEAR(TanggalPesan), MONTH(TanggalPesan) ORDER BY Tahun, Bulan; -- Hasil, contoh: -- Tahun Bulan TotalPenjualan -- 2026 1 125000000 -- 2026 2 98000000

GROUP BY tidak hanya bisa mengelompokkan berdasarkan kolom asli, melainkan juga berdasarkan ekspresi yang dihitung dari kolom tersebut, seperti YEAR(TanggalPesan) dan MONTH(TanggalPesan) untuk mengelompokkan data transaksi ke dalam periode tahun dan bulan. Ekspresi yang sama persis dengan yang ditulis di SELECT harus pula dituliskan di GROUP BY, karena SQL Server memperlakukan keduanya sebagai satu kesatuan definisi kelompok.

Pendekatan ini banyak dipakai untuk laporan tren bulanan atau tahunan tanpa perlu kolom tambahan khusus periode pada table sumber. Namun perlu diperhatikan bahwa membungkus kolom tanggal dengan fungsi seperti YEAR dan MONTH membuat GROUP BY tidak lagi SARGable terhadap index pada kolom tanggal tersebut, sehingga pada table dengan jutaan baris, pendekatan rentang tanggal eksplisit kadang lebih efisien dibanding mengandalkan fungsi langsung di dalam GROUP BY.

Kesalahan Umum yang Sering Terjadi

Mengira bisa memilih kolom bebas asal ada GROUP BY di query

Pemula sering mengira GROUP BY pada satu kolom mengizinkan kolom lain muncul bebas di SELECT, padahal SQL Server akan selalu menolak kolom non-agregat yang tidak disebutkan di GROUP BY.

Lupa bahwa GROUP BY pada ekspresi harus menulis ulang ekspresi yang sama persis

GROUP BY YEAR(OrderDate) tidak otomatis mengenali alias Tahun yang didefinisikan di SELECT pada beberapa versi SQL Server, sehingga ekspresi aslinya perlu ditulis ulang di GROUP BY.

Kapan Pendekatan Ini Tidak Cocok Dipakai

GROUP BY pada kolom dengan kardinalitas sangat tinggi (misalnya mengelompokkan berdasarkan CustomerId pada tabel dengan jutaan pelanggan) bisa menghasilkan jutaan baris hasil ringkasan yang hampir sama beratnya dengan data mentah, sehingga manfaat 'meringkas' menjadi kurang terasa dibanding mengelompokkan berdasarkan kolom dengan variasi nilai yang lebih sedikit.

Ringkasan

GROUP BY adalah fondasi untuk membuat laporan ringkas dari data mentah. Memahami aturan kolom non-agregat sejak awal akan mencegah error yang sering membingungkan pemula ketika pertama kali belajar agregasi data.

Kombinasi GROUP BY dengan ekspresi seperti YEAR() dan MONTH() membuka banyak kemungkinan laporan periodik yang sangat umum dibutuhkan di hampir setiap aplikasi bisnis.

Seri Belajar SQL Server:

← Sebelumnya: Memahami LEFT JOIN RIGHT JOIN dan FULL JOIN

→ Selanjutnya: Filtering Hasil Group dengan HAVING