Laporan bertingkat — subtotal per kota lalu total keseluruhan — biasanya dirakit dengan tiga query terpisah atau dioperasikan UNION manual. MySQL menyediakan jalan pintas yang melekat langsung pada GROUP BY: modifier WITH ROLLUP menambahkan baris super-agregat berisi subtotal dan grand total dalam satu eksekusi. Pada chapter ini kita membuat laporan bertingkat satu kolom dan dua kolom, membongkar jebakan utama teknik ini (nilai NULL penanda rollup yang menyamar seperti NULL biasa), lalu memakai fungsi GROUPING() untuk membedakan keduanya secara meyakinkan.
Menyiapkan Tabel Latihan #
Kita pakai tabel penjualan empat belas baris yang sama dengan seluruh chapter section ini. Nilai kolom kota dan produk dikombinasikan cukup variatif untuk menghasilkan struktur subtotal yang bermakna:
DROP DATABASE IF EXISTS belajar_agregasi;
CREATE DATABASE belajar_agregasi;
USE belajar_agregasi;Output yang Dihasilkan:
Query OK, 0 rows affected, 1 warning (0.00 sec)
Query OK, 1 row affected (0.00 sec)
Database changedCREATE 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:
Query OK, 0 rows affected (0.00 sec)
Query OK, 14 rows affected (0.00 sec)
Records: 14 Duplicates: 0 Warnings: 0Rollup Satu Kolom #
Query GROUP BY kota standar menghasilkan satu baris per kota. Menambahkan WITH ROLLUP di akhir klausa menambahkan satu baris ekstra: nilai kolom kota diganti NULL dan agregatnya berisi total seluruh kelompok:
SELECT kota, SUM(jumlah) AS total_unit
FROM penjualan
GROUP BY kota WITH ROLLUP;Output yang Dihasilkan:
+------------+------------+
| kota | total_unit |
+------------+------------+
| Bandung | 10 |
| Jakarta | 10 |
| Surabaya | 13 |
| Yogyakarta | 4 |
| NULL | 37 |
+------------+------------+
5 rows in set (0.00 sec)Baris terakhir dengan kota bernilai NULL adalah grand total — jumlah seluruh unit dari semua kota. Nilai NULL di sini bukan data kosong melainkan penanda bahwa baris tersebut merupakan baris super-agregat. Karena MySQL 8 tidak lagi mengurutkan hasil GROUP BY secara otomatis, baris rollup muncul pada posisi terakhir hasil di atas; urutan ini stabil untuk data kita, namun kode produksi sebaiknya tidak mengandalkan posisinya.
Rollup Dua Kolom: Subtotal Bertingkat #
Kekuatan sebenarnya WITH ROLLUP terlihat pada pengelompokan multi-kolom. Dengan GROUP BY kota, produk WITH ROLLUP, MySQL menghasilkan tiga tingkat agregasi: per kombinasi kota-produk, subtotal per kota (kolom produk menjadi NULL), dan grand total (kedua kolom NULL):
SELECT kota, produk, SUM(jumlah) AS total_unit
FROM penjualan
GROUP BY kota, produk WITH ROLLUP;Output yang Dihasilkan:
+------------+-------------+------------+
| kota | produk | total_unit |
+------------+-------------+------------+
| Bandung | Gula Aren | 4 |
| Bandung | Kopi Gayo | 1 |
| Bandung | Teh Melati | 5 |
| Bandung | NULL | 10 |
| Jakarta | Gula Aren | 2 |
| Jakarta | Kopi Gayo | 3 |
| Jakarta | Kopi Toraja | 4 |
| Jakarta | Teh Melati | 1 |
| Jakarta | NULL | 10 |
| Surabaya | Gula Aren | 6 |
| Surabaya | Kopi Toraja | 5 |
| Surabaya | Teh Melati | 2 |
| Surabaya | NULL | 13 |
| Yogyakarta | Kopi Gayo | 3 |
| Yogyakarta | Kopi Toraja | 1 |
| Yogyakarta | NULL | 4 |
| NULL | NULL | 37 |
+------------+-------------+------------+
17 rows in set (0.00 sec)Perhatikan pola barisnya: setelah baris-baris kombinasi Bandung dengan berbagai produk, muncul satu baris Bandung / NULL / 10 — subtotal kota Bandung; pola yang sama berulang untuk setiap kota; dan puncaknya satu baris NULL / NULL / 37 sebagai total keseluruhan. Tingkat subtotal mengikuti urutan kolom pada GROUP BY: subtotal per kota muncul karena kota adalah kolom pertama. Bila kita menulis GROUP BY produk, kota WITH ROLLUP, tingkat subtotal yang dihasilkan adalah per produk.
Membedakan NULL Rollup dari NULL Data #
Tabel latihan kita sengaja memiliki kolom pelanggan yang berisi NULL sungguhan pada enam baris. Keberadaan dua jenis NULL ini — penanda rollup dan data kosong — menjadi sumber kebingungan klasik WITH ROLLUP. Ambil laporan berikut yang mengelompokkan pelanggan:
SELECT pelanggan, SUM(jumlah) AS total_unit
FROM penjualan
GROUP BY pelanggan WITH ROLLUP;Output yang Dihasilkan:
+-----------+------------+
| pelanggan | total_unit |
+-----------+------------+
| NULL | 12 |
| Andi | 5 |
| Budi | 4 |
| Dewi | 1 |
| Rina | 8 |
| Siti | 7 |
| NULL | 37 |
+-----------+------------+
7 rows in set (0.00 sec)Dua baris bernilai NULL pada kolom pelanggan muncul: baris pertama adalah kelompok transaksi tunai (data asli tanpa nama pelanggan, total 12 unit), sedangkan baris terakhir adalah grand total (37 unit). Pemaca laporan yang teliti bisa membedakan keduanya dari nilai agregat, tetapi pembedaan berbasis tebakan angka rapuh untuk data besar. MySQL menyediakan fungsi khusus GROUPING(kolom) yang mengembalikan 1 ketika baris tersebut adalah baris super-agregat untuk kolom itu dan 0 untuk baris data biasa:
SELECT
COALESCE(pelanggan, 'TOTAL') AS pelanggan,
GROUPING(pelanggan) AS penanda_rollup,
SUM(jumlah) AS total_unit
FROM penjualan
GROUP BY pelanggan WITH ROLLUP;Output yang Dihasilkan:
+-----------+----------------+------------+
| pelanggan | penanda_rollup | total_unit |
+-----------+----------------+------------+
| TOTAL | 0 | 12 |
| Andi | 0 | 5 |
| Budi | 0 | 4 |
| Dewi | 0 | 1 |
| Rina | 0 | 8 |
| Siti | 0 | 7 |
| TOTAL | 1 | 37 |
+-----------+----------------+------------+
7 rows in set, 1 warning (0.00 sec)Hasilnya kini tegas: baris TOTAL membawa penanda 1 sedangkan baris transaksi tunai membawa 0. Kombinasi GROUPING() dengan COALESCE() — yang menggantikan NULL dengan label 'TOTAL' — menghasilkan kolom label yang aman langsung ditampilkan ke pembaca laporan tanpa risiko tertukar.
Batasan Praktis Lainnya #
Beberapa pembatasan lain patut diketahui sebelum memakai WITH ROLLUP di kode nyata. Pertama, LIMIT berlaku setelah baris rollup dibentuk, sehingga LIMIT 5 bisa memotong baris grand total — perlakuan yang biasanya tidak diinginkan pada laporan. Kedua, kedalaman rollup mengikuti jumlah kolom GROUP BY; laporan bertingkat lebih kompleks (misalnya per tanggal–kota–produk) tetap teknikal dapat dibuat, tetapi keterbacaannya cepat menurun dan cenderung lebih jelas bila dirakit dengan query terpisah atau UNION. Ketiga, rollup hanya tersedia untuk agregat klasik; kebutuhan agregat jendela berperingkat tetap harus memakai window function yang kita bahas pada section tersendiri.
Referensi #
- GROUP BY Modifiers — MySQL 8.4 Reference Manual
- Aggregate Function Descriptions — MySQL 8.4 Reference Manual
- SELECT Statement — MySQL 8.4 Reference Manual
Section agregasi data kini tuntas: dari COUNT tunggal sampai laporan bertingkat WITH ROLLUP. Perjalanan berikutnya membawa kita ke teknik yang mengubah cara data dari banyak tabel digabungkan — JOIN, dimulai dari konsep relasi antartabel.