SQL untuk Laporan Penjualan Harian: Query Template Siap Pakai
Blog/Tutorial SQL/SQL untuk Laporan Penjualan Harian: Query Template Siap Pakai

SQL untuk Laporan Penjualan Harian: Query Template Siap Pakai

BimaBima
·14 Oktober 2025·11 menit baca

Penulis

Bima

Bima

Founder & Data Professional

Bagikan

TL;DR

Laporan penjualan harian di SQL intinya kelompokin transaksi per tanggal pakai DATE_TRUNC, jumlahin nilainya, lalu urutkan dari tanggal terbaru. Supaya kepakai, tambahin empat hal: filter status biar transaksi batal nggak ikut, deret tanggal lewat generate_series biar hari kosong tetap muncul, fungsi LAG buat banding hari sebelumnya, dan running total buat angka kumulatif bulan berjalan. Lima template siap pakainya ada di artikel ini.

Laporan penjualan harian di SQL intinya satu query: kelompokin transaksi per tanggal, jumlahin nilainya, lalu urutkan dari tanggal terbaru.

Sisanya cuma tambahan yang bikin laporannya kepakai: tanggal kosong yang tetap muncul, perbandingan sama hari kemarin, dan angka kumulatif bulan berjalan.

Di bawah ini lima template yang tinggal kamu ganti nama tabel dan kolomnya. Semua contoh pakai sintaks PostgreSQL, dan aku tandain bagian yang beda kalau kamu pakai MySQL.

Apa aja yang harus ada di laporan penjualan harian?

Laporan penjualan harian minimal punya lima angka: tanggal, jumlah transaksi, total omzet, nilai transaksi rata-rata, dan perbandingan sama hari sebelumnya. Lima ini yang bikin orang bisa ambil keputusan pagi-pagi tanpa nanya lagi. Tambahan per cabang atau per kategori nyusul kalau tim butuh detail.

Yang sering kelupaan itu tanggal tanpa transaksi. Kalau baris tanggalnya hilang, grafiknya bohong dan rata-ratanya melenceng.

Struktur tabel yang dipakai di contoh

Semua query di bawah pakai tabel transaksi dengan kolom berikut. Kalau nama kolom kamu beda, ganti aja bagian ini.

KolomTipeIsi
id_transaksibigintNomor transaksi unik
waktu_transaksitimestampTanggal dan jam transaksi
cabangtextNama cabang toko
kategoritextKategori produk
qtyintJumlah barang
totalnumericNilai transaksi dalam rupiah
statustextselesai, batal, atau refund

Kolom status penting. Transaksi batal yang ikut kehitung bikin omzet kelihatan lebih besar dari kenyataan.

Template 1: rekap penjualan harian dasar

Ini pondasi semua laporan berikutnya. Empat langkah:

  1. Potong waktu_transaksi jadi tanggal doang pakai DATE_TRUNC.
  2. Buang transaksi batal lewat WHERE.
  3. Kelompokin per tanggal pakai GROUP BY.
  4. Hitung jumlah transaksi, total omzet, dan rata-rata nilai transaksi.
SELECT
  DATE_TRUNC('day', waktu_transaksi)::date AS tanggal,
  COUNT(*)                                  AS jml_transaksi,
  SUM(total)                                AS omzet,
  ROUND(AVG(total), 0)                      AS nilai_rata_rata
FROM transaksi
WHERE status = 'selesai'
  AND waktu_transaksi >= DATE_TRUNC('month', CURRENT_DATE)
GROUP BY 1
ORDER BY 1 DESC;

Hasilnya satu baris per tanggal, urut dari yang terbaru. Fungsi DATE_TRUNC aku bahas lebih detail di SQL DATE_TRUNC.

Di MySQL, ganti DATE_TRUNC('day', waktu_transaksi)::date jadi DATE(waktu_transaksi). Sisanya sama.

Template 2: bikin tanggal kosong tetap muncul

