Hasil analisis Bagian 3 tersebar pada tujuh output, sedangkan pemilik toko butuh satu ritual sederhana: satu perintah yang mencetak laporan lengkap sekaligus menghasilkan berkas untuk spreadsheet dan program lain. Pada bagian ini kita merakit laporan.sql — laporan akhir yang dibangun di atas temp view, dicetak rapi dengan .print dan .mode column, lalu ditutup eksport ke CSV dan JSON. Seluruh perintah eksportnya adalah materi Chapter Export dan Import Data; tampilan sementaranya dari Chapter View dan Dot Commands pada sqlite3 CLI.

Tampilan Sementara #

Dua query terberat analisis kita — omzet per bulan dan omzet per kategori — dipindahkan ke CREATE TEMP VIEW di kepala laporan.sql. Berbeda dari CREATE VIEW biasa yang menetap di database, temp view hanya hidup selama sesi CLI berlangsung; berkas penjualan.db tidak berubah sama sekali. Setelah laporan.sql dijalankan, buktinya terlihat dari daftar tabel:

text
sqlite> .tables
penjualan  produk   
sqlite>

Hanya penjualan dan produk yang ada — kedua view (v_omzet_bulanan dan v_omzet_kategori) sudah lenyap bersama sesi yang membuatnya. Pola ini cocok untuk laporan berulang: query panjang ditulis sekali di kepala skrip, sementara database tetap bersih.

Kode Lengkap Bagian 4 #

Simpan listing berikut sebagai laporan.sql:

sql
-- laporan.sql — Analisis Penjualan
-- Bagian 4: laporan akhir siap baca + eksport ke CSV dan JSON.
-- Jalankan: sqlite3 penjualan.db < laporan.sql
-- Prasyarat: schema.sql dan import.sql sudah dijalankan.

PRAGMA foreign_keys = ON;

-- Dua tampilan sementara (TEMP VIEW): hidup hanya selama sesi
-- CLI ini, jadi isi database tidak berubah sama sekali.
CREATE TEMP VIEW v_omzet_bulanan AS
SELECT strftime('%Y-%m', tanggal) AS bulan,
       COUNT(*)          AS transaksi,
       SUM(total_harga)  AS omzet
FROM penjualan
GROUP BY bulan;

CREATE TEMP VIEW v_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;

.headers on
.mode column

.print ===== LAPORAN PENJUALAN - SEMESTER I 2025 =====
.print
.print --- Ringkasan ---
SELECT COUNT(*)                AS transaksi,
       COUNT(DISTINCT tanggal) AS hari_aktif,
       SUM(jumlah)             AS item_terjual,
       SUM(total_harga)        AS total_omzet
FROM penjualan;

.print
.print --- Omzet Bulanan dan Pertumbuhan ---
SELECT bulan,
       transaksi,
       omzet,
       ROUND((omzet - LAG(omzet) OVER (ORDER BY bulan)) * 100.0
             / LAG(omzet) OVER (ORDER BY bulan), 1) AS pertumbuhan_persen
FROM v_omzet_bulanan
ORDER BY bulan;

.print
.print --- Lima Produk Teratas ---
SELECT pr.nama AS produk,
       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
.print --- Pangsa Kategori ---
SELECT kategori,
       omzet,
       ROUND(omzet * 100.0 / (SELECT SUM(omzet) FROM v_omzet_kategori), 1) AS persen
FROM v_omzet_kategori
ORDER BY omzet DESC;

-- Eksport CSV: tabel omzet bulanan untuk dibuka di spreadsheet.
.mode csv
.once laporan-omzet.csv
SELECT bulan,
       transaksi,
       omzet,
       ROUND((omzet - LAG(omzet) OVER (ORDER BY bulan)) * 100.0
             / LAG(omzet) OVER (ORDER BY bulan), 1) AS pertumbuhan_persen
FROM v_omzet_bulanan
ORDER BY bulan;

-- Eksport JSON: omzet per produk untuk konsumen program lain.
-- Semua kolom sengaja integer: nilai REAL akan tercetak dengan
-- representasi lengkapnya di JSON (lihat peringatan pada halaman ini).
.mode json
.once laporan-produk.json
SELECT pr.nama  AS produk,
       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;

