Aplikasi kasir toko mengekspor riwayat transaksinya sebagai berkas CSV setiap akhir bulan. Chapter Export dan Import Data sudah membahas mekanismenya; bagian ini menerapkannya pada data nyata: 60 baris transaksi semester I 2025. Fokus kita bukan sekadar memasukkan data, melainkan memverifikasi hasilnya — jumlah baris, tipe kolom, dan konsistensi angka — karena import yang tidak diverifikasi adalah import yang tidak bisa dipercaya ketika laporan akhir dipertanyakan.
Anatomi Berkas CSV #
Simpan data latihan berikut sebagai data_penjualan.csv pada folder yang sama dengan schema.sql. Sepuluh baris pertama (seluruh bulan Januari) terlihat seperti ini:
id,tanggal,produk_id,jumlah,total_harga
1,2025-01-03,1,2,130000
2,2025-01-05,2,5,90000
3,2025-01-07,5,3,114000
4,2025-01-12,4,1,72000
5,2025-01-12,1,1,65000
6,2025-01-17,6,4,88000
7,2025-01-21,3,2,50000
8,2025-01-24,8,6,72000
9,2025-01-24,7,3,48000
10,2025-01-28,1,2,130000Satu baris merekam satu transaksi. Berkas lengkapnya berisi 60 baris (id 1 sampai 60) yang membentang dari 2025-01-03 sampai 2025-06-28, tersedia pada bagian Berkas Sumber di bawah halaman ini. Kontrak setiap kolomnya:
| Kolom | Tipe di tabel | Aturan |
|---|---|---|
id | INTEGER | terisi, unik — menjadi PRIMARY KEY |
tanggal | TEXT | format ISO YYYY-MM-DD |
produk_id | INTEGER | harus ada di produk(id) |
jumlah | INTEGER | lebih besar dari 0 |
total_harga | INTEGER | sama dengan jumlah dikali harga_satuan produk |
Data latihan ini sengaja bersih. Pada data nyata, baris janggal hampir pasti muncul, dan tiga verifikasi pada import.sql di bawah adalah alarm pertamanya.
Kode Lengkap Bagian 2 #
Simpan listing berikut sebagai import.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;Mari kita bedah langkah-langkahnya:
PRAGMA foreign_keys = ON;— penegakan foreign key aktif selama import, sehingga baris denganproduk_idyang tidak dikenal langsung ditolak engine SQLite, bukan lolos sebagai baris yatim.DELETE FROM penjualan;— mengosongkan tabel sebelum import supaya skrip aman dijalankan ulang kapan pun tanpa menumpuk baris ganda..import --csv --skip 1 data_penjualan.csv penjualan— inti bagian ini. Opsi--csvmemilih format CSV,--skip 1melewati baris judulid,tanggal,produk_id,..., lalu berkas sumber dan tabel tujuan mengikuti dalam urutan itu.- Verifikasi jumlah —
COUNT(*)memastikan 60 baris benar-benar masuk. Bila.importgagal di tengah jalan, angka inilah yang pertama mencurigakan. - Verifikasi tipe —
typeof()memastikan kolom numerik tersimpan sebagaiinteger. Inilah penegakan type affinity yang dibahas pada Chapter Tipe Data dan Type Affinity. - Verifikasi konsistensi — query
JOINmembandingkantotal_hargadengan hasil kalijumlahdanharga_satuan; hasil 0 berarti seluruh 60 baris konsisten. - Verifikasi rentang —
MIN(),MAX(), danCOUNT(DISTINCT)menunjukkan periode data dan jumlah hari yang benar-benar memiliki transaksi.
Menjalankan Import #
Pastikan penjualan.db, schema.sql, data_penjualan.csv, dan import.sql berada dalam satu folder, lalu jalankan:
sqlite3 penjualan.db < import.sqlOutput yang Dihasilkan:
Jumlah baris:
baris_penjualan
---------------
60
Tipe data kolom (harus integer):
produk_id jumlah total_harga
--------- ------- -----------
integer integer integer
Baris dengan total janggal (harus 0):
baris_janggal
-------------
0
Rentang tanggal:
tanggal_awal tanggal_akhir hari_aktif
------------ ------------- ----------
2025-01-03 2025-06-28 48 Empat hasil tersebut adalah kartu lolosnya data kita. Mari kita baca maknanya:
baris_penjualan60 — seluruh baris data masuk, tidak ada yang tertinggal.integer|integer|integer—.importmenyerahkan seluruh nilai CSV sebagai teks, lalu type affinity kolomINTEGERmengonversinya menjadi angka (jebakan lengkapnya dibahas pada Chapter *Export dan Import Data). Kolomtanggalmemang dibiarkanTEXT` karena format ISO-nya harus dipertahankan apa adanya.baris_janggal0 — setiaptotal_hargasama persis denganjumlahdikaliharga_satuan. Analisis pada Bagian 3 boleh memakai kolom ini tanpa menghitung ulang.hari_aktif48 — 60 transaksi tersebar di 48 tanggal berbeda; ada tanggal dengan dua transaksi, misalnya2025-01-12dan2025-01-24.
Pemeriksaan Titik #
Selain verifikasi otomatis, membuka sesi interaktif dan memeriksa beberapa titik data membuat kita mengenal isi tabelnya secara langsung:
sqlite> .headers on
sqlite> .mode column
sqlite> SELECT * FROM penjualan ORDER BY id LIMIT 3;
id tanggal produk_id jumlah total_harga
-- ---------- --------- ------ -----------
1 2025-01-03 1 2 130000
2 2025-01-05 2 5 90000
3 2025-01-07 5 3 114000
sqlite> SELECT * FROM penjualan ORDER BY id DESC LIMIT 3;
id tanggal produk_id jumlah total_harga
-- ---------- --------- ------ -----------
60 2025-06-28 1 2 130000
59 2025-06-24 7 4 64000
58 2025-06-24 8 6 72000
sqlite> SELECT DISTINCT typeof(produk_id) AS produk_id, typeof(jumlah) AS jumlah,
...> typeof(total_harga) AS total_harga
...> FROM penjualan;
produk_id jumlah total_harga
--------- ------- -----------
integer integer integer
sqlite> SELECT COUNT(*) AS jumlah_produk FROM produk;
jumlah_produk
-------------
8 Tiga baris pertama dan tiga baris terakhir menampilkan data Januari dan Juni secara utuh — kolomnya terisi, format tanggalnya konsisten, dan urutan id mengikuti urutan waktu. Pemeriksaan typeof() berulang menghasilkan integer untuk seluruh kolom numerik, dan tabel produk masih berisi delapan baris data master.
Database kini berisi data yang masuk lewat pintu yang benar dan lolos empat verifikasi. Tabel penjualan siap dihitung — Bagian 3 menyusun query analisisnya. Berkas lengkap bagian ini (schema.sql, data_penjualan.csv, dan import.sql) tersedia pada bagian Berkas Sumber di bawah halaman.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
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);