2807 kata
14 menit

Query SQL Lanjutan: GROUP BY, HAVING, dan Subquery

Pada Bab 5, kita sudah berhasil menghubungkan tabel pelanggan, produk, pesanan, dan detail_pesanan menggunakan foreign key dan JOIN.

Sekarang database toko online kita sudah bisa menyimpan informasi pelanggan, produk, dan transaksi.

Tapi sebagai pemilik toko, tentu kita ingin mengetahui lebih banyak hal.

Misalnya:

  • Berapa total omzet penjualan?
  • Produk apa yang paling banyak terjual?
  • Siapa pelanggan dengan total pembelian terbesar?
  • Berapa jumlah pesanan yang dibuat setiap pelanggan?
  • Produk mana yang harganya di atas rata-rata?

Pertanyaan seperti itu tidak cukup dijawab hanya dengan mengambil seluruh isi tabel.

Kita membutuhkan query yang dapat menghitung, mengelompokkan, menyaring hasil perhitungan, dan menggunakan hasil query lain sebagai bagian dari kondisi.

Pada Bab 6 Series Database BapakGabut ini, kita akan mempelajari tiga konsep utama:

  1. GROUP BY untuk mengelompokkan data.
  2. HAVING untuk memfilter hasil pengelompokan.
  3. Subquery untuk menggunakan hasil sebuah query di dalam query lainnya.

Kita juga akan mengenal fungsi agregasi seperti COUNT, SUM, AVG, MIN, dan MAX.

Tenang, kita tidak sedang membuat sistem analitik perusahaan multinasional.

Kita masih mengurus toko online BapakGabut. Hanya saja, sekarang pemilik tokonya mulai ingin melihat laporan.

Persiapan: Database dari Bab Sebelumnya#

Pastikan MySQL Server berjalan dan DBeaver sudah terhubung.

Buka SQL Editor, kemudian pilih database:

USE db_toko_bapakgabut;

Database kita memiliki empat tabel:

TabelFungsi
pelangganMenyimpan data pelanggan
produkMenyimpan data produk
pesananMenyimpan informasi utama pesanan
detail_pesananMenyimpan produk dan jumlah dalam setiap pesanan

Relasinya:

pelanggan (1) --- (N) pesanan
pesanan (1) --- (N) detail_pesanan
produk (1) --- (N) detail_pesanan

Pada Bab 5, kita sudah memasukkan contoh transaksi:

  • Andi membeli 1 Keyboard Mechanical seharga Rp325.000.
  • Andi membeli 2 Mouse Wireless seharga Rp135.000 per unit.
  • Budi membeli 1 Monitor 24 Inch seharga Rp1.650.000.

Jika seluruh data latihan masih sesuai contoh, total pembeliannya adalah:

PelangganTotal Pembelian
AndiRp595.000
BudiRp1.650.000

Total nilai transaksi dari dua pesanan tersebut adalah Rp2.245.000.

Namun, hasil query di komputer kamu bisa berbeda jika sudah menambahkan, mengubah, atau menghapus data.

Contoh angka dalam artikel ini merupakan ilustrasi berdasarkan transaksi yang dibuat pada Bab 5, bukan hasil yang dijamin sama untuk setiap database.

Memeriksa Data Sebelum Membuat Laporan#

Coba jalankan:

SELECT
p.id_pesanan,
pl.nama_pelanggan,
pr.nama_produk,
dp.jumlah,
dp.harga_satuan,
dp.jumlah * dp.harga_satuan AS subtotal
FROM pesanan p
INNER JOIN pelanggan pl
ON p.id_pelanggan = pl.id_pelanggan
INNER JOIN detail_pesanan dp
ON p.id_pesanan = dp.id_pesanan
INNER JOIN produk pr
ON dp.id_produk = pr.id_produk
ORDER BY p.id_pesanan, dp.id_detail;

Query tersebut menggabungkan empat tabel untuk menampilkan detail pembelian.

Kita akan menggunakan data yang sama untuk berbagai latihan analisis.

Mengenal Fungsi Agregasi SQL#

