Data 60 baris sudah masuk dan lolos verifikasi; sekarang giliran pertanyaan pemilik toko yang menunggu jawaban. Pada bagian ini kita menyusun analisis.sql — tujuh query yang dijalankan sekali jalan dengan satu perintah, bukan diketik ulang setiap kali laporan dibutuhkan. Materi intinya berasal dari Chapter Fungsi Agregat, Menggabungkan Tabel dengan JOIN, Mengelompokan Hasil dengan GROUP BY dan HAVING, Window Functions, Common Table Expressions, dan Bekerja dengan Tanggal dan Waktu.
Tujuh Pertanyaan Analisis #
| # | Pertanyaan | Teknik utama |
|---|---|---|
| 1 | Berapa omzet tiap bulan? | strftime() + GROUP BY |
| 2 | Produk apa yang menopang omzet? | JOIN + ORDER BY + LIMIT |
| 3 | Seberapa besar pangsa tiap kategori? | CTE + subquery + ROUND() |
| 4 | Berapa omzet berjalan sampai bulan ini? | SUM() OVER |
| 5 | Produk mana juara di kategorinya? | RANK() OVER PARTITION BY |
| 6 | Bagaimana pertumbuhan bulan-ke-bulan? | LAG() |
| 7 | Seberapa padat hari penjualan? | COUNT(DISTINCT) + AVG() |
Kode Lengkap Bagian 3 #
Simpan listing berikut sebagai analisis.sql:
-- analisis.sql — Analisis Penjualan
-- Bagian 3: query analisis penjualan semester I 2025.
-- Jalankan: sqlite3 penjualan.db < analisis.sql
-- Prasyarat: schema.sql dan import.sql sudah dijalankan.
.headers on
.mode column
-- 1. Omzet per bulan: berapa pendapatan tiap bulan?
.print === 1. Omzet per Bulan ===
SELECT strftime('%Y-%m', tanggal) AS bulan,
COUNT(*) AS transaksi,
SUM(jumlah) AS item_terjual,
SUM(total_harga) AS omzet
FROM penjualan
GROUP BY bulan
ORDER BY bulan;
.print
-- 2. Lima produk dengan omzet terbesar.
.print === 2. Lima Produk Teratas ===
SELECT pr.nama,
pr.kategori,
SUM(pe.jumlah) AS item_terjual,
SUM(pe.total_harga) AS omzet
FROM penjualan pe
JOIN produk pr ON pr.id = pe.produk_id
GROUP BY pr.id
ORDER BY omzet DESC
LIMIT 5;
.print
-- 3. Pangsa omzet tiap kategori dalam persen.
.print === 3. Pangsa Kategori ===
WITH omzet_kategori AS (
SELECT pr.kategori, SUM(pe.total_harga) AS omzet
FROM penjualan pe
JOIN produk pr ON pr.id = pe.produk_id
GROUP BY pr.kategori
)
SELECT kategori,
omzet,
ROUND(omzet * 100.0 / (SELECT SUM(omzet) FROM omzet_kategori), 1) AS persen
FROM omzet_kategori
ORDER BY omzet DESC;
.print
-- 4. Omzet kumulatif: total berjalan dari Januari ke bulan berjalan.
.print === 4. Omzet Kumulatif Bulanan ===
WITH omzet_bulanan AS (
SELECT strftime('%Y-%m', tanggal) AS bulan,
SUM(total_harga) AS omzet
FROM penjualan
GROUP BY bulan
)
SELECT bulan,
omzet,
SUM(omzet) OVER (ORDER BY bulan) AS omzet_kumulatif
FROM omzet_bulanan
ORDER BY bulan;
.print
-- 5. Peringkat produk di dalam kategorinya masing-masing.
.print === 5. Peringkat Produk per Kategori ===
WITH omzet_produk AS (
SELECT pr.kategori, pr.nama, SUM(pe.total_harga) AS omzet
FROM penjualan pe
JOIN produk pr ON pr.id = pe.produk_id
GROUP BY pr.id
)
SELECT kategori,
nama,
omzet,
RANK() OVER (PARTITION BY kategori ORDER BY omzet DESC) AS peringkat
FROM omzet_produk
ORDER BY kategori, peringkat;
.print
-- 6. Pertumbuhan omzet bulan-ke-bulan dalam persen.
.print === 6. Pertumbuhan Bulanan ===
WITH omzet_bulanan AS (
SELECT strftime('%Y-%m', tanggal) AS bulan,
SUM(total_harga) AS omzet
FROM penjualan
GROUP BY bulan
)
SELECT bulan,
omzet,
LAG(omzet) OVER (ORDER BY bulan) AS omzet_bulan_lalu,
ROUND((omzet - LAG(omzet) OVER (ORDER BY bulan)) * 100.0
/ LAG(omzet) OVER (ORDER BY bulan), 1) AS pertumbuhan_persen
FROM omzet_bulanan
ORDER BY bulan;
.print
-- 7. Statistik harian: hari aktif dan nilai rata-rata transaksi.
.print === 7. Statistik Harian ===
SELECT COUNT(*) AS transaksi,
COUNT(DISTINCT tanggal) AS hari_aktif,
ROUND(COUNT(*) * 1.0 / COUNT(DISTINCT tanggal), 2) AS transaksi_per_hari,
ROUND(AVG(total_harga)) AS rata_rata_per_transaksi
FROM penjualan;Menjalankan Seluruh Analisis #
sqlite3 penjualan.db < analisis.sqlOutput yang Dihasilkan:
=== 1. Omzet per Bulan ===
bulan transaksi item_terjual omzet
------- --------- ------------ -------
2025-01 10 29 859000
2025-02 10 32 958000
2025-03 10 33 1006000
2025-04 10 34 1062000
2025-05 10 30 1019000
2025-06 10 35 1170000
=== 2. Lima Produk Teratas ===
nama kategori item_terjual omzet
----------------------- ------------ ------------ -------
Kopi Gayo 250 g Minuman 36 2340000
Beras Pandan Wangi 5 kg Makanan 11 792000
Teh Melati 100 g Minuman 42 756000
Minyak Goreng 2 L Rumah Tangga 17 646000
Gula Aren 500 g Makanan 18 450000
=== 3. Pangsa Kategori ===
kategori omzet persen
------------ ------- ------
Minuman 3096000 51.0
Makanan 1626000 26.8
Rumah Tangga 1352000 22.3
=== 4. Omzet Kumulatif Bulanan ===
bulan omzet omzet_kumulatif
------- ------- ---------------
2025-01 859000 859000
2025-02 958000 1817000
2025-03 1006000 2823000
2025-04 1062000 3885000
2025-05 1019000 4904000
2025-06 1170000 6074000
=== 5. Peringkat Produk per Kategori ===
kategori nama omzet peringkat
------------ ----------------------- ------- ---------
Makanan Beras Pandan Wangi 5 kg 792000 1
Makanan Gula Aren 500 g 450000 2
Makanan Sarden Kaleng 155 g 384000 3
Minuman Kopi Gayo 250 g 2340000 1
Minuman Teh Melati 100 g 756000 2
Rumah Tangga Minyak Goreng 2 L 646000 1
Rumah Tangga Sabun Mandi Cair 450 ml 418000 2
Rumah Tangga Pasta Gigi 190 g 288000 3
=== 6. Pertumbuhan Bulanan ===
bulan omzet omzet_bulan_lalu pertumbuhan_persen
------- ------- ---------------- ------------------
2025-01 859000
2025-02 958000 859000 11.5
2025-03 1006000 958000 5.0
2025-04 1062000 1006000 5.6
2025-05 1019000 1062000 -4.0
2025-06 1170000 1019000 14.8
=== 7. Statistik Harian ===
transaksi hari_aktif transaksi_per_hari rata_rata_per_transaksi
--------- ---------- ------------------ -----------------------
60 48 1.25 101233.0 Hasilnya memuat satu cerita utuh. Omzet naik dari 859000 pada Januari menjadi 1170000 pada Juni, dengan satu-satunya penurunan pada Mei (-4.0%) sebelum melonjak kembali 14.8%. Kopi Gayo 250 g menopang omzet terbesar — 2340000 dari total 6074000, hampir empat dari sepuluh rupiah pendapatan toko — sementara kategori Minuman menguasai 51.0% pangsa pasar. Bagian sisanya membedah query-query yang menghasilkan angka-angka tersebut.
Bedah Query Kunci #
Lima query dibedah lebih dalam di bawah ini; dua sisanya memakai pola yang sama. Sesi interaktifnya melanjutkan sesi Query 1, sehingga .headers on dan .mode column masih aktif — salin kembali keduanya bila Anda membuka sesi baru.
Query 1 — Omzet per Bulan #
sqlite> .headers on
sqlite> .mode column
sqlite> SELECT strftime('%Y-%m', tanggal) AS bulan,
...> COUNT(*) AS transaksi,
...> SUM(jumlah) AS item_terjual,
...> SUM(total_harga) AS omzet
...> FROM penjualan
...> GROUP BY bulan
...> ORDER BY bulan;
bulan transaksi item_terjual omzet
------- --------- ------------ -------
2025-01 10 29 859000
2025-02 10 32 958000
2025-03 10 33 1006000
2025-04 10 34 1062000
2025-05 10 30 1019000
2025-06 10 35 1170000strftime('%Y-%m', tanggal)— memotong tanggal ISO menjadi label bulan2025-01,2025-02, dan seterusnya; inilah jembatan dari kolom tanggal ke pengelompokan bulanan (Chapter Bekerja dengan Tanggal dan Waktu).GROUP BY bulan— alias dari daftarSELECTboleh dipakai langsung di sini, dan satu baris keluar untuk setiap bulan.ORDER BY bulan— label ISO urut secara alfabetis sekaligus kronologis; tanpaORDER BY, urutan bulan tidak dijamin.
Query 3 — Pangsa Kategori dengan CTE #
sqlite> WITH omzet_kategori AS (
...> SELECT pr.kategori, SUM(pe.total_harga) AS omzet
...> FROM penjualan pe
...> JOIN produk pr ON pr.id = pe.produk_id
...> GROUP BY pr.kategori
...> )
...> SELECT kategori,
...> omzet,
...> ROUND(omzet * 100.0 / (SELECT SUM(omzet) FROM omzet_kategori), 1) AS persen
...> FROM omzet_kategori
...> ORDER BY omzet DESC;
kategori omzet persen
------------ ------- ------
Minuman 3096000 51.0
Makanan 1626000 26.8
Rumah Tangga 1352000 22.3 WITH omzet_kategori AS (...)— CTE menampung pengelompokan omzet per kategori lebih dulu, sehingga query utama tinggal memakai hasilnya (Chapter Common Table Expressions).(SELECT SUM(omzet) FROM omzet_kategori)— subquery skalar yang menghitung total6074000sekali sebagai penyebut pembagian.omzet * 100.0 / ...— angka100.0memaksa pembagian pecahan; tanpa.0, SQLite membagi bilangan bulat dan memotong hasilnya menjadi0atau1.ROUND(..., 1)— membulatkan ke satu digit desimal sehingga hasilnya siap dipakai untuk diagram lingkaran:51.0,26.8,22.3.
Query 4 — Omzet Kumulatif dengan SUM() OVER #
sqlite> WITH omzet_bulanan AS (
...> SELECT strftime('%Y-%m', tanggal) AS bulan,
...> SUM(total_harga) AS omzet
...> FROM penjualan
...> GROUP BY bulan
...> )
...> SELECT bulan,
...> omzet,
...> SUM(omzet) OVER (ORDER BY bulan) AS omzet_kumulatif
...> FROM omzet_bulanan
...> ORDER BY bulan;
bulan omzet omzet_kumulatif
------- ------- ---------------
2025-01 859000 859000
2025-02 958000 1817000
2025-03 1006000 2823000
2025-04 1062000 3885000
2025-05 1019000 4904000
2025-06 1170000 6074000 SUM(omzet) OVER (ORDER BY bulan)— window function yang menjumlahkan baris saat ini dengan seluruh baris sebelumnya; hasilnya total berjalan (running total) dari Januari sampai bulan tersebut (Chapter Window Functions, tersedia sejak SQLite 3.25).- Bedanya dengan
GROUP BY— agregat biasa melipat banyak baris menjadi satu, sedangkan window function menambah kolom tanpa menghilangkan satu baris pun; kolomomzetdanomzet_kumulatifhidup berdampingan. - Baris terakhir
6074000— omzet kumulatif bulan Juni yang persis sama dengan total semester; kecocokan antar-query seperti ini adalah cara cepat memeriksa bahwa analisis tidak saling bertentangan.
Query 5 — Peringkat Produk per Kategori dengan RANK() #
sqlite> WITH omzet_produk AS (
...> SELECT pr.kategori, pr.nama, SUM(pe.total_harga) AS omzet
...> FROM penjualan pe
...> JOIN produk pr ON pr.id = pe.produk_id
...> GROUP BY pr.id
...> )
...> SELECT kategori,
...> nama,
...> omzet,
...> RANK() OVER (PARTITION BY kategori ORDER BY omzet DESC) AS peringkat
...> FROM omzet_produk
...> ORDER BY kategori, peringkat;
kategori nama omzet peringkat
------------ ----------------------- ------- ---------
Makanan Beras Pandan Wangi 5 kg 792000 1
Makanan Gula Aren 500 g 450000 2
Makanan Sarden Kaleng 155 g 384000 3
Minuman Kopi Gayo 250 g 2340000 1
Minuman Teh Melati 100 g 756000 2
Rumah Tangga Minyak Goreng 2 L 646000 1
Rumah Tangga Sabun Mandi Cair 450 ml 418000 2
Rumah Tangga Pasta Gigi 190 g 288000 3 PARTITION BY kategori— window dibagi tiga ruang terpisah, satu per kategori, sehingga peringkat dihitung di dalam kategorinya masing-masing.ORDER BY omzet DESCdi dalamOVER— menentukan urutan pemberian peringkat: omzet terbesar di tiap partisi mendapat peringkat 1 — Beras Pandan Wangi 5 kg untukMakanan, Kopi Gayo 250 g untukMinuman, Minyak Goreng 2 L untukRumah Tangga.ORDER BY kategori, peringkatdi query utama — memastikan urutan baris yang tercetak tetap sama setiap eksekusi.
Query 6 — Pertumbuhan Bulanan dengan LAG() #
Query keenam memakai LAG(omzet) OVER (ORDER BY bulan) untuk melihat omzet bulan sebelumnya, lalu menghitung persentasenya — hasilnya tabel pertumbuhan_persen pada output penuh di atas: 11.5, 5.0, 5.6, -4.0, 14.8.
LAG(omzet)— mengambil nilaiomzetdari satu baris sebelumnya dalam urutanbulan; di baris Februari, kolomomzet_bulan_laluberisi859000.ROUND((omzet - ...) * 100.0 / ..., 1)— omzet bulan berjalan dikurangi omzet bulan lalu, dibagi omzet bulan lalu, dikali100.0, lalu dibulatkan ke satu digit.- Baris Januari kosong —
LAG()tidak punya baris sebelumnya untuk bulan pertama, sehingga hasilnyaNULLdan kolom pertumbuhannya kosong. NilaiNULLdi sini tepat: pertumbuhan Januari memang tidak terdefinisi, bukan nol.
Kerapian Output sebagai Kontrak #
Seluruh query di atas ditutup ORDER BY eksplisit. Query tanpa ORDER BY boleh mengembalikan baris dalam urutan apa pun, dan urutan yang berubah-ubah antar eksekusi membuat laporan terasa salah meski angkanya benar. Angka NULL pada pertumbuhan Januari juga sengaja dibiarkan, bukan diganti 0 — laporan yang jujur menandai data yang tidak terdefinisi.
Query analisis siap dipakai. Bagian 4 merakit hasil-hasilnya menjadi laporan akhir sekali jalan beserta eksport ke CSV dan JSON. Berkas lengkap bagian ini (schema.sql, data_penjualan.csv, import.sql, dan analisis.sql) tersedia pada bagian Berkas Sumber di bawah halaman.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
analisis.sql
Unduh berkas-- analisis.sql — Analisis Penjualan
-- Bagian 3: query analisis penjualan semester I 2025.
-- Jalankan: sqlite3 penjualan.db < analisis.sql
-- Prasyarat: schema.sql dan import.sql sudah dijalankan.
.headers on
.mode column
-- 1. Omzet per bulan: berapa pendapatan tiap bulan?
.print === 1. Omzet per Bulan ===
SELECT strftime('%Y-%m', tanggal) AS bulan,
COUNT(*) AS transaksi,
SUM(jumlah) AS item_terjual,
SUM(total_harga) AS omzet
FROM penjualan
GROUP BY bulan
ORDER BY bulan;
.print
-- 2. Lima produk dengan omzet terbesar.
.print === 2. Lima Produk Teratas ===
SELECT pr.nama,
pr.kategori,
SUM(pe.jumlah) AS item_terjual,
SUM(pe.total_harga) AS omzet
FROM penjualan pe
JOIN produk pr ON pr.id = pe.produk_id
GROUP BY pr.id
ORDER BY omzet DESC
LIMIT 5;
.print
-- 3. Pangsa omzet tiap kategori dalam persen.
.print === 3. Pangsa Kategori ===
WITH omzet_kategori AS (
SELECT pr.kategori, SUM(pe.total_harga) AS omzet
FROM penjualan pe
JOIN produk pr ON pr.id = pe.produk_id
GROUP BY pr.kategori
)
SELECT kategori,
omzet,
ROUND(omzet * 100.0 / (SELECT SUM(omzet) FROM omzet_kategori), 1) AS persen
FROM omzet_kategori
ORDER BY omzet DESC;
.print
-- 4. Omzet kumulatif: total berjalan dari Januari ke bulan berjalan.
.print === 4. Omzet Kumulatif Bulanan ===
WITH omzet_bulanan AS (
SELECT strftime('%Y-%m', tanggal) AS bulan,
SUM(total_harga) AS omzet
FROM penjualan
GROUP BY bulan
)
SELECT bulan,
omzet,
SUM(omzet) OVER (ORDER BY bulan) AS omzet_kumulatif
FROM omzet_bulanan
ORDER BY bulan;
.print
-- 5. Peringkat produk di dalam kategorinya masing-masing.
.print === 5. Peringkat Produk per Kategori ===
WITH omzet_produk AS (
SELECT pr.kategori, pr.nama, SUM(pe.total_harga) AS omzet
FROM penjualan pe
JOIN produk pr ON pr.id = pe.produk_id
GROUP BY pr.id
)
SELECT kategori,
nama,
omzet,
RANK() OVER (PARTITION BY kategori ORDER BY omzet DESC) AS peringkat
FROM omzet_produk
ORDER BY kategori, peringkat;
.print
-- 6. Pertumbuhan omzet bulan-ke-bulan dalam persen.
.print === 6. Pertumbuhan Bulanan ===
WITH omzet_bulanan AS (
SELECT strftime('%Y-%m', tanggal) AS bulan,
SUM(total_harga) AS omzet
FROM penjualan
GROUP BY bulan
)
SELECT bulan,
omzet,
LAG(omzet) OVER (ORDER BY bulan) AS omzet_bulan_lalu,
ROUND((omzet - LAG(omzet) OVER (ORDER BY bulan)) * 100.0
/ LAG(omzet) OVER (ORDER BY bulan), 1) AS pertumbuhan_persen
FROM omzet_bulanan
ORDER BY bulan;
.print
-- 7. Statistik harian: hari aktif dan nilai rata-rata transaksi.
.print === 7. Statistik Harian ===
SELECT COUNT(*) AS transaksi,
COUNT(DISTINCT tanggal) AS hari_aktif,
ROUND(COUNT(*) * 1.0 / COUNT(DISTINCT tanggal), 2) AS transaksi_per_hari,
ROUND(AVG(total_harga)) AS rata_rata_per_transaksi
FROM penjualan;
import.sql
Unduh berkas-- import.sql — Analisis Penjualan
-- Bagian 2: import data_penjualan.csv ke tabel penjualan + verifikasi.
-- Jalankan: sqlite3 penjualan.db < import.sql
-- Prasyarat: schema.sql sudah dijalankan pada database yang sama.
PRAGMA foreign_keys = ON;
.headers on
.mode column
-- 1. Kosongkan tabel supaya skrip aman diulang kapan pun.
DELETE FROM penjualan;
-- 2. Import 60 baris data: format CSV, baris judul dilewati.
.import --csv --skip 1 data_penjualan.csv penjualan
-- 3. Verifikasi jumlah baris.
.print Jumlah baris:
SELECT COUNT(*) AS baris_penjualan FROM penjualan;
-- 4. Verifikasi tipe: .import menghasilkan teks, type affinity
-- kolom INTEGER harus mengonversinya menjadi integer.
.print Tipe data kolom (harus integer):
SELECT DISTINCT typeof(produk_id) AS produk_id,
typeof(jumlah) AS jumlah,
typeof(total_harga) AS total_harga
FROM penjualan;
-- 5. Verifikasi konsistensi: total_harga = jumlah x harga_satuan.
.print Baris dengan total janggal (harus 0):
SELECT COUNT(*) AS baris_janggal
FROM penjualan pe
JOIN produk pr ON pr.id = pe.produk_id
WHERE pe.total_harga <> pe.jumlah * pr.harga_satuan;
-- 6. Verifikasi rentang tanggal dan hari aktif.
.print Rentang tanggal:
SELECT MIN(tanggal) AS tanggal_awal,
MAX(tanggal) AS tanggal_akhir,
COUNT(DISTINCT tanggal) AS hari_aktif
FROM penjualan;
schema.sql
Unduh berkas-- schema.sql — Analisis Penjualan
-- Bagian 1: skema database analisis penjualan + data master produk.
-- Jalankan: sqlite3 penjualan.db < schema.sql
PRAGMA foreign_keys = ON;
-- Data master: delapan produk tetap yang dijual toko.
-- Kolom kategori memakai CHECK IN agar nilai liar tertolak sejak awal.
CREATE TABLE produk (
id INTEGER PRIMARY KEY,
nama TEXT NOT NULL,
kategori TEXT NOT NULL CHECK (kategori IN ('Makanan', 'Minuman', 'Rumah Tangga')),
harga_satuan INTEGER NOT NULL CHECK (harga_satuan > 0)
);
-- Data transaksi: satu baris = satu transaksi penjualan.
-- total_harga disimpan langsung (jumlah x harga saat transaksi),
-- sehingga riwayat tetap benar meski harga_satuan produk berubah.
CREATE TABLE penjualan (
id INTEGER PRIMARY KEY,
tanggal TEXT NOT NULL CHECK (tanggal IS date(tanggal)),
produk_id INTEGER NOT NULL REFERENCES produk(id) ON DELETE RESTRICT,
jumlah INTEGER NOT NULL CHECK (jumlah > 0),
total_harga INTEGER NOT NULL CHECK (total_harga >= 0)
);
-- Index untuk dua pola akses utama analisis:
-- rentang/bulan tanggal dan penggabungan dari produk ke penjualan.
CREATE INDEX idx_penjualan_tanggal ON penjualan(tanggal);
CREATE INDEX idx_penjualan_produk_id ON penjualan(produk_id);
INSERT INTO produk (id, nama, kategori, harga_satuan) VALUES
(1, 'Kopi Gayo 250 g', 'Minuman', 65000),
(2, 'Teh Melati 100 g', 'Minuman', 18000),
(3, 'Gula Aren 500 g', 'Makanan', 25000),
(4, 'Beras Pandan Wangi 5 kg', 'Makanan', 72000),
(5, 'Minyak Goreng 2 L', 'Rumah Tangga', 38000),
(6, 'Sabun Mandi Cair 450 ml', 'Rumah Tangga', 22000),
(7, 'Pasta Gigi 190 g', 'Rumah Tangga', 16000),
(8, 'Sarden Kaleng 155 g', 'Makanan', 12000);