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:

csv
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,130000

Satu 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:

KolomTipe di tabelAturan
idINTEGERterisi, unik — menjadi PRIMARY KEY
tanggalTEXTformat ISO YYYY-MM-DD
produk_idINTEGERharus ada di produk(id)
jumlahINTEGERlebih besar dari 0
total_hargaINTEGERsama 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:

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:

  1. PRAGMA foreign_keys = ON; — penegakan foreign key aktif selama import, sehingga baris dengan produk_id yang tidak dikenal langsung ditolak engine SQLite, bukan lolos sebagai baris yatim.
  2. DELETE FROM penjualan; — mengosongkan tabel sebelum import supaya skrip aman dijalankan ulang kapan pun tanpa menumpuk baris ganda.
  3. .import --csv --skip 1 data_penjualan.csv penjualan — inti bagian ini. Opsi --csv memilih format CSV, --skip 1 melewati baris judul id,tanggal,produk_id,..., lalu berkas sumber dan tabel tujuan mengikuti dalam urutan itu.
  4. Verifikasi jumlah — COUNT(*) memastikan 60 baris benar-benar masuk. Bila .import gagal di tengah jalan, angka inilah yang pertama mencurigakan.
  5. Verifikasi tipe — typeof() memastikan kolom numerik tersimpan sebagai integer. Inilah penegakan type affinity yang dibahas pada Chapter Tipe Data dan Type Affinity.
  6. Verifikasi konsistensi — query JOIN membandingkan total_harga dengan hasil kali jumlah dan harga_satuan; hasil 0 berarti seluruh 60 baris konsisten.
  7. Verifikasi rentang — MIN(), MAX(), dan COUNT(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:

bash
sqlite3 penjualan.db < import.sql

Output yang Dihasilkan:

text
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:

  1. baris_penjualan 60 — seluruh baris data masuk, tidak ada yang tertinggal.
  2. integer|integer|integer — .import menyerahkan seluruh nilai CSV sebagai teks, lalu type affinity kolom INTEGER mengonversinya menjadi angka (jebakan lengkapnya dibahas pada Chapter *Export dan Import Data). Kolom tanggalmemang dibiarkanTEXT` karena format ISO-nya harus dipertahankan apa adanya.
  3. baris_janggal 0 — setiap total_harga sama persis dengan jumlah dikali harga_satuan. Analisis pada Bagian 3 boleh memakai kolom ini tanpa menghitung ulang.
  4. hari_aktif 48 — 60 transaksi tersebar di 48 tanggal berbeda; ada tanggal dengan dua transaksi, misalnya 2025-01-12 dan 2025-01-24.

Pemeriksaan Titik #

Selain verifikasi otomatis, membuka sesi interaktif dan memeriksa beberapa titik data membuat kita mengenal isi tabelnya secara langsung:

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