Fungsi agregasi digunakan untuk menghasilkan nilai ringkasan dari sekumpulan baris.

Beberapa fungsi agregasi yang sering digunakan dalam MySQL adalah:

FungsiKegunaan
COUNT()Menghitung jumlah baris atau nilai non-NULL
SUM()Menjumlahkan nilai
AVG()Menghitung rata-rata
MIN()Mengambil nilai terkecil
MAX()Mengambil nilai terbesar

Mari kita praktikkan satu per satu.

COUNT — Menghitung Jumlah Data#

Misalnya, kita ingin mengetahui jumlah produk yang tersedia dalam tabel.

SELECT COUNT(*) AS jumlah_produk
FROM produk;

COUNT(*) menghitung jumlah baris pada tabel, termasuk baris yang memiliki nilai NULL pada kolom tertentu.

Jika tabel produk memiliki empat baris setelah latihan Bab 4, hasilnya adalah 4.

Tetapi jika kamu menambahkan produk lain, jumlahnya tentu berbeda.

Sekarang coba menghitung jumlah pelanggan:

SELECT COUNT(*) AS jumlah_pelanggan
FROM pelanggan;

Kita juga bisa menghitung jumlah pesanan:

SELECT COUNT(*) AS jumlah_pesanan
FROM pesanan;

Perhatikan perbedaan berikut:

SELECT
COUNT(*) AS total_baris,
COUNT(email) AS email_tidak_null
FROM pelanggan;

COUNT(*) menghitung semua baris.

Sementara COUNT(email) hanya menghitung baris dengan nilai email yang tidak NULL.

Karena pada struktur tabel pelanggan kita kolom email menggunakan NOT NULL, kedua hasil tersebut seharusnya sama.

SUM — Menjumlahkan Nilai#

Misalnya, kita ingin menghitung total stok seluruh produk.

SELECT SUM(stok) AS total_stok
FROM produk;

MySQL akan menjumlahkan nilai stok dari semua produk.

Sekarang coba menghitung total nilai seluruh detail transaksi:

SELECT
SUM(jumlah * harga_satuan) AS total_penjualan
FROM detail_pesanan;

Jika hanya ada tiga baris detail transaksi dari Bab 5, hasilnya:

2245000.00

Perhitungannya:

325000 + (2 × 135000) + 1650000 = 2245000

Nilai tersebut merupakan total nominal detail pesanan yang tercatat.

Kita menyebutnya total penjualan untuk latihan ini, tetapi pada aplikasi nyata, definisi omzet perlu mempertimbangkan status pesanan, pembatalan, retur, diskon, pajak, dan biaya lainnya.

Jangan langsung menganggap semua pesanan yang tercatat sudah menjadi pendapatan final.

AVG — Menghitung Rata-Rata#

Untuk menghitung rata-rata harga produk:

SELECT AVG(harga) AS rata_rata_harga
FROM produk;

AVG() menghitung rata-rata dari nilai yang tidak NULL.

Contoh lain, rata-rata jumlah unit per baris detail pesanan:

SELECT AVG(jumlah) AS rata_rata_jumlah
FROM detail_pesanan;

Perlu diperhatikan, query tersebut menghitung rata-rata jumlah unit per baris detail, bukan rata-rata jumlah barang per pesanan.

Kedua pertanyaan tersebut berbeda dan dapat menghasilkan nilai yang berbeda.

MIN dan MAX — Nilai Terkecil dan Terbesar#

Untuk mencari harga produk termurah dan termahal:

SELECT
MIN(harga) AS harga_termurah,
MAX(harga) AS harga_termahal
FROM produk;

Query tersebut hanya mengembalikan nilai harga, bukan nama produknya.

Kalau ingin mengetahui produk yang memiliki harga tersebut, kita perlu query tambahan atau menggunakan subquery yang akan dipelajari nanti.

GROUP BY — Mengelompokkan Data#

Sekarang kita masuk ke konsep utama pertama.

Bayangkan tabel detail_pesanan berisi banyak baris transaksi.

Kita ingin mengetahui berapa unit masing-masing produk yang sudah terjual.

