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 #

#PertanyaanTeknik utama
1Berapa omzet tiap bulan?strftime() + GROUP BY
2Produk apa yang menopang omzet?JOIN + ORDER BY + LIMIT
3Seberapa besar pangsa tiap kategori?CTE + subquery + ROUND()
4Berapa omzet berjalan sampai bulan ini?SUM() OVER
5Produk mana juara di kategorinya?RANK() OVER PARTITION BY
6Bagaimana pertumbuhan bulan-ke-bulan?LAG()
7Seberapa padat hari penjualan?COUNT(DISTINCT) + AVG()

Kode Lengkap Bagian 3 #

Simpan listing berikut sebagai analisis.sql:

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 #

bash
sqlite3 penjualan.db < analisis.sql

Output yang Dihasilkan:

text
=== 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 #

text
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            1170000
  1. strftime('%Y-%m', tanggal) — memotong tanggal ISO menjadi label bulan 2025-01, 2025-02, dan seterusnya; inilah jembatan dari kolom tanggal ke pengelompokan bulanan (Chapter Bekerja dengan Tanggal dan Waktu).
  2. GROUP BY bulan — alias dari daftar SELECT boleh dipakai langsung di sini, dan satu baris keluar untuk setiap bulan.
  3. ORDER BY bulan — label ISO urut secara alfabetis sekaligus kronologis; tanpa ORDER BY, urutan bulan tidak dijamin.

Query 3 — Pangsa Kategori dengan CTE #

text
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  
  1. WITH omzet_kategori AS (...) — CTE menampung pengelompokan omzet per kategori lebih dulu, sehingga query utama tinggal memakai hasilnya (Chapter Common Table Expressions).
  2. (SELECT SUM(omzet) FROM omzet_kategori) — subquery skalar yang menghitung total 6074000 sekali sebagai penyebut pembagian.
  3. omzet * 100.0 / ... — angka 100.0 memaksa pembagian pecahan; tanpa .0, SQLite membagi bilangan bulat dan memotong hasilnya menjadi 0 atau 1.
  4. 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 #

text
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        
  1. 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).
  2. Bedanya dengan GROUP BY — agregat biasa melipat banyak baris menjadi satu, sedangkan window function menambah kolom tanpa menghilangkan satu baris pun; kolom omzet dan omzet_kumulatif hidup berdampingan.
  3. 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() #

text
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        
  1. PARTITION BY kategori — window dibagi tiga ruang terpisah, satu per kategori, sehingga peringkat dihitung di dalam kategorinya masing-masing.
  2. ORDER BY omzet DESC di dalam OVER — menentukan urutan pemberian peringkat: omzet terbesar di tiap partisi mendapat peringkat 1 — Beras Pandan Wangi 5 kg untuk Makanan, Kopi Gayo 250 g untuk Minuman, Minyak Goreng 2 L untuk Rumah Tangga.
  3. ORDER BY kategori, peringkat di 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.

  1. LAG(omzet) — mengambil nilai omzet dari satu baris sebelumnya dalam urutan bulan; di baris Februari, kolom omzet_bulan_lalu berisi 859000.
  2. ROUND((omzet - ...) * 100.0 / ..., 1) — omzet bulan berjalan dikurangi omzet bulan lalu, dibagi omzet bulan lalu, dikali 100.0, lalu dibulatkan ke satu digit.
  3. Baris Januari kosong — LAG() tidak punya baris sebelumnya untuk bulan pertama, sehingga hasilnya NULL dan kolom pertumbuhannya kosong. Nilai NULL di 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
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;

import.sql

Unduh berkas
sql
-- 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
sql
-- 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);