TL;DR
Modus adalah nilai yang paling sering muncul di sebuah kolom. Di SQL, cara paling portabel menghitungnya adalah GROUP BY kolomnya, COUNT jumlah kemunculan, lalu ORDER BY hitungan itu menurun dan ambil baris teratas. PostgreSQL punya jalan pintas lewat MODE() WITHIN GROUP. Buat data yang punya lebih dari satu modus, pakai window function RANK biar semua nilai dengan frekuensi tertinggi ikut kebawa.
Modus di SQL adalah nilai yang paling sering muncul di sebuah kolom. Beda sama rata-rata atau median, modus jawab pertanyaan "nilai mana yang paling sering kejadian?".
Masalahnya, kebanyakan database nggak punya satu fungsi MODE() yang jalan di mana-mana. Tapi kamu tetap bisa ngitungnya cuma pakai GROUP BY, COUNT, dan ORDER BY.
Di bawah ini aku bahas cara portabel yang jalan di semua database, jalan pintas khusus PostgreSQL, cara nanganin modus ganda, sampai modus per cabang di dataset toko_berkah.
Modus adalah nilai yang paling sering muncul dalam satu kumpulan data. Kalau metode bayar di 100 transaksi isinya 60 QRIS, 30 tunai, 10 kartu, modusnya QRIS. Modus dipakai buat data kategori: metode bayar, kota, produk, status. Buat data kategori kayak gini, rata-rata dan median nggak ada artinya, cuma modus yang masuk akal.
Kalau kamu masih nyampur konsep modus, median, dan mean, aku pisahin bedanya di glosarium modus. Buat median, ada pembahasan sendiri di Menghitung Median dan Percentile di SQL.
Cara paling portabel: kelompokin nilainya, hitung berapa kali muncul, urutin dari yang paling banyak, ambil satu teratas. Empat baris, jalan di MySQL, PostgreSQL, SQL Server, SQLite, semuanya.
SELECT metode_bayar, COUNT(*) AS jumlah
FROM transaksi
GROUP BY metode_bayar
ORDER BY jumlah DESC
LIMIT 1;
GROUP BY ngumpulin baris per metode bayar. COUNT ngitung jumlah transaksi tiap metode. ORDER BY nyusun dari yang terbanyak. LIMIT 1 ngambil pemenangnya.
Di SQL Server, ganti LIMIT 1 jadi TOP 1 di depan: SELECT TOP 1 metode_bayar, COUNT(*) .... Sisanya sama.
Cara ini enak soalnya kamu juga langsung lihat angka frekuensinya, bukan cuma nama modusnya. Kalau selisih antara juara 1 dan 2 tipis, kamu langsung tau.
Kalau kamu pakai PostgreSQL, ada jalan pintas. Fungsi MODE() ngambil nilai terbanyak dalam satu baris hasil.
SELECT MODE() WITHIN GROUP (ORDER BY metode_bayar) AS modus_bayar
FROM transaksi;
Sintaks WITHIN GROUP (ORDER BY kolom) itu bagian wajibnya. Hasilnya satu nilai: metode bayar yang paling sering muncul. Rapi buat dipakai di dalam query yang lebih besar.
Satu catatan: kalau ada seri, MODE() cuma balikin satu nilai, yaitu yang pertama secara urutan. Jadi kalau kamu curiga ada modus ganda, cara ini bisa nyembunyiin fakta itu. Detail perilakunya ada di dokumentasi agregat PostgreSQL.
Data beneran sering punya seri. Dua produk sama-sama laku 214 kali. Kalau kamu pakai LIMIT 1, salah satunya ilang tanpa kamu sadari. Solusinya window function RANK.
WITH freq AS (
SELECT metode_bayar,
COUNT(*) AS jumlah,
RANK() OVER (ORDER BY COUNT(*) DESC) AS peringkat
FROM transaksi
GROUP BY metode_bayar
)
SELECT metode_bayar, jumlah
FROM freq
WHERE peringkat = 1;
RANK ngasih peringkat 1 ke semua nilai dengan frekuensi tertinggi. Kalau ada dua yang seri di puncak, dua-duanya dapat peringkat 1 dan dua-duanya kebawa. Nggak ada yang ketinggalan.
Ini bedanya sama LIMIT 1: LIMIT maksa milih satu, RANK jujur nunjukin kalau modusnya ternyata dua. Buat laporan yang harus akurat, aku selalu pakai versi RANK. Konsep window function ini juga aku pakai di Menghitung Persen dari Total.
Pertanyaan yang lebih sering muncul di kerjaan: produk terlaris di tiap cabang. Ini modus per grup. Pakai ROW_NUMBER dengan PARTITION.
WITH freq AS (
SELECT cabang,
produk,
COUNT(*) AS jumlah,
ROW_NUMBER() OVER (
PARTITION BY cabang
ORDER BY COUNT(*) DESC
) AS rn
FROM transaksi
GROUP BY cabang, produk
)
SELECT cabang, produk AS produk_terlaris, jumlah
FROM freq
WHERE rn = 1;
PARTITION BY cabang bikin peringkat dihitung ulang buat tiap cabang. ROW_NUMBER ngasih nomor urut, jadi rn = 1 itu produk paling laku di cabang itu. Hasilnya satu baris per cabang.
Kalau kamu mau seri ikut kebawa di sini juga, ganti ROW_NUMBER jadi RANK. Bedanya: ROW_NUMBER maksa satu pemenang per cabang, RANK bisa balikin dua kalau seri.
Dataset toko_berkah punya 8.412 transaksi dari 6 cabang, Januari sampai Juni 2026. Aku ngitung modus di beberapa kolom, dan dua angka bikin kaget pemiliknya.
Modus metode bayar keseluruhan: QRIS, 4.986 transaksi atau 59% dari total. Mereka kira tunai masih dominan. Ternyata udah lama ketinggalan.
Yang lebih menarik pas dipecah per cabang. Modus metode bayar beda-beda:
| Cabang | Modus metode bayar | Porsi |
|---|---|---|
| Depok | QRIS | 71% |
| Bekasi | QRIS | 63% |
| Cibinong | Tunai | 52% |
Cibinong satu-satunya cabang yang modusnya masih tunai. Ternyata lokasinya di pasar tradisional, pelanggannya beda profil. Temuan ini bikin mereka nunda pasang mesin EDC di Cibinong dan malah nambah kasir tunai.
Modus jam transaksi juga kepakai: puncaknya jam 17:00, bukan jam makan siang kayak yang mereka duga. Jadwal shift kasir ikut digeser.
Lupa ada modus ganda. LIMIT 1 dan MODE() cuma balikin satu nilai. Kalau ada seri, kamu nggak akan tau. Pakai RANK kalau akurasi penting.
NULL ikut kehitung. Kalau kolomnya ada NULL, GROUP BY bikin grup NULL sendiri yang kadang jadi "modus" palsu. Tambahin WHERE metode_bayar IS NOT NULL kalau NULL nggak relevan.
Modus dipakai buat data angka kontinu. Modus harga transaksi yang unik-unik hampir nggak ada gunanya, tiap nilai muncul sekali. Buat angka kontinu, median atau rata-rata lebih masuk akal.
ORDER BY salah arah. Lupa nulis DESC bikin kamu dapat nilai yang paling jarang, bukan yang paling sering. Cek selalu arah urutannya.
Tergantung databasenya. PostgreSQL punya MODE() WITHIN GROUP, dan Oracle punya fungsi STATS_MODE. Tapi MySQL, SQL Server, dan SQLite nggak punya fungsi modus bawaan. Buat itu kamu hitung manual pakai GROUP BY, COUNT, dan ORDER BY. Cara manual ini portabel dan jalan di semua database, jadi aku lebih sering pakai itu biar query-nya nggak kunci ke satu jenis database.
ROW_NUMBER selalu ngasih nomor urut unik, jadi selalu balikin tepat satu pemenang walaupun ada seri. RANK ngasih peringkat yang sama buat nilai yang seri, jadi kalau dua nilai sama-sama terbanyak, dua-duanya dapat peringkat 1 dan dua-duanya kebawa. Pakai ROW_NUMBER kalau kamu maksa satu jawaban. Pakai RANK kalau kamu mau jujur soal modus ganda.
Tambahin filter WHERE kolom IS NOT NULL sebelum GROUP BY. Kalau nggak, NULL diperlakukan sebagai satu grup sendiri dan bisa keluar sebagai modus kalau jumlahnya banyak. Tapi kadang NULL yang banyak itu justru info penting, misalnya banyak transaksi tanpa metode bayar tercatat. Jadi putuskan dulu: NULL ini mau kamu anggap nilai atau mau kamu buang.
Bisa di PostgreSQL. Gabungin MODE() WITHIN GROUP (ORDER BY kolom) buat modus dan PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY kolom) buat median dalam satu SELECT. Keduanya fungsi ordered-set aggregate, jadi sintaksnya mirip. Di database lain yang nggak punya fungsi ini, kamu perlu subquery terpisah buat masing-masing lalu gabungin hasilnya.
Kemungkinan kamu pakai RANK atau ROW_NUMBER dengan filter peringkat = 1, bukan LIMIT 1. Versi window function memang bisa balikin banyak baris kalau ada seri di puncak. Kalau kamu beneran cuma mau satu baris apa pun yang terjadi, pakai versi GROUP BY dengan ORDER BY dan LIMIT 1. Kalau mau semua modus yang seri, versi RANK itu udah bener.
Ringkasnya:
Buka satu tabel transaksi kamu, pilih kolom kategori kayak metode bayar atau kota, tempel query GROUP BY di atas. Dalam satu run kamu udah tau nilai apa yang paling sering muncul.
Mau latihan GROUP BY dan window function sambil jalanin query beneran? Coba modul di glosarium NgulikSQL, ada dataset yang bisa langsung kamu query.
Mau praktek langsung? Mulai latihan SQL gratis
Latihan interaktif, langsung di browser.
Nambah 30 hari ke tanggal invoice, ngurangin sebulan buat cari periode lalu, ngitung selisih hari antar order. Semua itu aritmetika tanggal, dan SQL punya operator INTERVAL buat ngerjainnya.
Ngurangin tahun sekarang sama tahun lahir kelihatan gampang, tapi hasilnya sering meleset setahun kalau ulang tahunnya belum lewat. Ini cara hitung umur yang akurat di SQL.
Cara pakai DATE_TRUNC di SQL buat memotong timestamp jadi awal bulan, minggu, atau jam. Kunci bikin laporan penjualan per periode yang rapi.