Jika menggunakan SUM(jumlah) tanpa pengelompokan, kita hanya mendapatkan total seluruh produk.

Padahal, kita ingin hasil per produk.

Di sinilah GROUP BY digunakan.

Menghitung Total Unit Terjual per Produk#

Jalankan:

SELECT
pr.id_produk,
pr.nama_produk,
SUM(dp.jumlah) AS total_terjual
FROM detail_pesanan dp
INNER JOIN produk pr
ON dp.id_produk = pr.id_produk
GROUP BY
pr.id_produk,
pr.nama_produk
ORDER BY total_terjual DESC;

Query tersebut melakukan beberapa hal:

  1. Menggabungkan detail pesanan dengan produk.
  2. Mengelompokkan baris berdasarkan produk.
  3. Menjumlahkan jumlah pembelian pada setiap kelompok.
  4. Mengurutkan hasil berdasarkan total unit terjual.

Jika menggunakan data contoh Bab 5, hasilnya:

ProdukTotal Terjual
Mouse Wireless2
Keyboard Mechanical1
Monitor 24 Inch1

Sekarang kita tahu bahwa Mouse Wireless menjadi produk dengan unit penjualan terbanyak dalam data latihan.

Namun, produk yang paling banyak terjual belum tentu menghasilkan nilai penjualan terbesar.

Harga produk juga berpengaruh.

Menghitung Nilai Penjualan per Produk#

Coba jalankan:

SELECT
pr.id_produk,
pr.nama_produk,
SUM(dp.jumlah) AS total_unit,
SUM(dp.jumlah * dp.harga_satuan) AS total_nilai
FROM detail_pesanan dp
INNER JOIN produk pr
ON dp.id_produk = pr.id_produk
GROUP BY
pr.id_produk,
pr.nama_produk
ORDER BY total_nilai DESC;

Hasil ilustrasinya:

ProdukTotal UnitTotal Nilai
Monitor 24 Inch11650000
Keyboard Mechanical1325000
Mouse Wireless2270000

Menarik, bukan?

Mouse Wireless paling banyak terjual berdasarkan jumlah unit.

Tetapi Monitor 24 Inch menghasilkan nilai penjualan terbesar.

Jadi, ketika seseorang bertanya “produk terlaris”, kita perlu memastikan maksudnya:

  • Paling banyak unit terjual?
  • Paling tinggi nilai penjualan?
  • Paling sering muncul dalam pesanan?

Definisi metrik yang berbeda dapat menghasilkan peringkat yang berbeda.

Menghitung Total Pembelian per Pelanggan#

Sekarang kita ingin mengetahui total nilai pembelian setiap pelanggan.

SELECT
pl.id_pelanggan,
pl.nama_pelanggan,
SUM(dp.jumlah * dp.harga_satuan) AS total_pembelian
FROM pelanggan pl
INNER JOIN pesanan p
ON pl.id_pelanggan = p.id_pelanggan
INNER JOIN detail_pesanan dp
ON p.id_pesanan = dp.id_pesanan
GROUP BY
pl.id_pelanggan,
pl.nama_pelanggan
ORDER BY total_pembelian DESC;

Jika data sesuai contoh, Budi berada di atas Andi karena total nilai pembeliannya lebih besar.

Query tersebut hanya menampilkan pelanggan yang memiliki detail transaksi.

Pelanggan yang belum melakukan pembelian tidak akan muncul karena kita menggunakan INNER JOIN.

GROUP BY dengan LEFT JOIN#

Bagaimana jika kita ingin menampilkan semua pelanggan, termasuk yang belum pernah membeli?

Gunakan LEFT JOIN.

SELECT
pl.id_pelanggan,
pl.nama_pelanggan,
COUNT(DISTINCT p.id_pesanan) AS jumlah_pesanan,
COALESCE(
SUM(dp.jumlah * dp.harga_satuan),
0
) AS total_pembelian
FROM pelanggan pl
LEFT JOIN pesanan p
ON pl.id_pelanggan = p.id_pelanggan
LEFT JOIN detail_pesanan dp
ON p.id_pesanan = dp.id_pesanan
GROUP BY
pl.id_pelanggan,
pl.nama_pelanggan
ORDER BY total_pembelian DESC;