Mari kita bedah susunannya:

  1. Dua CREATE TEMP VIEW — v_omzet_bulanan mengelompokkan omzet per bulan, v_omzet_kategori menggabungkan penjualan ke produk per kategori. Keduanya menjadi sumber query laporan di bawahnya.
  2. .print — menyisipkan judul bagian di antara hasil query; teks sesudah .print dicetak apa adanya ke layar.
  3. Bagian laporan — ringkasan satu baris, omzet bulanan dengan pertumbuhan LAG(), lima produk teratas, dan pangsa kategori. Seluruh window function dan CTE sudah dibedah pada Bagian 3; di sini tinggal memakainya.
  4. .mode csv + .once laporan-omzet.csv — hasil query berikutnya ditulis ke berkas CSV dengan baris judul kolom; setelah satu query, output kembali ke layar.
  5. .mode json + .once laporan-produk.json — query penutup diekspor sebagai array JSON untuk konsumen program lain.

Menjalankan Laporan #

bash
sqlite3 penjualan.db < laporan.sql

Output yang Dihasilkan:

text
===== LAPORAN PENJUALAN - SEMESTER I 2025 =====

--- Ringkasan ---
transaksi  hari_aktif  item_terjual  total_omzet
---------  ----------  ------------  -----------
60         48          193           6074000    

--- Omzet Bulanan dan Pertumbuhan ---
bulan    transaksi  omzet    pertumbuhan_persen
-------  ---------  -------  ------------------
2025-01  10         859000                     
2025-02  10         958000   11.5              
2025-03  10         1006000  5.0               
2025-04  10         1062000  5.6               
2025-05  10         1019000  -4.0              
2025-06  10         1170000  14.8              

--- Lima Produk Teratas ---
produk                   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 

--- Pangsa Kategori ---
kategori      omzet    persen
------------  -------  ------
Minuman       3096000  51.0  
Makanan       1626000  26.8  
Rumah Tangga  1352000  22.3  

Empat bagian laporan tampil berurutan dalam satu eksekusi: ringkasan semester, tabel pertumbuhan bulanan, produk penopang omzet, dan pangsa kategori — semua angka konsisten dengan analisis Bagian 3 karena sumber datanya sama.

Berkas Hasil Eksport #

Dua perintah .once di ekor skrip meninggalkan dua berkas di folder kerja. Isi laporan-omzet.csv:

bash
cat laporan-omzet.csv

Output yang Dihasilkan:

text
bulan,transaksi,omzet,pertumbuhan_persen
2025-01,10,859000,
2025-02,10,958000,11.5
2025-03,10,1006000,5.0
2025-04,10,1062000,5.6
2025-05,10,1019000,-4.0
2025-06,10,1170000,14.8

Baris pertama berisi nama kolom sehingga spreadsheet langsung mengenalinya, dan sel pertumbuhan Januari kosong karena NULL diekspor sebagai field kosong. Isi laporan-produk.json:

bash
cat laporan-produk.json

Output yang Dihasilkan:

json
[{"produk":"Kopi Gayo 250 g","kategori":"Minuman","item_terjual":36,"omzet":2340000},
{"produk":"Beras Pandan Wangi 5 kg","kategori":"Makanan","item_terjual":11,"omzet":792000},
{"produk":"Teh Melati 100 g","kategori":"Minuman","item_terjual":42,"omzet":756000},
{"produk":"Minyak Goreng 2 L","kategori":"Rumah Tangga","item_terjual":17,"omzet":646000},
{"produk":"Gula Aren 500 g","kategori":"Makanan","item_terjual":18,"omzet":450000},
{"produk":"Sabun Mandi Cair 450 ml","kategori":"Rumah Tangga","item_terjual":19,"omzet":418000},
{"produk":"Sarden Kaleng 155 g","kategori":"Makanan","item_terjual":32,"omzet":384000},
{"produk":"Pasta Gigi 190 g","kategori":"Rumah Tangga","item_terjual":18,"omzet":288000}]

Array JSON dengan satu objek per produk — delapan objek terurut dari omzet terbesar — siap dikonsumsi fetch() di browser atau json.loads() di Python.

Bentuk aslinya mudah dibuktikan sendiri:

text
sqlite> .mode json
sqlite> SELECT 26.8 AS persen;
[{"persen":26.80000000000000071}]
sqlite>

Uji End-to-End dari Nol #

