Sepanjang section ini satu pola kebutuhan muncul berulang kali dengan wajah berbeda: “hitung semua baris, tetapi untuk angka yang ini hanya baris QRIS”, “jumlahkan nilai per kota, tetapi kolomnya harus per kategori”, “berapa persentase transaksi tunai di tiap kota”. Kebutuhan itu — agregat yang hanya menelan baris pilihan — punya tiga penyelesaian yang telah kita sentuh satu per satu: CASE WHEN di dalam agregat, klausa FILTER, dan subquery dengan WHERE. Chapter penutup ini menarik ketiganya ke satu meja, membandingkan keterbacaan, portabilitas, dan perilaku mereka terhadap kelompok kosong, lalu merangkai teknik menjadi dua resep paling dicari di dunia laporan: pivot manual dan baris ringkasan satu query.

Tabel Latihan #

Tabel penjualan Toko Maju Jaya menemani kita terakhir kali — dua belas transaksi dengan kombinasi kategori, kota, dan metode pembayaran yang cukup beragam untuk semua demo:

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)

Karena chapter ini bersifat rangkuman, setiap teknik akan tetap diberi pengantar singkat — tetapi bila Anda melompat langsung ke sini, chapter FILTER Clause dan HAVING memuat bedah lengkapnya.

CASE WHEN di Dalam Agregat #

Pola tertuanya: bungkus kondisi dalam CASE, kembalikan nilai hanya pada baris yang cocok, dan biarkan agregat mengabaikan NULL sisanya. Aturan “agregat melewatkan NULL” dari chapter COUNT yang bekerja di balik layar:

-- CASE di dalam agregat: hitung nilai selain NULL saja

-- pivot hitungan: kategori menjadi kolom
SELECT kota,
       count(CASE WHEN kategori = 'Minuman' THEN 1 END)    AS minuman,
       count(CASE WHEN kategori = 'Makanan' THEN 1 END)    AS makanan,
       count(CASE WHEN kategori = 'Alat Tulis' THEN 1 END) AS alat_tulis
FROM penjualan
GROUP BY kota
ORDER BY kota;

-- tanpa ELSE: kelompok tanpa kecocokan menghasilkan NULL, bukan 0
SELECT kota,
       sum(CASE WHEN metode_bayar = 'transfer'
                THEN jumlah * harga_satuan END)        AS transfer_tanpa_else,
       sum(CASE WHEN metode_bayar = 'transfer'
                THEN jumlah * harga_satuan
                ELSE 0 END)                            AS transfer_dengan_else
FROM penjualan
GROUP BY kota
ORDER BY kota;

Output yang Dihasilkan:

text
   kota   | minuman | makanan | alat_tulis 
----------+---------+---------+------------
 Bandung  |       1 |       1 |          2
 Jakarta  |       2 |       2 |          1
 Surabaya |       1 |       1 |          1
(3 rows)

   kota   | transfer_tanpa_else | transfer_dengan_else 
----------+---------------------+----------------------
 Bandung  |          1305000.00 |           1305000.00
 Jakarta  |           500000.00 |            500000.00
 Surabaya |                     |                    0
(3 rows)

Query pertama menunjukkan pola count(CASE WHEN ... THEN 1 END): baris yang tidak cocok menghasilkan NULL dari CASE (karena tidak ada cabang ELSE), dan count mengabaikannya — Bandung terhitung satu transaksi Minuman, dua Alat Tulis. Query kedua membedah gotcha sum: Surabaya tidak punya transaksi transfer sama sekali, sehingga versi tanpa ELSE menjumlahkan kumpulan NULL dan menghasilkan NULL, sedangkan versi dengan ELSE 0 menghasilkan 0. Untuk laporan, ELSE 0 umumnya yang Anda mau — sel kosong harus bernilai nol, bukan NULL yang tampak seperti data hilang.

Tiga Pendekatan Berdampingan #

Pertanyaan yang sama — berapa transaksi QRIS per kategori? — dijawab tiga cara. Perhatikan hasilnya baris demi baris, karena ada satu perbedaan yang tidak tampak dari sintaks:

-- tiga cara menjawab pertanyaan yang sama: berapa transaksi qris per kategori?

-- cara 1: CASE di dalam agregat
SELECT kategori,
       count(CASE WHEN metode_bayar = 'qris' THEN 1 END) AS transaksi_qris
FROM penjualan
GROUP BY kategori
ORDER BY kategori;

-- cara 2: klausa FILTER
SELECT kategori,
       count(*) FILTER (WHERE metode_bayar = 'qris') AS transaksi_qris
FROM penjualan
GROUP BY kategori
ORDER BY kategori;

-- cara 3: subquery dengan WHERE (kelompok nol hilang!)
SELECT kategori,
       count(*) AS transaksi_qris
FROM penjualan
WHERE metode_bayar = 'qris'
GROUP BY kategori
ORDER BY kategori;

Output yang Dihasilkan:

text
  kategori  | transaksi_qris 
------------+----------------
 Alat Tulis |              2
 Makanan    |              0
 Minuman    |              2
(3 rows)

  kategori  | transaksi_qris 
------------+----------------
 Alat Tulis |              2
 Makanan    |              0
 Minuman    |              2
(3 rows)

  kategori  | transaksi_qris 
------------+----------------
 Alat Tulis |              2
 Minuman    |              2
(2 rows)

Cara 1 dan cara 2 menghasilkan tiga baris — Alat Tulis 2, Makanan 0, Minuman 2. Cara 3 hanya menghasilkan dua baris: Makanan hilang total. Mekanismenya lugas: subquery menyaring baris QRIS sebelum pengelompokan, dan kategori yang tidak memiliki satu pun baris QRIS tidak pernah sempat membentuk kelompok. CASE dan FILTER mengelompokkan dulu baru menyaring di dalam agregat, sehingga kelompok kosong tetap hadir dengan angka nol. Perbandingan menyeluruh ketiganya:

AspekCASE di agregatFILTERSubquery + WHERE
Keterbacaanbaik bila kondisi pendek; memburuk saat bertingkatpaling ringkas, kondisi berdiri sendiripaling intuitif, seperti query biasa
Status standarSQL lama, didukung semua databaseSQL:2003; PostgreSQL 9.4+SQL lama, didukung semua database
Satu pemindaianya — beberapa agregat sekali jalanya — beberapa agregat sekali jalantidak — satu query per angka
Kelompok kosongtetap tampil, bernilai 0tetap tampil, bernilai 0hilang tanpa jejak
Cocok untukportabilitas maksimum, database lamadefault di PostgreSQL modernlaporan satu kondisi tanpa kelompok nol

Pivot Manual: Baris Menjadi Kolom #

Pivot (atau crosstab) mengubah data memanjang menjadi matriks: nilai unik satu kolom dijadikan deretan kolom. SQL standar tidak punya klausa PIVOT seperti milik beberapa database lain, tetapi agregasi bersyarat menuntaskan polanya dengan elegan — setiap kolom hasil adalah satu agregat bersyarat:

-- pivot manual: baris menjadi kolom lewat agregasi bersyarat

-- hitungan transaksi: kota sebagai baris, kategori sebagai kolom
SELECT kota,
       count(CASE WHEN kategori = 'Minuman' THEN 1 END)    AS minuman,
       count(CASE WHEN kategori = 'Makanan' THEN 1 END)    AS makanan,
       count(CASE WHEN kategori = 'Alat Tulis' THEN 1 END) AS alat_tulis,
       count(*) AS total
FROM penjualan
GROUP BY kota
ORDER BY kota;

-- nilai rupiah: ELSE 0 menjaga sel yang kosong tetap bernilai nol
SELECT kota,
       sum(CASE WHEN kategori = 'Minuman'
                THEN jumlah * harga_satuan ELSE 0 END)    AS minuman,
       sum(CASE WHEN kategori = 'Makanan'
                THEN jumlah * harga_satuan ELSE 0 END)    AS makanan,
       sum(CASE WHEN kategori = 'Alat Tulis'
                THEN jumlah * harga_satuan ELSE 0 END)    AS alat_tulis,
       sum(jumlah * harga_satuan) AS total