Ada dua hal penting pada query tersebut.

Kenapa Menggunakan COUNT(DISTINCT)?#

Karena satu pesanan dapat memiliki beberapa baris detail.

Jika kita menggunakan COUNT(p.id_pesanan) setelah JOIN dengan detail_pesanan, satu pesanan yang memiliki dua produk bisa ikut terhitung dua kali.

Dengan COUNT(DISTINCT p.id_pesanan), kita menghitung ID pesanan yang berbeda.

Apa Fungsi COALESCE?#

COALESCE() mengembalikan nilai pertama yang tidak NULL dari daftar argumennya.

Pada query ini:

COALESCE(SUM(dp.jumlah * dp.harga_satuan), 0)

Jika pelanggan belum mempunyai detail pembelian dan hasil SUM() adalah NULL, nilai tersebut ditampilkan sebagai 0.

Dengan begitu, pelanggan tanpa pembelian tetap muncul dengan total nol.

WHERE dan HAVING: Apa Bedanya?#

Sebelum mengenal HAVING, kita sudah menggunakan WHERE untuk memfilter data.

Contohnya:

SELECT *
FROM produk
WHERE harga > 200000;

Query tersebut memfilter baris produk berdasarkan harga.

Tetapi bagaimana jika kita ingin menampilkan pelanggan dengan total pembelian di atas Rp500.000?

Total pembelian tersebut baru diketahui setelah kita melakukan pengelompokan dan perhitungan SUM().

Untuk memfilter hasil agregasi, kita menggunakan HAVING.

Menggunakan HAVING#

Jalankan:

SELECT
pl.id_pelanggan,
pl.nama_pelanggan,
SUM(dp.jumlah * dp.harga_satuan) AS total_pembelian
FROM pelanggan pl
INNER JOIN pesanan p
ON pl.id_pelanggan = p.id_pelanggan
INNER JOIN detail_pesanan dp
ON p.id_pesanan = dp.id_pesanan
GROUP BY
pl.id_pelanggan,
pl.nama_pelanggan
HAVING SUM(dp.jumlah * dp.harga_satuan) > 500000
ORDER BY total_pembelian DESC;

Query tersebut hanya menampilkan pelanggan dengan total pembelian lebih dari Rp500.000.

Pada data contoh, Andi dan Budi sama-sama memenuhi kondisi.

Perbedaan WHERE dan HAVING#

WHEREHAVING
Memfilter baris sebelum pengelompokanMemfilter kelompok setelah agregasi
Umumnya menggunakan kondisi pada nilai barisDapat menggunakan kondisi fungsi agregasi
Digunakan sebelum GROUP BY dalam penulisan queryDigunakan setelah GROUP BY

Secara konseptual, prosesnya dapat dibayangkan seperti ini:

FROM / JOIN
|
WHERE
|
GROUP BY
|
HAVING
|
SELECT
|
ORDER BY
|
LIMIT

Urutan tersebut merupakan gambaran logis untuk memahami query, bukan urutan fisik yang selalu digunakan oleh mesin database.

MySQL dapat mengoptimalkan pelaksanaan query dengan strategi yang berbeda.

Menggabungkan WHERE dan HAVING#

Misalnya, kita hanya ingin menghitung pesanan berstatus selesai, lalu menampilkan pelanggan dengan total pembelian lebih dari Rp500.000.

SELECT
pl.id_pelanggan,
pl.nama_pelanggan,
SUM(dp.jumlah * dp.harga_satuan) AS total_pembelian
FROM pelanggan pl
INNER JOIN pesanan p
ON pl.id_pelanggan = p.id_pelanggan
INNER JOIN detail_pesanan dp
ON p.id_pesanan = dp.id_pesanan
WHERE p.status_pesanan = 'selesai'
GROUP BY
pl.id_pelanggan,
pl.nama_pelanggan
HAVING SUM(dp.jumlah * dp.harga_satuan) > 500000;