Proyek ditutup dengan membongkar dan membangun ulang semuanya — cara terbaik memastikan tidak ada langkah yang diam-diam bergantung pada sisa percobaan:

bash
rm -f penjualan.db
sqlite3 penjualan.db < schema.sql
sqlite3 penjualan.db < import.sql
sqlite3 penjualan.db < laporan.sql

Urutan perintah tersebut mengulang seluruh proyek: skema dibangun, 60 baris terimpor dan lolos empat verifikasi, lalu laporan tercetak persis seperti di atas bersama dua berkas hasil. Satu pemeriksaan terakhir merangkum keadaan akhir database:

bash
sqlite3 penjualan.db "SELECT COUNT(*), SUM(total_harga) FROM penjualan;"

Output yang Dihasilkan:

text
60|6074000

Enam puluh baris dengan total 6074000 — cocok dengan laporan dan analisis. Alur ini juga menjadi ritual bulanan pemilik toko: ganti data_penjualan.csv dengan ekspor bulan berikutnya, jalankan ulang tiga perintah terakhir, dan laporan baru siap. import.sql mengosongkan tabelnya lebih dulu, sehingga data lama tidak pernah menumpuk.

Proyek analisis penjualan selesai: skema dengan penegakan yang terbukti, import CSV yang terverifikasi, tujuh query analisis dari omzet bulanan sampai statistik harian, dan laporan akhir yang mengekspor hasilnya ke dunia luar. Berkas lengkap bagian ini (schema.sql, data_penjualan.csv, import.sql, analisis.sql, dan laporan.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;

laporan.sql

Unduh berkas
sql
-- laporan.sql — Analisis Penjualan
-- Bagian 4: laporan akhir siap baca + eksport ke CSV dan JSON.
-- Jalankan: sqlite3 penjualan.db < laporan.sql
-- Prasyarat: schema.sql dan import.sql sudah dijalankan.

PRAGMA foreign_keys = ON;

-- Dua tampilan sementara (TEMP VIEW): hidup hanya selama sesi
-- CLI ini, jadi isi database tidak berubah sama sekali.
CREATE TEMP VIEW v_omzet_bulanan AS
SELECT strftime('%Y-%m', tanggal) AS bulan,
       COUNT(*)          AS transaksi,
       SUM(total_harga)  AS omzet
FROM penjualan
GROUP BY bulan;

CREATE TEMP VIEW v_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;

.headers on
.mode column

.print ===== LAPORAN PENJUALAN - SEMESTER I 2025 =====
.print
.print --- Ringkasan ---
SELECT COUNT(*)                AS transaksi,
       COUNT(DISTINCT tanggal) AS hari_aktif,
       SUM(jumlah)             AS item_terjual,
       SUM(total_harga)        AS total_omzet
FROM penjualan;

.print
.print --- Omzet Bulanan dan Pertumbuhan ---
SELECT bulan,
       transaksi,
       omzet,
       ROUND((omzet - LAG(omzet) OVER (ORDER BY bulan)) * 100.0
             / LAG(omzet) OVER (ORDER BY bulan), 1) AS pertumbuhan_persen
FROM v_omzet_bulanan
ORDER BY bulan;

.print
.print --- Lima Produk Teratas ---
SELECT pr.nama AS produk,
       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
.print --- Pangsa Kategori ---
SELECT kategori,
       omzet,
       ROUND(omzet * 100.0 / (SELECT SUM(omzet) FROM v_omzet_kategori), 1) AS persen
FROM v_omzet_kategori
ORDER BY omzet DESC;

-- Eksport CSV: tabel omzet bulanan untuk dibuka di spreadsheet.
.mode csv
.once laporan-omzet.csv
SELECT bulan,
       transaksi,
       omzet,
       ROUND((omzet - LAG(omzet) OVER (ORDER BY bulan)) * 100.0
             / LAG(omzet) OVER (ORDER BY bulan), 1) AS pertumbuhan_persen
FROM v_omzet_bulanan
ORDER BY bulan;

-- Eksport JSON: omzet per produk untuk konsumen program lain.
-- Semua kolom sengaja integer: nilai REAL akan tercetak dengan
-- representasi lengkapnya di JSON (lihat peringatan pada halaman ini).
.mode json
.once laporan-produk.json
SELECT pr.nama  AS produk,
       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;

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);