Kalau tanggal 14 Oktober nol transaksi, query pertama nggak ngeluarin barisnya sama sekali. Grafik jadi keliatan mulus padahal ada hari mati.

Solusinya bikin deret tanggal dulu, baru LEFT JOIN ke data transaksi.

WITH kalender AS (
  SELECT generate_series(
    DATE_TRUNC('month', CURRENT_DATE)::date,
    CURRENT_DATE,
    interval '1 day'
  )::date AS tanggal
),
harian AS (
  SELECT
    waktu_transaksi::date AS tanggal,
    COUNT(*)              AS jml_transaksi,
    SUM(total)            AS omzet
  FROM transaksi
  WHERE status = 'selesai'
  GROUP BY 1
)
SELECT
  k.tanggal,
  COALESCE(h.jml_transaksi, 0) AS jml_transaksi,
  COALESCE(h.omzet, 0)         AS omzet
FROM kalender k
LEFT JOIN harian h ON h.tanggal = k.tanggal
ORDER BY k.tanggal;

Fungsi generate_series bikin deret tanggal tanpa perlu tabel tambahan. Dokumentasi resminya ada di halaman set returning functions PostgreSQL.

COALESCE ngubah NULL jadi nol, jadi grafiknya jujur. Cara lain pakai tabel kalender permanen aku tulis di bikin tabel kalender di SQL.

Template 3: bandingin sama hari sebelumnya

Angka omzet Rp 18 juta itu bagus atau jelek? Nggak ketauan tanpa pembanding. Fungsi LAG ngambil nilai baris sebelumnya.

WITH harian AS (
  SELECT
    waktu_transaksi::date AS tanggal,
    SUM(total)            AS omzet
  FROM transaksi
  WHERE status = 'selesai'
  GROUP BY 1
)
SELECT
  tanggal,
  omzet,
  LAG(omzet) OVER (ORDER BY tanggal) AS omzet_kemarin,
  ROUND(
    (omzet - LAG(omzet) OVER (ORDER BY tanggal))
    * 100.0
    / NULLIF(LAG(omzet) OVER (ORDER BY tanggal), 0)
  , 1) AS pertumbuhan_persen
FROM harian
ORDER BY tanggal DESC;

NULLIF di penyebut wajib. Tanpa itu, hari dengan omzet nol kemarin bikin query error pembagian nol.

Pembahasan lengkap pola ini ada di SQL day over day.

Template 4: pecah per cabang dalam satu baris tanggal

Manajemen sering minta satu baris per tanggal dengan kolom terpisah per cabang. Ini dikerjain pakai CASE di dalam SUM.

SELECT
  waktu_transaksi::date AS tanggal,
  SUM(CASE WHEN cabang = 'Jakarta' THEN total ELSE 0 END) AS jakarta,
  SUM(CASE WHEN cabang = 'Bandung' THEN total ELSE 0 END) AS bandung,
  SUM(CASE WHEN cabang = 'Surabaya' THEN total ELSE 0 END) AS surabaya,
  SUM(CASE WHEN cabang = 'Medan' THEN total ELSE 0 END)   AS medan,
  SUM(total) AS total_semua
FROM transaksi
WHERE status = 'selesai'
  AND waktu_transaksi >= CURRENT_DATE - interval '30 days'
GROUP BY 1
ORDER BY 1 DESC;

Pola ini namanya conditional aggregation, salah satu jenis aggregate function yang paling sering kepakai buat laporan.

Kekurangannya, nama cabang harus ditulis manual. Kalau cabangnya sering nambah, mending pakai bentuk baris panjang lalu pivot di tool visualisasi.

Template 5: kumulatif bulan berjalan

Pertanyaan yang paling sering muncul di grup WhatsApp kantor: sampai hari ini udah berapa? Ini jawabannya pakai running total.