WHERE memilih baris pesanan yang berstatus selesai.

Setelah itu, GROUP BY mengelompokkan data berdasarkan pelanggan.

Terakhir, HAVING memfilter kelompok berdasarkan total pembelian.

Catatan: Pada Bab 5, pesanan contoh kita menggunakan status baru. Jadi, query di atas bisa menghasilkan nol baris jika belum ada pesanan berstatus selesai.

Hal tersebut bukan berarti query salah.

Hasilnya memang mengikuti kondisi data.

Untuk mencoba dengan data contoh tanpa mengubah status pesanan, kamu dapat mengganti 'selesai' menjadi 'baru'.

Mengenal Subquery#

Sekarang kita masuk ke konsep utama berikutnya.

Subquery adalah query yang ditulis di dalam query lain.

Hasil subquery dapat digunakan oleh query luar untuk melakukan perbandingan, pemfilteran, atau membentuk sumber data sementara.

Misalnya, kita ingin mencari produk yang harganya lebih tinggi daripada rata-rata harga seluruh produk.

Kita bisa memecah pertanyaannya menjadi dua:

  1. Berapa rata-rata harga produk?
  2. Produk apa saja yang harganya lebih tinggi dari nilai tersebut?

Subquery memungkinkan kita menggabungkan kedua langkah itu dalam satu perintah SQL.

Subquery dengan Nilai Tunggal#

Pertama, lihat query untuk menghitung rata-rata harga:

SELECT AVG(harga)
FROM produk;

Sekarang gunakan query tersebut sebagai bagian dari kondisi:

SELECT
id_produk,
nama_produk,
harga
FROM produk
WHERE harga > (
SELECT AVG(harga)
FROM produk
)
ORDER BY harga DESC;

Bagian di dalam tanda kurung merupakan subquery.

Subquery menghitung rata-rata harga produk.

Kemudian query luar mengambil produk yang harganya lebih besar dari rata-rata tersebut.

Jenis ini sering disebut scalar subquery karena diharapkan menghasilkan satu nilai.

Jika subquery yang digunakan dalam konteks nilai tunggal menghasilkan lebih dari satu baris, MySQL akan menampilkan error.

Subquery dengan IN#

Subquery juga dapat menghasilkan beberapa baris.

Misalnya, kita ingin mengetahui pelanggan yang pernah membuat pesanan.

SELECT
id_pelanggan,
nama_pelanggan
FROM pelanggan
WHERE id_pelanggan IN (
SELECT id_pelanggan
FROM pesanan
);

Subquery mengambil daftar ID pelanggan dari tabel pesanan.

Kemudian query luar mencari pelanggan yang ID-nya terdapat dalam daftar tersebut.

Perintah IN digunakan untuk memeriksa apakah suatu nilai termasuk dalam kumpulan nilai.

Query seperti ini juga dapat ditulis menggunakan JOIN atau EXISTS.

Pilihan terbaik bergantung pada kebutuhan, bentuk data, dan rencana eksekusi database.

Subquery dengan EXISTS#

EXISTS digunakan untuk memeriksa apakah sebuah subquery menghasilkan setidaknya satu baris.

Misalnya, kita ingin menampilkan pelanggan yang sudah pernah membuat pesanan.

SELECT
pl.id_pelanggan,
pl.nama_pelanggan
FROM pelanggan pl
WHERE EXISTS (
SELECT 1
FROM pesanan p
WHERE p.id_pelanggan = pl.id_pelanggan
);

Subquery tersebut mengacu pada kolom dari query luar, yaitu pl.id_pelanggan.

Jenis seperti ini disebut correlated subquery.

Untuk setiap pelanggan yang diperiksa secara logis, kondisi EXISTS menentukan apakah ada pesanan yang cocok.

SELECT 1 digunakan karena EXISTS hanya membutuhkan informasi apakah baris ditemukan, bukan isi kolom yang dipilih.

MySQL dapat mengoptimalkan query EXISTS sehingga cara eksekusi fisiknya tidak selalu sama dengan gambaran evaluasi baris demi baris.

