Subtotal bertingkat adalah nyawa laporan keuangan: baris detail per cabang, subtotal per wilayah, total perusahaan di kaki lembar. Chapter sebelumnya menunjukkan bahwa GROUPING SETS sanggup menghasilkan semua level itu dalam satu query — tetapi menulis daftar set ((a, b), (a), ()) untuk setiap laporan bertingkat cepat terasa ritualistis. PostgreSQL menyediakan dua pintasan untuk dua pola paling umum: ROLLUP untuk hierarki subtotal yang menyempit selangkah demi selangkah, dan CUBE untuk semua kombinasi kolom sekaligus — bahan baku tabel silang. Keduanya adalah gula sintaksis di atas GROUPING SETS yang sama, jadi seluruh pengetahuan Anda tentang NULL subtotal dan fungsi grouping() langsung berlaku.
Tabel Latihan #
Kita tutup rangkaian latihan dengan tabel penjualan Toko Maju Jaya yang sama — dua belas transaksi, tiga kategori, tiga kota:
-- tabel latihan: penjualan Toko Maju Jaya, Januari-Februari 2026
CREATE TABLE penjualan (
id integer PRIMARY KEY,
tanggal date NOT NULL,
kategori text NOT NULL,
produk text NOT NULL,
kota text NOT NULL,
jumlah integer NOT NULL,
harga_satuan numeric(12,2) NOT NULL,
metode_bayar text
);
INSERT INTO penjualan (id, tanggal, kategori, produk, kota, jumlah, harga_satuan, metode_bayar) VALUES
( 1, DATE '2026-01-05', 'Minuman', 'Kopi Gayo', 'Jakarta', 10, 85000.00, 'tunai'),
( 2, DATE '2026-01-08', 'Makanan', 'Biskuit', 'Jakarta', 20, 25000.00, 'transfer'),
( 3, DATE '2026-01-12', 'Alat Tulis', 'Pulpen', 'Bandung', 15, 8000.00, 'qris'),
( 4, DATE '2026-01-15', 'Minuman', 'Kopi Gayo', 'Bandung', 8, 85000.00, 'transfer'),
( 5, DATE '2026-01-20', 'Makanan', 'Biskuit', 'Surabaya', 12, 25000.00, NULL),
( 6, DATE '2026-01-25', 'Alat Tulis', 'Pulpen', 'Jakarta', 30, 8000.00, 'tunai'),
( 7, DATE '2026-02-02', 'Minuman', 'Kopi Gayo', 'Surabaya', 12, 85000.00, 'qris'),
( 8, DATE '2026-02-06', 'Makanan', 'Biskuit', 'Bandung', 25, 25000.00, 'transfer'),
( 9, DATE '2026-02-10', 'Alat Tulis', 'Pulpen', 'Surabaya', 6, 8000.00, NULL),
(10, DATE '2026-02-14', 'Minuman', 'Kopi Gayo', 'Jakarta', 5, 85000.00, 'qris'),
(11, DATE '2026-02-18', 'Makanan', 'Biskuit', 'Jakarta', 18, 25000.00, 'tunai'),
(12, DATE '2026-02-24', 'Alat Tulis', 'Pulpen', 'Bandung', 9, 8000.00, 'qris');
SELECT id, tanggal, kategori, produk, kota, jumlah, harga_satuan, metode_bayar
FROM penjualan
ORDER BY id;Output yang Dihasilkan:
CREATE TABLE
INSERT 0 12
id | tanggal | kategori | produk | kota | jumlah | harga_satuan | metode_bayar
----+------------+------------+-----------+----------+--------+--------------+--------------
1 | 2026-01-05 | Minuman | Kopi Gayo | Jakarta | 10 | 85000.00 | tunai
2 | 2026-01-08 | Makanan | Biskuit | Jakarta | 20 | 25000.00 | transfer
3 | 2026-01-12 | Alat Tulis | Pulpen | Bandung | 15 | 8000.00 | qris
4 | 2026-01-15 | Minuman | Kopi Gayo | Bandung | 8 | 85000.00 | transfer
5 | 2026-01-20 | Makanan | Biskuit | Surabaya | 12 | 25000.00 |
6 | 2026-01-25 | Alat Tulis | Pulpen | Jakarta | 30 | 8000.00 | tunai
7 | 2026-02-02 | Minuman | Kopi Gayo | Surabaya | 12 | 85000.00 | qris
8 | 2026-02-06 | Makanan | Biskuit | Bandung | 25 | 25000.00 | transfer
9 | 2026-02-10 | Alat Tulis | Pulpen | Surabaya | 6 | 8000.00 |
10 | 2026-02-14 | Minuman | Kopi Gayo | Jakarta | 5 | 85000.00 | qris
11 | 2026-02-18 | Makanan | Biskuit | Jakarta | 18 | 25000.00 | tunai
12 | 2026-02-24 | Alat Tulis | Pulpen | Bandung | 9 | 8000.00 | qris
(12 rows)Jika Anda datang langsung ke chapter ini, kembali sebentar ke Grouping Sets
untuk konsep NULL subtotal dan grouping() — dua chapter ini sebaiknya dibaca berurutan.
ROLLUP: Subtotal Bertingkat dalam Rupiah #
ROLLUP (a, b) adalah singkatan dari GROUPING SETS ((a, b), (a), ()) — hierarki yang terbentuk dengan memangkas satu kolom dari kanan pada setiap tingkat:
-- ROLLUP: subtotal bertingkat dari hierarki kategori -> kota
SELECT kategori,
kota,
count(*) AS transaksi,
sum(jumlah * harga_satuan) AS total_nilai
FROM penjualan
GROUP BY ROLLUP (kategori, kota)
ORDER BY kategori NULLS LAST, kota NULLS LAST;Output yang Dihasilkan:
kategori | kota | transaksi | total_nilai
------------+----------+-----------+-------------
Alat Tulis | Bandung | 2 | 192000.00
Alat Tulis | Jakarta | 1 | 240000.00
Alat Tulis | Surabaya | 1 | 48000.00
Alat Tulis | | 4 | 480000.00
Makanan | Bandung | 1 | 625000.00
Makanan | Jakarta | 2 | 950000.00
Makanan | Surabaya | 1 | 300000.00
Makanan | | 4 | 1875000.00
Minuman | Bandung | 1 | 680000.00
Minuman | Jakarta | 2 | 1275000.00
Minuman | Surabaya | 1 | 1020000.00
Minuman | | 4 | 2975000.00
| | 12 | 5330000.00
(13 rows)Tiga belas baris hasil — identik dengan query GROUPING SETS ((kategori, kota), (kategori), ()) di chapter sebelumnya, karena memang keduanya query yang sama. Baca arah uangnya: sembilan baris detail (Alat Tulis–Bandung 192.000 rupiah dan seterusnya), tiga subtotal kategori (Alat Tulis 480.000, Makanan 1.875.000, Minuman 2.975.000), dan total keseluruhan 5.330.000 rupiah yang menjumlah semua level di atasnya. Urutan ORDER BY kategori NULLS LAST, kota NULLS LAST menaruh setiap subtotal di ujung blok kategorinya dan total di baris terakhir — tata letak subtotal yang wajar tanpa kerja ekstra.
Pemetaan lengkap kedua pintasan terhadap GROUPING SETS:
| Pintasan | Setara dengan | Pola yang dihasilkan |
|---|---|---|
ROLLUP (a, b) | GROUPING SETS ((a, b), (a), ()) | hierarki menyempit: detail → subtotal a → total |
ROLLUP (a, b, c) | GROUPING SETS ((a, b, c), (a, b), (a), ()) | hierarki empat tingkat |
CUBE (a, b) | GROUPING SETS ((a, b), (a), (b), ()) | semua kombinasi kolom |
CUBE (a, b, c) | kombinasi 8 set: (a,b,c), (a,b), (a,c), (b,c), (a), (b), (c), () | semua kombinasi — barisnya banyak |
Label Subtotal yang Layak Baca #
NULL sebagai penanda subtotal bekerja, tetapi laporan yang ditujukan untuk manusia butuh label. Resepnya menggabungkan CASE, grouping(), dan ORDER BY berbasis grouping() — semuanya bahan dari chapter Grouping Sets
:
-- label subtotal yang enak dibaca: grouping() memandu CASE dan pengurutan
SELECT CASE WHEN grouping(kategori) = 1
THEN 'TOTAL SEMUA'
ELSE kategori
END AS kategori,
CASE WHEN grouping(kategori) = 0 AND grouping(kota) = 1
THEN '* Subtotal *'
ELSE kota
END AS kota,
count(*) AS transaksi,
sum(jumlah * harga_satuan) AS total_nilai
FROM penjualan
GROUP BY ROLLUP (kategori, kota)
ORDER BY grouping(kategori), kategori, grouping(kota), kota;Output yang Dihasilkan:
kategori | kota | transaksi | total_nilai
-------------+--------------+-----------+-------------
Alat Tulis | Bandung | 2 | 192000.00
Alat Tulis | Jakarta | 1 | 240000.00
Alat Tulis | Surabaya | 1 | 48000.00
Alat Tulis | * Subtotal * | 4 | 480000.00
Makanan | Bandung | 1 | 625000.00
Makanan | Jakarta | 2 | 950000.00
Makanan | Surabaya | 1 | 300000.00
Makanan | * Subtotal * | 4 | 1875000.00
Minuman | Bandung | 1 | 680000.00
Minuman | Jakarta | 2 | 1275000.00
Minuman | Surabaya | 1 | 1020000.00
Minuman | * Subtotal * | 4 | 2975000.00
TOTAL SEMUA | | 12 | 5330000.00
(13 rows)Bedah tiga komponennya:
- Kolom
kategori—grouping(kategori) = 1hanya terjadi pada baris total, yang kemudian diberikan labelTOTAL SEMUA; baris lain menampilkan nama kategori asli. - Kolom
kota— subtotal kategori (grouping(kota) = 1tetapigrouping(kategori) = 0) diberi label* Subtotal *; baris total tidak masuk cabang ini karenagrouping(kategori)sudah bernilai 1, sehingga kolomnya tetap kosong. ORDER BY grouping(kategori), kategori, grouping(kota), kota— mengurutkan berdasarkan level dulu baru isi kolom: semua baris level-detail dan subtotal kategori (flag 0) tampil lebih dulu, disusul baris total (flag 1). Mengurutkan lewatgrouping()lebih tegas daripada mengandalkan urutan NULL karena urutan level menjadi eksplisit.
ROLLUP Tiga Kolom: Empat Tingkat Sekaligus #
Kekuatan ROLLUP makin terasa ketika hierarki memanjang. Query berikut membatasi data ke Februari demi menjaga hasil tetap ringkas, lalu menggulung tiga kolom — kategori → kota → metode pembayaran:
-- ROLLUP tiga kolom: kategori -> kota -> metode pembayaran (hanya Februari)
SELECT kategori,
kota,
metode_bayar,
grouping(kategori) AS g_kategori,
grouping(kota) AS g_kota,
grouping(metode_bayar) AS g_metode,
count(*) AS transaksi
FROM penjualan
WHERE tanggal >= DATE '2026-02-01'
GROUP BY ROLLUP (kategori, kota, metode_bayar)
ORDER BY kategori NULLS LAST, kota NULLS LAST, metode_bayar NULLS LAST,
grouping(metode_bayar);Output yang Dihasilkan:
kategori | kota | metode_bayar | g_kategori | g_kota | g_metode | transaksi
------------+----------+--------------+------------+--------+----------+-----------
Alat Tulis | Bandung | qris | 0 | 0 | 0 | 1
Alat Tulis | Bandung | | 0 | 0 | 1 | 1
Alat Tulis | Surabaya | | 0 | 0 | 0 | 1
Alat Tulis | Surabaya | | 0 | 0 | 1 | 1
Alat Tulis | | | 0 | 1 | 1 | 2
Makanan | Bandung | transfer | 0 | 0 | 0 | 1
Makanan | Bandung | | 0 | 0 | 1 | 1
Makanan | Jakarta | tunai | 0 | 0 | 0 | 1
Makanan | Jakarta | | 0 | 0 | 1 | 1
Makanan | | | 0 | 1 | 1 | 2
Minuman | Jakarta | qris | 0 | 0 | 0 | 1
Minuman | Jakarta | | 0 | 0 | 1 | 1
Minuman | Surabaya | qris | 0 | 0 | 0 | 1
Minuman | Surabaya | | 0 | 0 | 1 | 1
Minuman | | | 0 | 1 | 1 | 2
| | | 1 | 1 | 1 | 6
(16 rows)Enam belas baris hasil tersebar pada empat tingkat yang tandai tiga bendera grouping():
(g_kategori, g_kota, g_metode) | Tingkat | Makna baris |
|---|---|---|
(0, 0, 0) | detail | satu kombinasi kategori–kota–metode pembayaran |
(0, 0, 1) | subtotal kota | total pasangan kategori–kota lintas metode |
(0, 1, 1) | subtotal kategori | total satu kategori lintas kota dan metode |
(1, 1, 1) | total keseluruhan | seluruh penjualan Februari (6 transaksi) |
CUBE: Semua Kombinasi, Bahan Baku Tabel Silang #
CUBE (a, b) meminta semua kombinasi: (a, b), (a), (b), dan (). Selisih satu set itu kelihatannya kecil, tetapi mengubah watak hasilnya — ROLLUP menghasilkan laporan bertingkat satu arah, CUBE menghasilkan matriks:
-- CUBE: semua kombinasi pengelompokan, bahan baku tabel silang
SELECT kategori,
kota,
count(*) AS transaksi,
sum(jumlah * harga_satuan) AS total_nilai
FROM penjualan
GROUP BY CUBE (kategori, kota)
ORDER BY kategori NULLS LAST, kota NULLS LAST;Output yang Dihasilkan:
kategori | kota | transaksi | total_nilai
------------+----------+-----------+-------------
Alat Tulis | Bandung | 2 | 192000.00
Alat Tulis | Jakarta | 1 | 240000.00
Alat Tulis | Surabaya | 1 | 48000.00
Alat Tulis | | 4 | 480000.00
Makanan | Bandung | 1 | 625000.00
Makanan | Jakarta | 2 | 950000.00
Makanan | Surabaya | 1 | 300000.00
Makanan | | 4 | 1875000.00
Minuman | Bandung | 1 | 680000.00
Minuman | Jakarta | 2 | 1275000.00
Minuman | Surabaya | 1 | 1020000.00
Minuman | | 4 | 2975000.00
| Bandung | 4 | 1497000.00
| Jakarta | 5 | 2465000.00
| Surabaya | 3 | 1368000.00
| | 12 | 5330000.00
(16 rows)Enam belas baris: tiga belas baris yang sama dengan ROLLUP plus tiga baris baru — total per kota (kategori NULL). Susun ulang angka itu sebagai matriks dan watak cross-tab-nya langsung terlihat:
| Bandung | Jakarta | Surabaya | Total kategori | |
|---|---|---|---|---|
| Alat Tulis | 192.000 | 240.000 | 48.000 | 480.000 |
| Makanan | 625.000 | 950.000 | 300.000 | 1.875.000 |
| Minuman | 680.000 | 1.275.000 | 1.020.000 | 2.975.000 |
| Total kota | 1.497.000 | 2.465.000 | 1.368.000 | 5.330.000 |
Sel-sel isi matriks berasal dari set (kategori, kota), baris “Total kategori” dari set (kategori), kolom “Total kota” dari set (kota) yang hanya disediakan CUBE, dan pojok kanan bawah dari set (). Satu query CUBE adalah seluruh isi matriks itu — laporan silang tinggal urusan menata ulang baris dan kolomnya di sisi penyaji.
Menambang Satu Level dari Hasil CUBE #
Hasil CUBE kadang berlebihan: Anda hanya butuh satu level darinya. Fungsi grouping() bisa menyaring level tepat di klausa HAVING:
-- menambang satu level dari hasil CUBE dengan grouping() di HAVING
-- ambil baris total per kota: kategori tidak dikelompokkan, kota dikelompokkan
SELECT kategori,
kota,
sum(jumlah * harga_satuan) AS total_nilai
FROM penjualan
GROUP BY CUBE (kategori, kota)
HAVING grouping(kategori) = 1 AND grouping(kota) = 0
ORDER BY kota;Output yang Dihasilkan:
kategori | kota | total_nilai
----------+----------+-------------
| Bandung | 1497000.00
| Jakarta | 2465000.00
| Surabaya | 1368000.00
(3 rows)Kondisi grouping(kategori) = 1 AND grouping(kota) = 0 menyaring kombinasi bendera (1, 0) — baris total per kota — sehingga hasilnya tiga baris bersih tanpa detail maupun total keseluruhan. Teknik penulisan kondisinya penting: grouping() boleh dipakai di HAVING dan ORDER BY, tetapi PostgreSQL menolaknya di WHERE dengan pesan grouping operations are not allowed in WHERE, karena WHERE dievaluasi sebelum pengelompokan terjadi. Bila Anda lebih nyaman memakai WHERE, bungkus query CUBE sebagai subquery, proyeksikan bendera grouping() sebagai kolom, lalu saring kolomnya di query luar.
Soal performa: seperti dibandingkan di chapter Grouping Sets
, satu query ROLLUP/CUBE menggantikan UNION ALL beberapa query — tabel dipindai sekali untuk semua level, bukan sekali per level. Pada tabel kecil perbedaannya kosmetik; pada tabel laporan besar perbedaannya bisa menjadi beda antara laporan yang siap pagi dan yang belum selesai. Analisis rencana eksekusi untuk membuktikannya sendiri menunggu section Query Optimization.
ROLLUP dan CUBE melengkapi kotak perkakas agregasi Anda: fungsi agregat dari awal section, GROUP BY dan HAVING untuk mengelompokkan dan menyaring, FILTER untuk agregasi selektif, dan sekarang subtotal multidimensi. Tinggal satu benang merah yang belum ditarik simpulnya: tiga gaya agregasi bersyarat — CASE WHEN, FILTER, dan subquery — dalam satu perbandingan, plus pola pivot yang mengubah baris menjadi kolom. Chapter penutup section ini merangkai semuanya: Agregasi dengan Kondisi
.
Referensi #
- SELECT (SQL Reference) — sintaks ROLLUP, CUBE, dan GROUPING SETS beserta padanannya.
- Aggregate Functions
— dokumentasi fungsi
grouping()pada tabel Grouping Operations.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
buat-tabel.sql
Unduh berkas-- tabel latihan: penjualan Toko Maju Jaya, Januari-Februari 2026
CREATE TABLE penjualan (
id integer PRIMARY KEY,
tanggal date NOT NULL,
kategori text NOT NULL,
produk text NOT NULL,
kota text NOT NULL,
jumlah integer NOT NULL,
harga_satuan numeric(12,2) NOT NULL,
metode_bayar text
);
INSERT INTO penjualan (id, tanggal, kategori, produk, kota, jumlah, harga_satuan, metode_bayar) VALUES
( 1, DATE '2026-01-05', 'Minuman', 'Kopi Gayo', 'Jakarta', 10, 85000.00, 'tunai'),
( 2, DATE '2026-01-08', 'Makanan', 'Biskuit', 'Jakarta', 20, 25000.00, 'transfer'),
( 3, DATE '2026-01-12', 'Alat Tulis', 'Pulpen', 'Bandung', 15, 8000.00, 'qris'),
( 4, DATE '2026-01-15', 'Minuman', 'Kopi Gayo', 'Bandung', 8, 85000.00, 'transfer'),
( 5, DATE '2026-01-20', 'Makanan', 'Biskuit', 'Surabaya', 12, 25000.00, NULL),
( 6, DATE '2026-01-25', 'Alat Tulis', 'Pulpen', 'Jakarta', 30, 8000.00, 'tunai'),
( 7, DATE '2026-02-02', 'Minuman', 'Kopi Gayo', 'Surabaya', 12, 85000.00, 'qris'),
( 8, DATE '2026-02-06', 'Makanan', 'Biskuit', 'Bandung', 25, 25000.00, 'transfer'),
( 9, DATE '2026-02-10', 'Alat Tulis', 'Pulpen', 'Surabaya', 6, 8000.00, NULL),
(10, DATE '2026-02-14', 'Minuman', 'Kopi Gayo', 'Jakarta', 5, 85000.00, 'qris'),
(11, DATE '2026-02-18', 'Makanan', 'Biskuit', 'Jakarta', 18, 25000.00, 'tunai'),
(12, DATE '2026-02-24', 'Alat Tulis', 'Pulpen', 'Bandung', 9, 8000.00, 'qris');
SELECT id, tanggal, kategori, produk, kota, jumlah, harga_satuan, metode_bayar
FROM penjualan
ORDER BY id;
cube-dasar.sql
Unduh berkas-- CUBE: semua kombinasi pengelompokan, bahan baku tabel silang
SELECT kategori,
kota,
count(*) AS transaksi,
sum(jumlah * harga_satuan) AS total_nilai
FROM penjualan
GROUP BY CUBE (kategori, kota)
ORDER BY kategori NULLS LAST, kota NULLS LAST;
cube-ekstrak.sql
Unduh berkas-- menambang satu level dari hasil CUBE dengan grouping() di HAVING
-- ambil baris total per kota: kategori tidak dikelompokkan, kota dikelompokkan
SELECT kategori,
kota,
sum(jumlah * harga_satuan) AS total_nilai
FROM penjualan
GROUP BY CUBE (kategori, kota)
HAVING grouping(kategori) = 1 AND grouping(kota) = 0
ORDER BY kota;
label-subtotal.sql
Unduh berkas-- label subtotal yang enak dibaca: grouping() memandu CASE dan pengurutan
SELECT CASE WHEN grouping(kategori) = 1
THEN 'TOTAL SEMUA'
ELSE kategori
END AS kategori,
CASE WHEN grouping(kategori) = 0 AND grouping(kota) = 1
THEN '* Subtotal *'
ELSE kota
END AS kota,
count(*) AS transaksi,
sum(jumlah * harga_satuan) AS total_nilai
FROM penjualan
GROUP BY ROLLUP (kategori, kota)
ORDER BY grouping(kategori), kategori, grouping(kota), kota;
rollup-dasar.sql
Unduh berkas-- ROLLUP: subtotal bertingkat dari hierarki kategori -> kota
SELECT kategori,
kota,
count(*) AS transaksi,
sum(jumlah * harga_satuan) AS total_nilai
FROM penjualan
GROUP BY ROLLUP (kategori, kota)
ORDER BY kategori NULLS LAST, kota NULLS LAST;
rollup-tiga-kolom.sql
Unduh berkas-- ROLLUP tiga kolom: kategori -> kota -> metode pembayaran (hanya Februari)
SELECT kategori,
kota,
metode_bayar,
grouping(kategori) AS g_kategori,
grouping(kota) AS g_kota,
grouping(metode_bayar) AS g_metode,
count(*) AS transaksi
FROM penjualan
WHERE tanggal >= DATE '2026-02-01'
GROUP BY ROLLUP (kategori, kota, metode_bayar)
ORDER BY kategori NULLS LAST, kota NULLS LAST, metode_bayar NULLS LAST,
grouping(metode_bayar);