FROM penjualan
GROUP BY kota
ORDER BY kota;

Output yang Dihasilkan:

text
   kota   | minuman | makanan | alat_tulis | total 
----------+---------+---------+------------+-------
 Bandung  |       1 |       1 |          2 |     4
 Jakarta  |       2 |       2 |          1 |     5
 Surabaya |       1 |       1 |          1 |     3
(3 rows)

   kota   |  minuman   |  makanan  | alat_tulis |   total    
----------+------------+-----------+------------+------------
 Bandung  |  680000.00 | 625000.00 |  192000.00 | 1497000.00
 Jakarta  | 1275000.00 | 950000.00 |  240000.00 | 2465000.00
 Surabaya | 1020000.00 | 300000.00 |   48000.00 | 1368000.00
(3 rows)

Query pertama memutar hitungan transaksi: tiga baris kota, tiga kolom kategori, satu kolom total tanpa kondisi sebagai penjaga. Query kedua memutar nilai rupiah — matriks 3×3 lengkap dengan total barisnya: Minuman Jakarta 1.275.000, Makanan Bandung 625.000, dan seterusnya, berujung total keseluruhan 5.330.000. Resepnya berhenti di tiga langkah: (1) pilih kolom pemutar untuk GROUP BY — itu barisnya; (2) satu agregat bersyarat per nilai yang ingin menjadi kolom; (3) ELSE 0 pada sum agar sel kosong bernilai nol. Batas pola ini juga nyata: daftar kolom ditulis tangan, sehingga kategori baru menuntut query disunting — pivot statis, bukan dinamis.

Resep Praktis: Laporan Satu Query #

Penutup yang mempertemukan semuanya — satu query laporan per kota yang memuat agregat polos, FILTER, round, dan aritmetika antar agregat dalam satu SELECT:

-- resep laporan satu query: FILTER + round + aritmetika antar agregat

SELECT kota,
       count(*) AS transaksi,
       count(*) FILTER (WHERE metode_bayar = 'qris') AS transaksi_qris,
       round(100.0 * count(*) FILTER (WHERE metode_bayar = 'qris') / count(*), 1)
           AS persen_qris,
       sum(jumlah * harga_satuan) AS total_nilai,
       round(avg(jumlah), 2) AS rata_item
FROM penjualan
GROUP BY kota
ORDER BY kota;

Output yang Dihasilkan:

text
   kota   | transaksi | transaksi_qris | persen_qris | total_nilai | rata_item 
----------+-----------+----------------+-------------+-------------+-----------
 Bandung  |         4 |              2 |        50.0 |  1497000.00 |     14.25
 Jakarta  |         5 |              1 |        20.0 |  2465000.00 |     16.60
 Surabaya |         3 |              1 |        33.3 |  1368000.00 |     10.00
(3 rows)

Enam kolom dari satu kali pemindaian: jumlah transaksi, irisan QRIS-nya, persentase QRIS (100.0 * mendorong pembagian ke tipe numer agar desimalnya tidak terpotong), total nilai rupiah, dan rata-rata item. Bandung mencatat 50% transaksi QRIS sementara Jakarta 20% — angka yang akan sulit dibaca bila diambil dari query terpisah. Kolom persen_qris juga mengingatkan aturan GROUP BY dari awal section: setiap kolom non-agregat harus masuk GROUP BY, dan sisanya dibangun dari ekspresi agregat.

Agregasi data kini tuntas sebagai satu kotak perkakas: dari count dan sum di chapter-chapter awal, GROUP BY/HAVING untuk mengelompokkan dan menyaring, FILTER serta CASE untuk agregasi bersyarat, hingga GROUPING SETS, ROLLUP, dan CUBE untuk subtotal multidimensi. Tetapi semua latihan kita selama ini memakai satu tabel — padahal kekuatan sejati database relational justru muncul saat data dari beberapa tabel digabungkan. Section berikutnya membuka wilayah itu: JOIN .

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;