Mencari Pelanggan yang Belum Pernah Memesan#

Kita bisa menggunakan NOT EXISTS.

SELECT
pl.id_pelanggan,
pl.nama_pelanggan
FROM pelanggan pl
WHERE NOT EXISTS (
SELECT 1
FROM pesanan p
WHERE p.id_pelanggan = pl.id_pelanggan
);

Query tersebut menampilkan pelanggan yang tidak memiliki pesanan.

Ini merupakan alternatif dari pola LEFT JOIN ... IS NULL yang sudah kita pelajari pada Bab 5.

Subquery pada FROM: Derived Table#

Selain digunakan dalam WHERE, subquery juga dapat digunakan sebagai sumber data pada bagian FROM.

Contohnya, kita ingin menghitung rata-rata nilai pesanan.

Langkah pertama adalah menghitung total setiap pesanan.

SELECT
id_pesanan,
SUM(jumlah * harga_satuan) AS total_pesanan
FROM detail_pesanan
GROUP BY id_pesanan;

Setelah itu, kita menghitung rata-rata dari total-total tersebut.

SELECT
AVG(ringkasan.total_pesanan) AS rata_rata_nilai_pesanan
FROM (
SELECT
id_pesanan,
SUM(jumlah * harga_satuan) AS total_pesanan
FROM detail_pesanan
GROUP BY id_pesanan
) AS ringkasan;

Subquery pada FROM tersebut disebut derived table.

MySQL memperlakukannya sebagai hasil tabel yang dapat digunakan oleh query luar.

Perhatikan alias ringkasan.

Pada MySQL, derived table harus mempunyai alias.

Jika hanya terdapat dua pesanan contoh dari Bab 5, totalnya adalah Rp595.000 dan Rp1.650.000.

Maka rata-ratanya:

(595000 + 1650000) / 2 = 1122500

Jadi, rata-rata nilai dua pesanan tersebut adalah Rp1.122.500.

Perlu diperhatikan, query ini hanya menghitung pesanan yang memiliki baris detail.

Pesanan yang belum mempunyai detail tidak ikut dihitung.

JOIN atau Subquery, Mana yang Lebih Baik?#

Setelah mengenal subquery, mungkin muncul pertanyaan:

Kalau JOIN sudah bisa menggabungkan tabel, kenapa kita masih membutuhkan subquery?

Karena keduanya memiliki kegunaan yang saling melengkapi.

JOIN cocok ketika kita perlu menggabungkan informasi dari beberapa tabel.

Subquery sering berguna ketika kita ingin menggunakan hasil suatu perhitungan atau pencarian sebagai bagian dari query lain.

Contohnya:

KebutuhanPendekatan yang Bisa Digunakan
Menampilkan nama pelanggan dan pesanannyaJOIN
Mencari produk di atas rata-rata hargaSubquery
Mencari pelanggan yang pernah memesanJOIN, IN, atau EXISTS
Menghitung rata-rata dari total setiap pesananDerived table atau pendekatan agregasi bertahap

Tidak ada aturan bahwa subquery selalu lebih lambat daripada JOIN, atau sebaliknya.

Performa bergantung pada struktur tabel, jumlah data, index, kondisi query, dan optimasi MySQL.

Pada Bab 7, kita akan mempelajari bagaimana index dan EXPLAIN membantu memahami performa query.

Kesalahan Umum Saat Menggunakan GROUP BY dan Subquery#

Ada beberapa kesalahan yang sering terjadi ketika query mulai kompleks.

1. Memilih Kolom yang Tidak Sesuai dengan GROUP BY#

Perhatikan contoh:

SELECT
id_pelanggan,
status_pesanan,
COUNT(*)
FROM pesanan
GROUP BY id_pelanggan;

Query tersebut bermasalah secara konsep karena satu pelanggan bisa mempunyai beberapa pesanan dengan status berbeda.

Kolom status_pesanan tidak memiliki satu nilai yang pasti untuk setiap kelompok pelanggan.

Pada MySQL dengan ONLY_FULL_GROUP_BY aktif, query seperti ini dapat ditolak jika kolom tersebut tidak bergantung secara fungsional pada kolom pengelompokan.

