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:

sql buat-tabel.sql
-- 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:

text
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:

text
  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:

PintasanSetara denganPola 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:

text
  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:

  1. Kolom kategori — grouping(kategori) = 1 hanya terjadi pada baris total, yang kemudian diberikan label TOTAL SEMUA; baris lain menampilkan nama kategori asli.
  2. Kolom kota — subtotal kategori (grouping(kota) = 1 tetapi grouping(kategori) = 0) diberi label * Subtotal *; baris total tidak masuk cabang ini karena grouping(kategori) sudah bernilai 1, sehingga kolomnya tetap kosong.
  3. 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 lewat grouping() 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:

text
  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)TingkatMakna baris
(0, 0, 0)detailsatu kombinasi kategori–kota–metode pembayaran
(0, 0, 1)subtotal kotatotal pasangan kategori–kota lintas metode
(0, 1, 1)subtotal kategoritotal satu kategori lintas kota dan metode
(1, 1, 1)total keseluruhanseluruh 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:

sql cube-dasar.sql
-- 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:

text
  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:

BandungJakartaSurabayaTotal kategori
Alat Tulis192.000240.00048.000480.000
Makanan625.000950.000300.0001.875.000
Minuman680.0001.275.0001.020.0002.975.000
Total kota1.497.0002.465.0001.368.0005.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:

text
 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 #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

buat-tabel.sql

Unduh berkas
sql
-- 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
sql
-- 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
sql
-- 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
sql
-- 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
sql
-- 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
sql
-- 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);