TL;DR
Analisis kohort pendapatan ngelompokin pelanggan berdasarkan bulan transaksi pertama, lalu ngukur berapa rupiah yang tiap kelompok hasilkan di bulan-bulan berikutnya. Bedanya sama kohort retensi cuma satu: yang dijumlahin nilai transaksi, bukan jumlah orang. Karena itu nilainya bisa lewat 100% waktu pelanggan yang bertahan belanja lebih banyak, sesuatu yang nggak pernah muncul di tabel retensi. Query-nya butuh tiga tahap: label kohort per pelanggan, hitung jarak bulan, lalu bagi tiap sel dengan nilai bulan ke-0 pakai FIRST_VALUE.
Analisis kohort pendapatan ngelompokin pelanggan berdasarkan bulan transaksi pertama, lalu ngukur berapa rupiah yang tiap kelompok itu hasilkan di bulan-bulan berikutnya.
Bedanya sama kohort retensi biasa cuma satu: yang dihitung nilai transaksi, bukan jumlah orang.
Perbedaan kecil itu ngasih jawaban yang beda. Kohort pendapatan bisa nunjukin kelompok pelanggan yang jumlahnya nyusut tapi belanjanya naik, dan itu nggak keliatan sama sekali di tabel retensi.
Kohort retensi ngitung berapa persen pelanggan yang balik belanja di bulan ke-1, ke-2, dan seterusnya. Kohort pendapatan ngitung berapa persen nilai belanja bulan pertama yang bertahan di bulan-bulan berikutnya. Satu ngukur kepala, satunya ngukur rupiah, dan dua angka itu sering bergerak ke arah yang beda.
| Aspek | Kohort retensi | Kohort pendapatan |
|---|---|---|
| Yang dihitung | Jumlah pelanggan aktif | Total nilai transaksi |
| Nilai bisa di atas 100%? | Nggak | Bisa, kalau belanja per orang naik |
| Pertanyaan yang dijawab | Berapa orang yang balik? | Berapa rupiah yang balik? |
| Paling kepakai buat | Ukur pengalaman produk | Ukur nilai jangka panjang pelanggan |
Angka di atas 100% itu yang paling menarik. Kalau 40% pelanggan bertahan tapi mereka belanja tiga kali lipat, pendapatan kohortnya naik walau kepalanya nyusut.
Buat dasar konsepnya, cek cohort analysis dulu. Versi retensinya aku bahas di matriks retensi kohort di SQL.
Semua query pakai tabel transaksi dengan kolom id_pelanggan, waktu_transaksi, total, dan status. Dialeknya PostgreSQL.
Empat langkah yang selalu sama:
WITH kohort_pelanggan AS (
SELECT
id_pelanggan,
DATE_TRUNC('month', MIN(waktu_transaksi))::date AS bulan_kohort
FROM transaksi
WHERE status = 'selesai'
GROUP BY id_pelanggan
)
SELECT * FROM kohort_pelanggan LIMIT 10;
Satu pelanggan cuma punya satu bulan kohort selamanya. Itu yang bikin perbandingan antar kohort adil.
Filter status = 'selesai' penting di langkah ini. Kalau transaksi batal ikut kehitung, ada pelanggan yang labelnya kegeser ke bulan yang lebih awal.
WITH kohort_pelanggan AS (
SELECT
id_pelanggan,
DATE_TRUNC('month', MIN(waktu_transaksi))::date AS bulan_kohort
FROM transaksi
WHERE status = 'selesai'
GROUP BY id_pelanggan
),
transaksi_berlabel AS (
SELECT
k.bulan_kohort,
(EXTRACT(YEAR FROM t.waktu_transaksi) - EXTRACT(YEAR FROM k.bulan_kohort)) * 12
+ (EXTRACT(MONTH FROM t.waktu_transaksi) - EXTRACT(MONTH FROM k.bulan_kohort))
AS bulan_ke,
t.total,
t.id_pelanggan
FROM transaksi t
JOIN kohort_pelanggan k ON k.id_pelanggan = t.id_pelanggan
WHERE t.status = 'selesai'
),
rekap AS (
SELECT
bulan_kohort,
bulan_ke,
SUM(total) AS pendapatan,
COUNT(DISTINCT id_pelanggan) AS pelanggan_aktif
FROM transaksi_berlabel
GROUP BY 1, 2
)
SELECT
bulan_kohort,
bulan_ke,
pendapatan,
pelanggan_aktif,
ROUND(
pendapatan * 100.0
/ FIRST_VALUE(pendapatan) OVER (PARTITION BY bulan_kohort ORDER BY bulan_ke)
, 1) AS persen_pendapatan
FROM rekap
ORDER BY bulan_kohort, bulan_ke;
Bagian kunci ada di FIRST_VALUE. Fungsi itu ngambil nilai bulan ke-0 dari tiap kohort sebagai penyebut, tanpa perlu subquery tambahan.
Cara hitung bulan_ke pakai selisih tahun dikali 12 plus selisih bulan. Ini lebih aman daripada ngurangin tanggal, soalnya panjang bulan beda-beda.
Dokumentasi resmi fungsi jendela ada di halaman window functions PostgreSQL.
Pertanyaan yang sering nyusul: satu pelanggan baru rata-rata ngasih berapa rupiah selama 6 bulan pertama? Ini versi kumulatifnya.
WITH kohort_pelanggan AS (
SELECT
id_pelanggan,
DATE_TRUNC('month', MIN(waktu_transaksi))::date AS bulan_kohort
FROM transaksi
WHERE status = 'selesai'
GROUP BY id_pelanggan
),
ukuran_kohort AS (
SELECT bulan_kohort, COUNT(*) AS jml_pelanggan
FROM kohort_pelanggan
GROUP BY 1
),
bulanan AS (
SELECT
k.bulan_kohort,
(EXTRACT(YEAR FROM t.waktu_transaksi) - EXTRACT(YEAR FROM k.bulan_kohort)) * 12
+ (EXTRACT(MONTH FROM t.waktu_transaksi) - EXTRACT(MONTH FROM k.bulan_kohort))
AS bulan_ke,
SUM(t.total) AS pendapatan
FROM transaksi t
JOIN kohort_pelanggan k ON k.id_pelanggan = t.id_pelanggan
WHERE t.status = 'selesai'
GROUP BY 1, 2
)
SELECT
b.bulan_kohort,
b.bulan_ke,
ROUND(
SUM(b.pendapatan) OVER (PARTITION BY b.bulan_kohort ORDER BY b.bulan_ke)
/ u.jml_pelanggan
, 0) AS pendapatan_kumulatif_per_pelanggan
FROM bulanan b
JOIN ukuran_kohort u ON u.bulan_kohort = b.bulan_kohort
ORDER BY 1, 2;
Penyebutnya jumlah pelanggan awal kohort, bukan pelanggan yang masih aktif. Itu yang bikin angkanya kepakai buat nentuin batas biaya akuisisi.
Kalau biaya dapetin satu pelanggan Rp 45.000 dan angka kumulatif bulan ke-6 baru Rp 38.000, kampanyenya belum balik modal.
Dataset toko_berkah punya 4.310 pelanggan dengan transaksi Mei sampai Oktober 2025. Ini potongan matriks pendapatan tiga kohort pertama, dalam persentase dari bulan ke-0.
| Kohort | Bulan 0 | Bulan 1 | Bulan 2 | Bulan 3 |
|---|---|---|---|---|
| Mei 2025 | 100% | 41,2% | 36,8% | 44,5% |
| Jun 2025 | 100% | 38,6% | 33,1% | 35,9% |
| Jul 2025 | 100% | 52,4% | 47,9% | 49,2% |
Kohort Juli jauh lebih kuat dari dua kohort sebelumnya, selisihnya 13,8 poin di bulan ke-1.
Yang menarik, retensi pelanggannya nggak beda jauh. Kohort Mei 28,4% dan kohort Juli 30,1%, cuma beda 1,7 poin.
Jadi kenaikan pendapatannya bukan dari jumlah orang yang balik, tapi dari nilai belanja per orang. Nilai transaksi rata-rata pelanggan Juli yang balik di bulan ke-1 adalah Rp 312.400, sedangkan kohort Mei cuma Rp 198.700.
Selisih 57,2% itu yang bikin matriks pendapatannya beda jauh padahal matriks retensinya mirip.
Kalau kamu cuma lihat tabel retensi, kamu bakal simpulin ketiga kohort ini setara. Padahal nggak.
Ngitung bulan_ke pakai selisih hari dibagi 30. Hasilnya geser di bulan yang panjangnya 31 hari. Pakai selisih tahun dan bulan.
Bandingin kohort yang umurnya beda. Kohort Oktober baru punya bulan ke-0, jadi wajar keliatan lebih jelek. Potong tabelnya di jarak bulan yang sama buat semua kohort.
Lupa filter status transaksi. Transaksi batal bikin label kohort dan nilai pendapatannya sama-sama meleset.
Nyampur mata uang atau nilai sebelum pajak. Sepakati dulu definisi pendapatan yang dipakai, lalu tulis di deskripsi laporannya.
Nganggap sel kosong sama dengan nol. Kohort tanpa transaksi di bulan tertentu nggak muncul barisnya. Gabungin ke deret bulan lengkap kalau kamu bikin grafik.
Ngukur kohort mingguan buat produk yang siklus belinya bulanan. Datanya jadi terlalu tipis dan naik turun nggak karuan. Sesuaikan ukuran kohort sama siklus beli produkmu.
Bisa, dan itu tanda bagus. Angka di atas 100% berarti kelompok pelanggan itu belanja lebih banyak di bulan berikutnya dibanding bulan pertama mereka, walau jumlah orangnya nyusut. Ini kejadian kalau pelanggan yang bertahan naik kelas ke produk yang lebih mahal atau nambah frekuensi belanja. Retensi jumlah orang nggak akan pernah lewat 100%, tapi retensi rupiah bisa.
Sesuaikan sama siklus beli produkmu. Buat produk yang dibeli sebulan sekali atau lebih jarang, pakai kohort bulanan biar tiap sel punya cukup data. Kohort mingguan cocok buat produk dengan pembelian sering seperti makanan atau aplikasi harian. Kalau selnya terlalu tipis, angkanya naik turun tanpa pola dan gampang salah dibaca.
Minimal 6 bulan biar kamu bisa lihat pola di bulan ke-3 sampai ke-5 untuk beberapa kohort sekaligus. Dengan 3 bulan data, cuma kohort pertama yang punya jejak panjang dan kesimpulannya rapuh. Kalau bisnismu baru jalan, tetap bikin tabelnya dari sekarang lalu baca ulang tiap bulan. Polanya baru kelihatan setelah beberapa kohort punya panjang yang sama.
Simpan hasil langkah pertama ke tabel terpisah yang isinya id pelanggan dan bulan kohortnya, lalu perbarui sekali sehari. Bagian yang paling berat biasanya ngitung ulang MIN waktu transaksi buat semua pelanggan tiap kali query jalan. Tambahin juga index di kolom id pelanggan dan waktu transaksi, dan batasi rentang tanggalnya secukupnya.
Tetap berguna, tapi bacanya harus hati-hati. Kalau satu kohort cuma isi 30 pelanggan, satu order besar bisa narik seluruh barisnya. Cek angka mediannya bareng total pendapatan biar kamu tau apakah kenaikannya merata atau cuma dari satu orang. Buat data kecil, kohort per kuartal sering lebih stabil dari kohort bulanan.
Kohort pendapatan ngasih jawaban yang nggak bisa dikasih tabel retensi: berapa rupiah yang beneran bertahan dari tiap kelompok pelanggan.
Query-nya nggak jauh beda dari kohort retensi. Yang berubah cuma yang dijumlahin, dari COUNT(DISTINCT id_pelanggan) jadi SUM(total).
Yang paling menentukan hasilnya bukan query, tapi konsistensi definisi pendapatan dan filter status transaksinya.
Coba jalanin query kedua ke data transaksimu, lalu bandingin hasilnya sama tabel retensi yang udah ada. Lanjut baca SQL repeat purchase buat ngukur pembelian ulang per pelanggan, atau pahami dulu metric yang mau kamu pakai.
Mau praktek langsung? Mulai latihan SQL gratis
Latihan interaktif, langsung di browser.
Kolom waktu isinya angka gede kayak 1704067200 dan bikin bingung? Itu Unix timestamp. Ini cara ngubahnya jadi tanggal beneran di SQL, plus balik lagi.
Data transaksi tersimpan UTC, tapi laporan harus jam WIB. Kalau salah konversi, angka penjualan tengah malam bisa kecatat di tanggal yang salah. Ini cara handle timezone di SQL dengan benar.
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.