WITH harian AS (
  SELECT
    waktu_transaksi::date AS tanggal,
    SUM(total)            AS omzet
  FROM transaksi
  WHERE status = 'selesai'
    AND waktu_transaksi >= DATE_TRUNC('month', CURRENT_DATE)
  GROUP BY 1
)
SELECT
  tanggal,
  omzet,
  SUM(omzet) OVER (ORDER BY tanggal
                   ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
    AS omzet_kumulatif
FROM harian
ORDER BY tanggal;

Bagian ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW bikin penjumlahan nambah terus dari baris pertama sampai baris yang lagi dibaca.

Angka kumulatif ini yang biasanya dipasang jadi KPI di layar kantor.

Contoh kasus: laporan harian toko_berkah Oktober 2025

Dataset toko_berkah punya 1.842 transaksi Oktober 2025 dari 4 cabang, dengan total omzet Rp 487.310.000.

Setelah kelima query di atas dijalanin, ini yang keliatan:

  • Rata-rata omzet harian Rp 15.719.677, tapi sebarannya lebar. Hari tertinggi Rp 31.480.000 dan terendah Rp 6.120.000.
  • Sabtu dan Minggu nyumbang 34,7% omzet padahal cuma 8 dari 31 hari. Nilai per hari akhir pekan 1,9 kali hari kerja.
  • Ada 2 tanggal tanpa transaksi di cabang Medan. Tanpa template 2, dua hari itu hilang dari grafik dan rata-rata harian cabang Medan kelihatan 7% lebih tinggi dari sebenernya.
  • Nilai transaksi rata-rata Rp 264.556, sedangkan mediannya Rp 164.500.

Temuan ketiga itu yang paling sering luput. Rata-rata yang dihitung dari hari yang ada transaksinya doang selalu lebih besar dari rata-rata kalender.

Kalau targetmu ditetapin per hari kalender, hitungnya harus pakai penyebut jumlah hari, bukan jumlah baris hasil query.

Kesalahan umum waktu bikin laporan penjualan harian

Lupa filter status. Transaksi batal dan refund ikut kejumlah, omzet jadi lebih besar. Selalu mulai dari WHERE status = 'selesai'.

Salah zona waktu. Kalau kolomnya timestamptz dan server-nya UTC, transaksi jam 7 malam WIB bisa kecatat ke tanggal berikutnya. Konversi dulu pakai AT TIME ZONE 'Asia/Jakarta'.

Nggak nanganin tanggal kosong. Grafik jadi bohong dan rata-rata melenceng ke atas.

Pembagian nol di rumus pertumbuhan. Selalu bungkus penyebut pakai NULLIF(..., 0).

Pakai SELECT * di query laporan. Kolom nambah di kemudian hari bikin dashboard rusak diam-diam. Sebut kolomnya satu per satu.

Ngefilter tanggal pakai fungsi di kolom. Nulis WHERE DATE(waktu_transaksi) = CURRENT_DATE bikin index nggak kepakai. Ganti jadi rentang: WHERE waktu_transaksi >= CURRENT_DATE AND waktu_transaksi < CURRENT_DATE + 1.

FAQ

Query laporan penjualan harian aku lambat, gimana benerinnya?

Cek dulu tiga hal. Pertama, pastiin ada index di kolom tanggal transaksi. Kedua, jangan bungkus kolom tanggal pakai fungsi di klausa WHERE, soalnya index-nya jadi nggak kepakai, ganti pakai rentang tanggal. Ketiga, batasi rentang datanya, jangan tarik semua tahun kalau yang dibutuhin cuma bulan berjalan. Tiga langkah ini biasanya udah motong waktu query secara signifikan.

Gimana cara nampilin tanggal yang nol transaksi di laporan?

Bikin deret tanggal dulu pakai generate_series di PostgreSQL atau tabel kalender di database lain, lalu LEFT JOIN ke hasil agregasi transaksi. Bungkus kolom angkanya pakai COALESCE biar NULL berubah jadi nol. Tanpa langkah ini, hari tanpa transaksi hilang dari hasil dan bikin grafik tren kelihatan lebih mulus dari kenyataan.

Sebaiknya rekap harian dihitung ulang tiap kali atau disimpan di tabel?

Kalau datanya di bawah jutaan baris dan query-nya jalan di bawah beberapa detik, hitung ulang aja tiap kali. Begitu data mulai berat atau laporannya dibuka banyak orang tiap pagi, simpan hasilnya ke tabel ringkasan yang diperbarui sekali sehari. Cara kedua bikin dashboard jauh lebih ringan dan angkanya konsisten buat semua orang.

Transaksi tengah malam masuk tanggal yang mana?

Tergantung zona waktu kolomnya. Kalau tipe datanya timestamptz dan server pakai UTC, transaksi jam 7 malam WIB tercatat sebagai hari berikutnya dalam UTC. Konversi dulu pakai AT TIME ZONE Asia/Jakarta sebelum dipotong jadi tanggal. Kalau nggak, laporan harianmu bakal geser dan angkanya nggak cocok sama laporan kasir.

Perlu pisahin transaksi refund atau cukup difilter aja?

Tergantung kebutuhan tim keuangan. Buat laporan operasional harian, filter status selesai aja udah cukup. Tapi kalau kamu perlu angka bersih, hitung refund sebagai nilai negatif lalu jumlahkan bareng. Yang penting metode-nya konsisten dan ditulis di deskripsi laporan, biar angkanya nggak beda sama laporan lain di kantor.

Penutup

Lima template di atas nutup hampir semua permintaan laporan penjualan harian. Rekap dasar buat angkanya, kalender buat kejujuran grafiknya, LAG buat konteks, CASE buat detail cabang, dan running total buat pertanyaan sampai hari ini berapa.

Yang bikin laporanmu dipercaya bukan query-nya, tapi filter status dan penanganan tanggal kosongnya.

Coba salin template 1, ganti nama tabelmu, dan jalanin di NgulikSQL hari ini. Lanjut baca cara isi tanggal kosong di SQL kalau data harianmu banyak bolongnya.

Coba Langsung

Mau praktek langsung? Mulai latihan SQL gratis

Latihan interaktif, langsung di browser.

Buka NgulikSQL →
Bagikan:
Bima
Ditulis oleh

Bima

Founder & Data Professional

Founder Ngulik Data. Passionate about making data analysis accessible for everyone.

Artikel terkait

Cara Membuat Pivot Dinamis di SQL (Panduan 2026)
Tutorial SQL
19 Juli 2026•10 menit baca

Cara Membuat Pivot Dinamis di SQL (Panduan 2026)

Pivot dinamis di SQL ngubah baris jadi kolom tanpa kamu hardcode nama kolomnya. Ini cara bikinnya pakai CASE WHEN dan versi yang kolomnya ngikut data.

BimaBima
Menghitung YTD, QTD, dan MTD di SQL (Panduan 2026)
Tutorial SQL
17 Juli 2026•9 menit baca

Menghitung YTD, QTD, dan MTD di SQL (Panduan 2026)

YTD, QTD, dan MTD ngukur total dari awal tahun, kuartal, atau bulan sampai hari ini. Ini cara ngitungnya di SQL pakai DATE_TRUNC, plus satu query gabungan.

BimaBima
Gap and Island Analysis di SQL
Tutorial SQL
15 Juli 2026•10 menit baca

Gap and Island Analysis di SQL

Gap and island analysis di SQL nyari deret data yang berturut-turut (island) dan celah yang bolong (gap). Cocok buat hitung streak login atau cari tanggal transaksi yang hilang.

BimaBima
Kembali ke Blog
Ngulik Data logoNgulik Data

Platform edukasi data lengkap untuk professionals Indonesia. Belajar SQL, Data Analysis, dan lebih banyak lagi dengan praktek langsung dan feedback real-time.

© 2026 Ngulik Data. Semua hak dilindungi.

TAUTAN
BantuanHargaDatasetBlogAfiliasi
LEGAL
Syarat & KetentuanKebijakan Privasi
Ngulik Data
DatasetLeaderboardBlogStore