case-dalam-agregat.sql

Unduh berkas
sql
-- CASE di dalam agregat: hitung nilai selain NULL saja

-- pivot hitungan: kategori menjadi kolom
SELECT kota,
       count(CASE WHEN kategori = 'Minuman' THEN 1 END)    AS minuman,
       count(CASE WHEN kategori = 'Makanan' THEN 1 END)    AS makanan,
       count(CASE WHEN kategori = 'Alat Tulis' THEN 1 END) AS alat_tulis
FROM penjualan
GROUP BY kota
ORDER BY kota;

-- tanpa ELSE: kelompok tanpa kecocokan menghasilkan NULL, bukan 0
SELECT kota,
       sum(CASE WHEN metode_bayar = 'transfer'
                THEN jumlah * harga_satuan END)        AS transfer_tanpa_else,
       sum(CASE WHEN metode_bayar = 'transfer'
                THEN jumlah * harga_satuan
                ELSE 0 END)                            AS transfer_dengan_else
FROM penjualan
GROUP BY kota
ORDER BY kota;

pivot-manual.sql

Unduh berkas
sql
-- pivot manual: baris menjadi kolom lewat agregasi bersyarat

-- hitungan transaksi: kota sebagai baris, kategori sebagai kolom
SELECT kota,
       count(CASE WHEN kategori = 'Minuman' THEN 1 END)    AS minuman,
       count(CASE WHEN kategori = 'Makanan' THEN 1 END)    AS makanan,
       count(CASE WHEN kategori = 'Alat Tulis' THEN 1 END) AS alat_tulis,
       count(*) AS total
FROM penjualan
GROUP BY kota
ORDER BY kota;

-- nilai rupiah: ELSE 0 menjaga sel yang kosong tetap bernilai nol
SELECT kota,
       sum(CASE WHEN kategori = 'Minuman'
                THEN jumlah * harga_satuan ELSE 0 END)    AS minuman,
       sum(CASE WHEN kategori = 'Makanan'
                THEN jumlah * harga_satuan ELSE 0 END)    AS makanan,
       sum(CASE WHEN kategori = 'Alat Tulis'
                THEN jumlah * harga_satuan ELSE 0 END)    AS alat_tulis,
       sum(jumlah * harga_satuan) AS total
FROM penjualan
GROUP BY kota
ORDER BY kota;

resep-praktis.sql

Unduh berkas
sql
-- resep laporan satu query: FILTER + round + aritmetika antar agregat

SELECT kota,
       count(*) AS transaksi,
       count(*) FILTER (WHERE metode_bayar = 'qris') AS transaksi_qris,
       round(100.0 * count(*) FILTER (WHERE metode_bayar = 'qris') / count(*), 1)
           AS persen_qris,
       sum(jumlah * harga_satuan) AS total_nilai,
       round(avg(jumlah), 2) AS rata_item
FROM penjualan
GROUP BY kota
ORDER BY kota;

tiga-pendekatan.sql

Unduh berkas
sql
-- tiga cara menjawab pertanyaan yang sama: berapa transaksi qris per kategori?

-- cara 1: CASE di dalam agregat
SELECT kategori,
       count(CASE WHEN metode_bayar = 'qris' THEN 1 END) AS transaksi_qris
FROM penjualan
GROUP BY kategori
ORDER BY kategori;

-- cara 2: klausa FILTER
SELECT kategori,
       count(*) FILTER (WHERE metode_bayar = 'qris') AS transaksi_qris
FROM penjualan
GROUP BY kategori
ORDER BY kategori;

-- cara 3: subquery dengan WHERE (kelompok nol hilang!)
SELECT kategori,
       count(*) AS transaksi_qris
FROM penjualan
WHERE metode_bayar = 'qris'
GROUP BY kategori
ORDER BY kategori;