Agregat murni seperti yang kita pakai di empat chapter sebelumnya menelan seluruh tabel dan mengembalikan satu baris — tetapi laporan nyata jarang berbentuk “total perusahaan” saja; hampir selalu “total per penjual”, “per kategori”, “per bulan”. Klausa GROUP BY adalah jembatannya: baris-baris dikemas dulu menjadi kelompok menurut satu atau beberapa kolom, lalu agregat berjalan di dalam tiap kelompok, dan setiap kelompok menyumbang tepat satu baris hasil. Konsep sederhana ini adalah pintu masuk analisis data di SQL — sekaligus sumber error paling tersohor: column must appear in the GROUP BY clause. Chapter ini membedah mekanisme pengelompokannya, aturan kolom SELECT-nya yang ketat, pengelompokan dengan ekspresi, dan perilaku NULL yang menjadi kejutannya sendiri.
Tabel Latihan: penjualan #
Masih dengan domain yang sama untuk seluruh section: 12 transaksi Toko Maju Jaya sepanjang Januari–Februari 2026. kategori kosong pada 2 baris dan diskon kosong pada 6 baris — keduanya akan berperan lagi di paruh akhir chapter:
-- Tabel latihan utama Section Agregasi Data:
-- transaksi penjualan Toko Maju Jaya, Januari-Februari 2026.
CREATE TABLE penjualan (
id integer PRIMARY KEY,
tanggal date NOT NULL,
penjual text NOT NULL,
kategori text, -- NULL = transaksi belum dikategorikan
produk text NOT NULL,
jumlah integer NOT NULL, -- unit terjual
harga numeric(10,2) NOT NULL, -- harga satuan, rupiah
diskon numeric(5,2) -- NULL = tanpa diskon
);
INSERT INTO penjualan (id, tanggal, penjual, kategori, produk, jumlah, harga, diskon) VALUES
( 1, DATE '2026-01-05', 'Budi', 'Kopi', 'Kopi Gayo', 10, 85000.00, 5.00),
( 2, DATE '2026-01-08', 'Siti', 'Teh', 'Teh Melati', 25, 35000.00, NULL),
( 3, DATE '2026-01-12', 'Andi', 'Gula', 'Gula Merah', 40, 15000.00, NULL),
( 4, DATE '2026-01-15', 'Budi', 'Kopi', 'Kopi Toraja', 8, 90000.00, 10.00),
( 5, DATE '2026-01-20', 'Dewi', NULL, 'Kopi Gayo', 12, 85000.00, NULL),
( 6, DATE '2026-01-22', 'Siti', 'Teh', 'Teh Hijau', 15, 40000.00, 7.50),
( 7, DATE '2026-01-25', 'Andi', 'Gula', 'Gula Merah', 30, 15000.00, 2.50),
( 8, DATE '2026-02-02', 'Budi', NULL, 'Teh Melati', 20, 35000.00, NULL),
( 9, DATE '2026-02-05', 'Dewi', 'Kopi', 'Kopi Gayo', 18, 85000.00, 8.00),
(10, DATE '2026-02-10', 'Siti', 'Teh', 'Teh Melati', 22, 35000.00, NULL),
(11, DATE '2026-02-14', 'Andi', 'Gula', 'Gula Merah', 35, 15000.00, NULL),
(12, DATE '2026-02-18', 'Budi', 'Kopi', 'Kopi Toraja', 6, 90000.00, 12.00);
SELECT id, tanggal, penjual, kategori, produk, jumlah, harga, diskon
FROM penjualan
ORDER BY id;Output yang Dihasilkan:
CREATE TABLE
INSERT 0 12
id | tanggal | penjual | kategori | produk | jumlah | harga | diskon
----+------------+---------+----------+-------------+--------+----------+--------
1 | 2026-01-05 | Budi | Kopi | Kopi Gayo | 10 | 85000.00 | 5.00
2 | 2026-01-08 | Siti | Teh | Teh Melati | 25 | 35000.00 |
3 | 2026-01-12 | Andi | Gula | Gula Merah | 40 | 15000.00 |
4 | 2026-01-15 | Budi | Kopi | Kopi Toraja | 8 | 90000.00 | 10.00
5 | 2026-01-20 | Dewi | | Kopi Gayo | 12 | 85000.00 |
6 | 2026-01-22 | Siti | Teh | Teh Hijau | 15 | 40000.00 | 7.50
7 | 2026-01-25 | Andi | Gula | Gula Merah | 30 | 15000.00 | 2.50
8 | 2026-02-02 | Budi | | Teh Melati | 20 | 35000.00 |
9 | 2026-02-05 | Dewi | Kopi | Kopi Gayo | 18 | 85000.00 | 8.00
10 | 2026-02-10 | Siti | Teh | Teh Melati | 22 | 35000.00 |
11 | 2026-02-14 | Andi | Gula | Gula Merah | 35 | 15000.00 |
12 | 2026-02-18 | Budi | Kopi | Kopi Toraja | 6 | 90000.00 | 12.00
(12 rows)Mekanisme: Kelompok Dulu, Baru Agregat #
Cara termudah memahami GROUP BY adalah membayangkan tiga tahap yang berjalan berurutan. Pertama, seluruh baris hasil FROM dan WHERE dibagi ke dalam keranjang menurut nilai kolom pengelompok — semua baris bernilai Budi masuk satu keranjang, semua baris Siti ke keranjang lain. Kedua, agregat seperti count(*) atau sum(...) berjalan terpisah di dalam tiap keranjang. Ketiga, tiap keranjang memunculkan tepat satu baris hasil. Diagram berikut merangkum alurnya untuk GROUP BY penjual:
Sebelum dan sesudah GROUP BY, query melewati tahapan logis dengan urutan tetap — memahami urutan ini menjelaskan banyak perilaku yang akan kita temui, termasuk mengapa WHERE tidak bisa menyaring hasil agregat:
| Tahap | Klausa | Tugas |
|---|---|---|
| 1 | FROM | Menentukan sumber baris mentah |
| 2 | WHERE | Menyaring baris mentah, sebelum pengelompokan |
| 3 | GROUP BY | Mengemas baris yang lolos menjadi kelompok |
| 4 | HAVING | Menyaring kelompok hasil agregat (chapter berikutnya) |
| 5 | SELECT | Memilih kolom kelompok dan hasil agregat |
| 6 | ORDER BY | Mengurutkan baris hasil akhir |
Satu Kolom Pengelompok #
Bentuk paling dasar: satu kolom, beberapa agregat, satu baris hasil per kelompok:
-- GROUP BY satu kolom: baris dikemas per penjual, agregat berjalan
-- di dalam tiap kelompok, hasilnya satu baris per penjual.
SELECT penjual,
count(*) AS transaksi,
sum(jumlah) AS total_item,
sum(jumlah * harga) AS total_nilai
FROM penjualan
GROUP BY penjual
ORDER BY penjual;
-- Tanpa GROUP BY agregat menelan seluruh tabel; dengan GROUP BY
-- angka yang sama terpecah per kelompok — totalnya tetap 241 item
-- dan 9180000.00 rupiah seperti versi satu baris.
SELECT sum(jumlah) AS total_item,
sum(jumlah * harga) AS total_nilai
FROM penjualan;Output yang Dihasilkan:
penjual | transaksi | total_item | total_nilai
---------+-----------+------------+-------------
Andi | 3 | 105 | 1575000.00
Budi | 4 | 44 | 2810000.00
Dewi | 2 | 30 | 2550000.00
Siti | 3 | 62 | 2245000.00
(4 rows)
total_item | total_nilai
------------+-------------
241 | 9180000.00
(1 row)Mari kita bedah query pertama baris per baris:
GROUP BY penjual— kolompenjualmenjadi papan pembagi; 12 baris terpecah menjadi 4 kelompok.count(*),sum(jumlah),sum(jumlah * harga)— ketiga agregat dihitung terpisah di tiap keranjang: keranjang Andi menghasilkan3, 105, 1575000.00, keranjang Budi4, 44, 2810000.00, dan seterusnya.ORDER BY penjual— mengurutkan hasil akhir menurut kolom kelompok, bukan kebetulan urutan internal.
Perhatikan pembacaan hasilnya: Andi unggul di total item (105 unit) meski kalah dari Budi di jumlah transaksi (3 lawan 4), dan Dewi hanya bertransaksi 2 kali tetapi nilainya (2550000.00) menyalip Siti yang bertransaksi 3 kali. Query kedua di file yang sama menegaskan sifat “pecahan” dari pengelompokan: versi tanpa GROUP BY menghasilkan total keseluruhan — 241 item dan 9180000.00 rupiah — yang merupakan penjumlahan keempat baris versi berkelompok. Pengelompokan tidak mengubah total, hanya cara memandangnya.
Aturan Emas: Dikelompokkan atau Diagregasi #
Inilah sumber error paling sering dalam sejarah SQL pemula: setiap kolom di daftar SELECT harus salah satu dari dua hal — masuk GROUP BY, atau dibungkus agregat. Logikanya soal ambiguitas: bila hasil hanya boleh satu baris per kelompok, kolom produk yang isinya banyak-macam di dalam keranjang Budi tidak punya wakil tunggal yang jelas:
-- Aturan GROUP BY: setiap kolom di SELECT harus dikelompokkan atau diagregasi.
-- Query ini MELANGGAR aturan itu: produk tidak masuk GROUP BY dan tidak
-- dibungkus agregat — PostgreSQL menolaknya karena tidak tahu produk mana
-- yang layak mewakili kelompok tiap penjual.
SELECT penjual, produk, count(*) AS transaksi
FROM penjualan
GROUP BY penjual;Output yang Dihasilkan:
psql:group-by-error.sql:7: ERROR: column "penjualan.produk" must appear in the GROUP BY clause or be used in an aggregate function
LINE 1: SELECT penjual, produk, count(*) AS transaksi
^PostgreSQL menolak query dengan pesan column "penjualan.produk" must appear in the GROUP BY clause or be used in an aggregate function — bukan untuk menyulitkan, melainkan karena jawaban yang mungkin ada banyak (produk pertama? terakhir? terlaris?) dan database menolak menebak. Dua jalan keluarnya masing-masing menjawab pertanyaan berbeda, dan keduanya kita uji sekarang:
-- Perbaikan pertama: produk ikut masuk GROUP BY.
-- Kelompok kini pasangan (penjual, produk) — lebih halus dari per penjual saja.
SELECT penjual, produk,
count(*) AS transaksi,
sum(jumlah) AS total_item
FROM penjualan
GROUP BY penjual, produk
ORDER BY penjual, produk;
-- Perbaikan kedua: bungkus kolom tambahan dengan agregat.
-- max(produk) memilih satu wakil yang pasti — produk terakhir menurut abjad.
SELECT penjual,
count(*) AS transaksi,
max(produk) AS produk_wakil
FROM penjualan
GROUP BY penjual
ORDER BY penjual;
-- Analisis dua kolom yang lain: kombinasi (kategori, penjual).
-- Budi muncul di dua kelompok berbeda — Kopi dan tanpa kategori.
SELECT kategori, penjual,
count(*) AS transaksi,
sum(jumlah * harga) AS total_nilai
FROM penjualan
GROUP BY kategori, penjual
ORDER BY kategori, penjual;Output yang Dihasilkan:
penjual | produk | transaksi | total_item
---------+-------------+-----------+------------
Andi | Gula Merah | 3 | 105
Budi | Kopi Gayo | 1 | 10
Budi | Kopi Toraja | 2 | 14
Budi | Teh Melati | 1 | 20
Dewi | Kopi Gayo | 2 | 30
Siti | Teh Hijau | 1 | 15
Siti | Teh Melati | 2 | 47
(7 rows)
penjual | transaksi | produk_wakil
---------+-----------+--------------
Andi | 3 | Gula Merah
Budi | 4 | Teh Melati
Dewi | 2 | Kopi Gayo
Siti | 3 | Teh Melati
(4 rows)
kategori | penjual | transaksi | total_nilai
----------+---------+-----------+-------------
Gula | Andi | 3 | 1575000.00
Kopi | Budi | 3 | 2110000.00
Kopi | Dewi | 1 | 1530000.00
Teh | Siti | 3 | 2245000.00
| Budi | 1 | 700000.00
| Dewi | 1 | 1020000.00
(6 rows)Perbaikan pertama memperhalus kelompoknya: GROUP BY penjual, produk membuat kelompok per pasangan nilai — Budi kini muncul tiga kali, satu baris per produk yang dijualnya. Perbaikan kedua mempertahankan kelompok per penjual tetapi memaksa produk melewati agregat: max(produk) memilih wakil yang pasti (produk terakhir menurut abjad) sehingga aturan terpenuhi. Query ketiga memperlihatkan analisis dua kolom yang lazim di laporan: kombinasi (kategori, penjual) menghasilkan enam kelompok, dan Budi terbelah menjadi dua baris — di kelompok Kopi dan di kelompok kategori kosong.
Mengelompokkan dengan Ekspresi #
Papan pembagi tidak harus nama kolom — ekspresi pun sah, dan inilah cara standar mengelompokkan per bulan, per pekan, atau per rentang apa pun:
-- GROUP BY boleh memakai ekspresi, bukan sekadar nama kolom.
-- Ekspresi di SELECT dan di GROUP BY harus sama tulisannya.
SELECT to_char(tanggal, 'YYYY-MM') AS bulan,
count(*) AS transaksi,
sum(jumlah * harga) AS total_nilai
FROM penjualan
GROUP BY to_char(tanggal, 'YYYY-MM')
ORDER BY bulan;
-- Pola saudaranya untuk deret waktu: date_trunc() memotong tanggal
-- ke awal bulan. Hasilnya timestamp, jadi dilemparkan ke date agar rapi.
SELECT date_trunc('month', tanggal)::date AS awal_bulan,
count(*) AS transaksi,
sum(jumlah) AS total_item
FROM penjualan
GROUP BY date_trunc('month', tanggal)
ORDER BY awal_bulan;Output yang Dihasilkan:
bulan | transaksi | total_nilai
---------+-----------+-------------
2026-01 | 7 | 5115000.00
2026-02 | 5 | 4065000.00
(2 rows)
awal_bulan | transaksi | total_item
------------+-----------+------------
2026-01-01 | 7 | 140
2026-02-01 | 5 | 101
(2 rows)Query pertama mengelompokkan menurut label bulan hasil to_char(tanggal, 'YYYY-MM'): tujuh transaksi Januari senilai 5115000.00 dan lima transaksi Februari senilai 4065000.00. Query kedua memakai date_trunc('month', tanggal) yang memotong setiap tanggal ke awal bulannya — pola yang sama dipakai untuk granularitas pekan, kuartal, atau tahun dengan cukup mengganti argumennya. Dua kebiasaan perlu dicatat: tulis ekspresi yang sama persis di SELECT dan GROUP BY, dan ORDER BY pada kolom hasilnya (bulan atau awal_bulan) menjaga urutan kronologis. PostgreSQL juga menerima GROUP BY bulan (nama alias) dan GROUP BY 1 (posisi kolom) sebagai kenyamanan tambahan — keduanya ekstensi PostgreSQL yang tidak dimiliki SQL standar, jadi pikirkan portabilitas sebelum memakainya.
NULL Membentuk Kelompoknya Sendiri #
Sepanjang section ini kita melihat agregat mengabaikan NULL — count(kategori), sum(diskon), semuanya melewatkan NULL. Di GROUP BY, aturannya terbalik: NULL pada kolom pengelompok bukan diabaikan, melainkan diperlakukan sebagai nilai kelompok yang sah:
-- NULL pada kolom pengelompok membentuk kelompoknya sendiri:
-- dua transaksi tanpa kategori menjadi satu baris hasil.
SELECT kategori,
count(*) AS transaksi,
sum(jumlah * harga) AS total_nilai
FROM penjualan
GROUP BY kategori
ORDER BY kategori;
-- Untuk laporan, beri label pada kelompok NULL dengan coalesce().
-- GROUP BY tetap memakai kolom asli; hanya tampilannya yang dirapikan.
SELECT coalesce(kategori, '(tanpa kategori)') AS kategori_rapian,
count(*) AS transaksi,
sum(jumlah * harga) AS total_nilai
FROM penjualan
GROUP BY kategori
ORDER BY kategori;Output yang Dihasilkan:
kategori | transaksi | total_nilai
----------+-----------+-------------
Gula | 3 | 1575000.00
Kopi | 4 | 3640000.00
Teh | 3 | 2245000.00
| 2 | 1720000.00
(4 rows)
kategori_rapian | transaksi | total_nilai
------------------+-----------+-------------
Gula | 3 | 1575000.00
Kopi | 4 | 3640000.00
Teh | 3 | 2245000.00
(tanpa kategori) | 2 | 1720000.00
(4 rows)Query pertama menghasilkan empat baris: tiga kategori terisi plus satu baris dengan kategori kosong — dua transaksi yang belum dikategorikan berkumpul di situ dengan total nilai 1720000.00. Di sinilah paradoks yang layak dihafal: agregat mengabaikan NULL, pengelompokan merangkul NULL. Query kedua memperlihatkan pola pelaporannya: coalesce(kategori, '(tanpa kategori)') memberi label pada kelompok kosong tanpa mengubah GROUP BY — pengelompokan tetap pada kolom asli, hanya tampilan yang dirapikan. Perhatikan pula posisi barisnya: dengan ORDER BY kategori urutan menaik, PostgreSQL menempatkan NULL di posisi terakhir (perilaku NULLS LAST bawaan untuk urutan naik — detailnya di chapter ORDER BY
).
Urutan Hasil yang Deterministik #
Satu kebiasaan terakhir yang dipakai diam-diam sepanjang chapter: GROUP BY tidak menjamin urutan baris hasil. Urutan internal keranjang sewaktu-waktu bisa berubah mengikuti rencana eksekusi, dan membiarkannya begitu berarti laporan Anda tampil acak antar eksekusi. Karena itu setiap query di chapter ini — dan setiap query berkelompok di course ini — selalu diakhiri ORDER BY pada kolom kelompoknya, ditambah kolom pemecah seri bila satu kolom belum menentukan urutan penuh: ORDER BY kategori, penjual untuk kelompok dua kolom, ORDER BY bulan untuk kelompok ekspresi. Aturan praktisnya sederhana: bila urutan baris hasil akan dibaca manusia, tulis ORDER BY-nya secara eksplisit.
Kelompok kini terbentuk rapi — tetapi laporan nyata jarang berhenti di “semua kelompok”: kita ingin hanya penjual dengan transaksi di atas ambang tertentu, hanya kategori dengan rata-rata di atas target. Menyaring baris sebelum pengelompokan adalah tugas WHERE; menyaring kelompok setelah agregat butuh alat baru. Chapter berikutnya membahas alat itu beserta pembeda tugasnya dari WHERE: HAVING
.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
buat-tabel.sql
Unduh berkas-- Tabel latihan utama Section Agregasi Data:
-- transaksi penjualan Toko Maju Jaya, Januari-Februari 2026.
CREATE TABLE penjualan (
id integer PRIMARY KEY,
tanggal date NOT NULL,
penjual text NOT NULL,
kategori text, -- NULL = transaksi belum dikategorikan
produk text NOT NULL,
jumlah integer NOT NULL, -- unit terjual
harga numeric(10,2) NOT NULL, -- harga satuan, rupiah
diskon numeric(5,2) -- NULL = tanpa diskon
);
INSERT INTO penjualan (id, tanggal, penjual, kategori, produk, jumlah, harga, diskon) VALUES
( 1, DATE '2026-01-05', 'Budi', 'Kopi', 'Kopi Gayo', 10, 85000.00, 5.00),
( 2, DATE '2026-01-08', 'Siti', 'Teh', 'Teh Melati', 25, 35000.00, NULL),
( 3, DATE '2026-01-12', 'Andi', 'Gula', 'Gula Merah', 40, 15000.00, NULL),
( 4, DATE '2026-01-15', 'Budi', 'Kopi', 'Kopi Toraja', 8, 90000.00, 10.00),
( 5, DATE '2026-01-20', 'Dewi', NULL, 'Kopi Gayo', 12, 85000.00, NULL),
( 6, DATE '2026-01-22', 'Siti', 'Teh', 'Teh Hijau', 15, 40000.00, 7.50),
( 7, DATE '2026-01-25', 'Andi', 'Gula', 'Gula Merah', 30, 15000.00, 2.50),
( 8, DATE '2026-02-02', 'Budi', NULL, 'Teh Melati', 20, 35000.00, NULL),
( 9, DATE '2026-02-05', 'Dewi', 'Kopi', 'Kopi Gayo', 18, 85000.00, 8.00),
(10, DATE '2026-02-10', 'Siti', 'Teh', 'Teh Melati', 22, 35000.00, NULL),
(11, DATE '2026-02-14', 'Andi', 'Gula', 'Gula Merah', 35, 15000.00, NULL),
(12, DATE '2026-02-18', 'Budi', 'Kopi', 'Kopi Toraja', 6, 90000.00, 12.00);
SELECT id, tanggal, penjual, kategori, produk, jumlah, harga, diskon
FROM penjualan
ORDER BY id;
group-by-error.sql
Unduh berkas-- Aturan GROUP BY: setiap kolom di SELECT harus dikelompokkan atau diagregasi.
-- Query ini MELANGGAR aturan itu: produk tidak masuk GROUP BY dan tidak
-- dibungkus agregat — PostgreSQL menolaknya karena tidak tahu produk mana
-- yang layak mewakili kelompok tiap penjual.
SELECT penjual, produk, count(*) AS transaksi
FROM penjualan
GROUP BY penjual;
group-dua-kolom.sql
Unduh berkas-- Perbaikan pertama: produk ikut masuk GROUP BY.
-- Kelompok kini pasangan (penjual, produk) — lebih halus dari per penjual saja.
SELECT penjual, produk,
count(*) AS transaksi,
sum(jumlah) AS total_item
FROM penjualan
GROUP BY penjual, produk
ORDER BY penjual, produk;
-- Perbaikan kedua: bungkus kolom tambahan dengan agregat.
-- max(produk) memilih satu wakil yang pasti — produk terakhir menurut abjad.
SELECT penjual,
count(*) AS transaksi,
max(produk) AS produk_wakil
FROM penjualan
GROUP BY penjual
ORDER BY penjual;
-- Analisis dua kolom yang lain: kombinasi (kategori, penjual).
-- Budi muncul di dua kelompok berbeda — Kopi dan tanpa kategori.
SELECT kategori, penjual,
count(*) AS transaksi,
sum(jumlah * harga) AS total_nilai
FROM penjualan
GROUP BY kategori, penjual
ORDER BY kategori, penjual;
group-ekspresi.sql
Unduh berkas-- GROUP BY boleh memakai ekspresi, bukan sekadar nama kolom.
-- Ekspresi di SELECT dan di GROUP BY harus sama tulisannya.
SELECT to_char(tanggal, 'YYYY-MM') AS bulan,
count(*) AS transaksi,
sum(jumlah * harga) AS total_nilai
FROM penjualan
GROUP BY to_char(tanggal, 'YYYY-MM')
ORDER BY bulan;
-- Pola saudaranya untuk deret waktu: date_trunc() memotong tanggal
-- ke awal bulan. Hasilnya timestamp, jadi dilemparkan ke date agar rapi.
SELECT date_trunc('month', tanggal)::date AS awal_bulan,
count(*) AS transaksi,
sum(jumlah) AS total_item
FROM penjualan
GROUP BY date_trunc('month', tanggal)
ORDER BY awal_bulan;
group-null.sql
Unduh berkas-- NULL pada kolom pengelompok membentuk kelompoknya sendiri:
-- dua transaksi tanpa kategori menjadi satu baris hasil.
SELECT kategori,
count(*) AS transaksi,
sum(jumlah * harga) AS total_nilai
FROM penjualan
GROUP BY kategori
ORDER BY kategori;
-- Untuk laporan, beri label pada kelompok NULL dengan coalesce().
-- GROUP BY tetap memakai kolom asli; hanya tampilannya yang dirapikan.
SELECT coalesce(kategori, '(tanpa kategori)') AS kategori_rapian,
count(*) AS transaksi,
sum(jumlah * harga) AS total_nilai
FROM penjualan
GROUP BY kategori
ORDER BY kategori;
group-satu-kolom.sql
Unduh berkas-- GROUP BY satu kolom: baris dikemas per penjual, agregat berjalan
-- di dalam tiap kelompok, hasilnya satu baris per penjual.
SELECT penjual,
count(*) AS transaksi,
sum(jumlah) AS total_item,
sum(jumlah * harga) AS total_nilai
FROM penjualan
GROUP BY penjual
ORDER BY penjual;
-- Tanpa GROUP BY agregat menelan seluruh tabel; dengan GROUP BY
-- angka yang sama terpecah per kelompok — totalnya tetap 241 item
-- dan 9180000.00 rupiah seperti versi satu baris.
SELECT sum(jumlah) AS total_item,
sum(jumlah * harga) AS total_nilai
FROM penjualan;