Jika kita ingin menghitung jumlah pesanan berdasarkan pelanggan dan status, gunakan:

SELECT
id_pelanggan,
status_pesanan,
COUNT(*) AS jumlah_pesanan
FROM pesanan
GROUP BY
id_pelanggan,
status_pesanan;

Dengan begitu, pengelompokannya menjadi jelas.

2. Menggunakan WHERE untuk Memfilter SUM#

Contoh yang tidak tepat:

SELECT
id_pesanan,
SUM(jumlah * harga_satuan) AS total
FROM detail_pesanan
WHERE SUM(jumlah * harga_satuan) > 500000
GROUP BY id_pesanan;

Fungsi agregasi tidak digunakan secara langsung dalam kondisi WHERE pada tingkat query yang sama.

Gunakan HAVING:

SELECT
id_pesanan,
SUM(jumlah * harga_satuan) AS total
FROM detail_pesanan
GROUP BY id_pesanan
HAVING SUM(jumlah * harga_satuan) > 500000;

3. Menganggap COUNT Selalu Menghitung Data Unik#

Perhatikan:

SELECT COUNT(id_pesanan)
FROM detail_pesanan;

Query tersebut menghitung jumlah nilai id_pesanan yang tidak NULL pada baris detail.

Jika satu pesanan memiliki tiga baris detail, ID pesanan tersebut ikut dihitung tiga kali.

Untuk menghitung jumlah pesanan yang berbeda:

SELECT COUNT(DISTINCT id_pesanan) AS jumlah_pesanan
FROM detail_pesanan;

4. Tidak Memahami Hasil NULL#

Fungsi agregasi memiliki perilaku berbeda terhadap NULL.

COUNT(*) menghitung baris, sedangkan COUNT(kolom) mengabaikan nilai NULL.

SUM() dan AVG() juga mengabaikan nilai NULL dalam perhitungannya.

Jika tidak ada nilai yang dapat dijumlahkan, SUM() dapat menghasilkan NULL.

Gunakan COALESCE() ketika memang ingin menampilkan nilai pengganti, misalnya nol.

5. Menggunakan Subquery Tanpa Memahami Jumlah Hasilnya#

Subquery yang digunakan bersama operator = harus menghasilkan paling banyak satu baris.

Jika subquery dapat menghasilkan banyak nilai, pertimbangkan menggunakan IN, EXISTS, atau pendekatan JOIN sesuai kebutuhan.

Memahami bentuk hasil subquery sama pentingnya dengan memahami sintaksnya.

Latihan Mandiri#

Sekarang kita coba menggabungkan beberapa konsep dalam satu latihan.

Gunakan database db_toko_bapakgabut.

Latihan 1 — Jumlah Pesanan per Pelanggan#

Tampilkan:

  • Nama pelanggan.
  • Jumlah pesanan.

Tampilkan semua pelanggan, termasuk yang belum pernah memesan.

Urutkan berdasarkan jumlah pesanan terbanyak.

Latihan 2 — Produk dengan Penjualan Lebih dari Satu Unit#

Tampilkan:

  • Nama produk.
  • Total unit terjual.

Hanya tampilkan produk dengan total unit terjual lebih dari satu.

Gunakan GROUP BY dan HAVING.

Latihan 3 — Produk di Atas Rata-Rata Harga#

Tampilkan nama dan harga produk yang harganya lebih besar dari rata-rata harga seluruh produk.

Gunakan subquery.

Latihan 4 — Pelanggan Tanpa Pesanan#

Tampilkan pelanggan yang belum pernah membuat pesanan.

Gunakan NOT EXISTS.

Latihan 5 — Pesanan dengan Total di Atas Rp500.000#

Tampilkan:

  • ID pesanan.
  • Total nilai pesanan.

Hanya tampilkan pesanan yang totalnya lebih dari Rp500.000.

Urutkan berdasarkan total terbesar.

Coba kerjakan sendiri sebelum melihat pembahasannya.

Pembahasan Latihan#

Jawaban Latihan 1#

