TL;DR
MAX di SQL mengembalikan nilai terbesar dari sebuah kolom dan MIN mengembalikan yang terkecil, contohnya MAX(total) dan MIN(tanggal). Dua-duanya cuma mengembalikan angkanya, bukan baris tempat angka itu berada. Buat ambil baris lengkapnya, pakai ORDER BY ... LIMIT 1 untuk satu baris, atau window function ROW_NUMBER kalau kamu butuh baris teratas per grup.
MAX di SQL ngambil nilai terbesar dari sebuah kolom. MIN ngambil yang terkecil.
SELECT MAX(total) AS transaksi_terbesar,
MIN(total) AS transaksi_terkecil
FROM penjualan;
Dua baris, selesai. Masalahnya muncul di pertanyaan lanjutan yang hampir selalu ditanya bos kamu: "transaksi 120.000 itu punya siapa?"
MAX nggak bisa jawab. Dia ngeluarin angka, bukan baris.
MIN dan MAX adalah fungsi agregat yang ngambil satu nilai ekstrem dari banyak baris. MAX ngasih nilai terbesar di kolom yang kamu sebut, MIN ngasih yang terkecil. Dua-duanya ngelewatin baris NULL, dan dua-duanya jalan di kolom angka, tanggal, maupun teks.
Di kolom tanggal, MIN artinya paling awal. Di kolom teks, urutannya alfabet.
Fungsi agregat lain yang sering dipakai bareng ada di panduan fungsi agregat SQL.
Pakai tabel penjualan dari dataset toko_berkah, 10 transaksi Januari 2026.
| id | tanggal | kota | kategori | total | ongkir |
|---|---|---|---|---|---|
| 1 | 2026-01-03 | Bandung | Minuman | 30.000 | NULL |
| 2 | 2026-01-05 | Bandung | Makanan | 45.000 | 10.000 |
| 3 | 2026-01-07 | Surabaya | Minuman | 45.000 | 12.000 |
| 4 | 2026-01-09 | Surabaya | Sembako | 120.000 | NULL |
| 5 | 2026-01-12 | Medan | Makanan | 90.000 | 15.000 |
| 6 | 2026-01-15 | Bandung | Sembako | 120.000 | 10.000 |
| 7 | 2026-01-18 | Medan | Minuman | 60.000 | NULL |
| 8 | 2026-01-21 | Surabaya | Makanan | 45.000 | 12.000 |
| 9 | 2026-01-25 | Bandung | Minuman | 30.000 | 10.000 |
| 10 | 2026-01-28 | Medan | Sembako | 120.000 | 15.000 |
SELECT MAX(total) AS transaksi_terbesar,
MIN(total) AS transaksi_terkecil,
MIN(tanggal) AS transaksi_pertama,
MAX(tanggal) AS transaksi_terakhir
FROM penjualan;
-- 120000 | 30000 | 2026-01-03 | 2026-01-28
MIN dan MAX di kolom tanggal ini sering aku pakai buat cek rentang data. Sebelum bikin laporan bulanan, aku selalu jalanin dulu. Mastiin datanya beneran nyampe akhir bulan.
Nggak. Dua-duanya ngelewatin NULL diam-diam.
SELECT MIN(ongkir) AS ongkir_termurah,
MAX(ongkir) AS ongkir_termahal
FROM penjualan;
-- 10000 | 15000
Tiga baris ongkirnya NULL, dan tiga-tiganya dilewati. Hasilnya tetap angka, bukan NULL.
Ini beda dari intuisi banyak orang: mereka nyangka nilai "paling kecil" harusnya kosong. Kalau NULL di kolom kamu artinya nol, bungkus pakai COALESCE(ongkir, 0) dulu. Perilaku NULL lengkapnya ada di glosarium NULL.
Query ini kelihatan masuk akal. Tapi salah.
-- SALAH
SELECT kota, kategori, MAX(total)
FROM penjualan;
MAX memampatkan 10 baris jadi 1 angka. Kolom kota punya 10 nilai berbeda. Database bingung mana yang mau ditampilin.
PostgreSQL langsung nolak dengan pesan error. MySQL versi lama malah ngizinin dan ngasih nilai acak, dan ini lebih bahaya, soalnya hasilnya kelihatan bener padahal ngawur.
Ada tiga cara. Pilih sesuai kebutuhan.
SELECT *
FROM penjualan
ORDER BY total DESC, id_transaksi ASC
LIMIT 1;
-- id 4 | 2026-01-09 | Surabaya | Sembako | 120000
Urutin dari besar ke kecil, ambil satu. Aku selalu nambahin id_transaksi di ORDER BY sebagai pemecah seri. Tanpa itu, hasilnya bisa beda tiap kali query dijalanin.
Di SQL Server, ganti LIMIT jadi SELECT TOP 1.
SELECT *
FROM penjualan
WHERE total = (SELECT MAX(total) FROM penjualan);
Di dataset ini, hasilnya 3 baris: id 4, 6, dan 10. Tiga-tiganya 120.000.
LIMIT 1 cuma ngasih satu. Subquery ngasih semuanya. Beda hasil, beda pertanyaan.
SELECT kota, kategori, tanggal, total
FROM (
SELECT p.*,
ROW_NUMBER() OVER (PARTITION BY kota ORDER BY total DESC, id_transaksi) AS urutan
FROM penjualan p
) t
WHERE urutan = 1;
PARTITION BY bikin penomoran ulang di tiap kota. Baris nomor 1 di tiap kota = transaksi terbesarnya. Sekali query, dapat pemenang semua kota.
SELECT kota,
MAX(total) AS terbesar,
MIN(total) AS terkecil
FROM penjualan
GROUP BY kota
ORDER BY kota;
| kota | terbesar | terkecil |
|---|---|---|
| Bandung | 120.000 | 30.000 |
| Medan | 120.000 | 60.000 |
| Surabaya | 120.000 | 45.000 |
MAX ketiga kota di toko_berkah identik: 120.000. Tapi MIN-nya beda dua kali lipat: Bandung 30.000, Medan 60.000. Kalau kamu cuma laporin MAX, tiga kota ini kelihatan sama padahal perilaku belanjanya beda jauh.
Pelanggan Bandung banyak yang beli eceran kecil. Pelanggan Medan nggak pernah belanja di bawah 60.000. Insight ini nggak muncul dari MAX, dan nggak muncul dari SUM atau AVG juga.
Perilaku lengkapnya bisa kamu cek di dokumentasi PostgreSQL untuk aggregate functions.
Paling gampang: SELECT * FROM penjualan ORDER BY total DESC LIMIT 1. Kalau butuh baris tertinggi per grup, pakai ROW_NUMBER() OVER (PARTITION BY kota ORDER BY total DESC) lalu saring yang nomornya 1.
Iya. Mereka ngelewatin baris NULL dan nggak akan pernah ngasih NULL kecuali seluruh kolomnya NULL. Di toko_berkah, MIN(ongkir) ngasih 10.000 walaupun ada 3 baris NULL.
Bisa. Urutannya ngikutin collation database. MIN(kota) di data ini ngasih Bandung, soalnya B paling awal secara alfabet.
MAX ngasih angka, ORDER BY LIMIT ngasih baris lengkapnya. Kalau kamu cuma butuh angkanya, MAX lebih cepat. Database nggak perlu ngurutin seluruh tabel.
Query di artikel ini udah dites dan siap copy-paste. Latihan langsung di halaman fungsi MAX, editornya jalan di browser.
Lanjut baca: Operator LIKE SQL buat nyari teks pakai wildcard.
Mau praktek langsung? Mulai latihan SQL gratis
Latihan interaktif, langsung di browser.
Analisa per kuartal, hari kerja, atau musim jadi ribet kalau tiap query ngitung ulang atribut tanggal. Tabel kalender nyimpen semua atribut itu sekali, biar tinggal di-JOIN.
Laporan penjualan harian sering bolong di hari tanpa transaksi. Ini cara bikin deret tanggal lengkap di SQL biar tiap hari muncul, walau nilainya nol.
Struktur organisasi tersimpan sebagai kolom id_atasan yang saling nunjuk. Ini cara narik rantai jabatan, hitung total bawahan, dan span of control cuma pakai SQL.