SELECT
pl.id_pelanggan,
pl.nama_pelanggan,
COUNT(p.id_pesanan) AS jumlah_pesanan
FROM pelanggan pl
LEFT JOIN pesanan p
ON pl.id_pelanggan = p.id_pelanggan
GROUP BY
pl.id_pelanggan,
pl.nama_pelanggan
ORDER BY jumlah_pesanan DESC, pl.nama_pelanggan ASC;

Karena query ini hanya menggabungkan pelanggan dan pesanan, setiap baris pesanan dihitung satu kali.

Jika kita ikut menggabungkan detail pesanan, perlu memperhatikan potensi penghitungan ganda.

Jawaban Latihan 2#

SELECT
pr.id_produk,
pr.nama_produk,
SUM(dp.jumlah) AS total_terjual
FROM produk pr
INNER JOIN detail_pesanan dp
ON pr.id_produk = dp.id_produk
GROUP BY
pr.id_produk,
pr.nama_produk
HAVING SUM(dp.jumlah) > 1
ORDER BY total_terjual DESC;

Jawaban Latihan 3#

SELECT
id_produk,
nama_produk,
harga
FROM produk
WHERE harga > (
SELECT AVG(harga)
FROM produk
)
ORDER BY harga DESC;

Jawaban Latihan 4#

SELECT
pl.id_pelanggan,
pl.nama_pelanggan
FROM pelanggan pl
WHERE NOT EXISTS (
SELECT 1
FROM pesanan p
WHERE p.id_pelanggan = pl.id_pelanggan
);

Jawaban Latihan 5#

SELECT
id_pesanan,
SUM(jumlah * harga_satuan) AS total_pesanan
FROM detail_pesanan
GROUP BY id_pesanan
HAVING SUM(jumlah * harga_satuan) > 500000
ORDER BY total_pesanan DESC;

Jika data sesuai contoh Bab 5, kedua pesanan memenuhi kondisi tersebut.

Andi memiliki total Rp595.000 dan Budi Rp1.650.000.

Kesimpulan#

Pada Bab 6 ini, kita sudah mempelajari cara mengubah data transaksi menjadi informasi yang lebih berguna.

Kita menggunakan fungsi agregasi untuk menghitung jumlah, total, rata-rata, nilai terkecil, dan nilai terbesar.

Dengan GROUP BY, kita dapat mengelompokkan transaksi berdasarkan produk, pelanggan, atau pesanan.

Dengan HAVING, kita dapat menyaring kelompok berdasarkan hasil agregasi.

Kita juga mempelajari subquery menggunakan nilai tunggal, IN, EXISTS, NOT EXISTS, dan derived table.

Sekarang kita sudah bisa menjawab berbagai pertanyaan bisnis menggunakan SQL.

Misalnya, produk mana yang paling banyak terjual, siapa pelanggan dengan nilai pembelian terbesar, dan berapa rata-rata nilai pesanan.

Tetapi ada tantangan berikutnya.

Bagaimana jika tabel produk berisi ratusan ribu baris dan tabel transaksi berisi jutaan data?

Apakah query yang kita tulis masih cukup cepat?

Dan bagaimana memastikan perubahan data tidak berhenti di tengah jalan sehingga stok dan transaksi menjadi tidak konsisten?

Pada Bab 7, kita akan mempelajari Index dan Transaction MySQL: Bikin Query Lebih Efisien dan Aman.

Kita akan mengenal index, membaca hasil EXPLAIN, serta menggunakan START TRANSACTION, COMMIT, dan ROLLBACK.

Karena membuat laporan penjualan itu penting.

Tapi memastikan query tidak membuat pengguna menunggu terlalu lama juga tidak kalah penting.

Sampai jumpa di artikel berikutnya, Gabuters!

Bagian 6 dari 8Tulisan ini bagian dari seri

Bagikan Artikel

Buat poster berbagi atau salin tautan artikel ini.

Lanjut membaca

Bacaan terkait

Berdasarkan tag dan kategori yang sama

Coba jalur lain

Pilihan konsisten dari artikel lain