Semua laporan berjalan, tetapi dua hal mengganjal: kecepatan dan kenyamanan. Setiap JOIN dari tabel induk ke peminjaman memindai seluruh tabel anak, karena SQLite tidak membuat index otomatis untuk kolom foreign key — berbeda dari sebagian database server. Dan pustakawan tidak selayaknya mengetik JOIN tiga tabel setiap kali ingin tahu buku mana yang belum kembali. Bagian ini menuntaskan keduanya lewat optimasi.sql: index untuk kecepatan, view untuk kenyamanan. Materi intinya berasal dari Chapter Index dan Pencarian Cepat, Strategi Index, View, Menganalisis Query dengan EXPLAIN QUERY PLAN, dan ANALYZE dan Statistik Query Planner.
Index untuk Pola Akses Kita #
Index dipilih dari pola akses nyata laporan Bagian 3, bukan dari daftar “semua kolom yang kelihatan penting” — prinsip Chapter Strategi Index:
| Index | Kolom | Alasan |
|---|---|---|
idx_buku_kategori_id | buku.kategori_id | JOIN kategori ke buku pada Laporan 1 dan 4 |
idx_buku_rak_kode | buku.rak_kode | pencarian buku menurut rak |
idx_peminjaman_buku_id | peminjaman.buku_id | riwayat peminjaman satu buku |
idx_peminjaman_anggota_id | peminjaman.anggota_id | riwayat peminjaman satu anggota |
idx_buku_judul | buku.judul | pencarian judul, kolom yang paling sering diketik pengguna |
idx_peminjaman_tanggal | peminjaman.tanggal_pinjam | rentang tanggal seperti “peminjaman bulan ini” |
idx_peminjaman_aktif | peminjaman(buku_id) WHERE tanggal_kembali IS NULL | index parsial khusus baris aktif |
Enam index pertama sederhana: satu kolom, satu B-tree. Yang terakhir adalah index parsial — klausa WHERE pada definisi index membatasi isi index hanya pada baris yang memenuhi syarat. Laporan harian pustakawan hampir selalu menyentuh peminjaman yang masih berjalan, jadi mengindeks seluruh riwayat hanya menambah beban penulisan tanpa manfaat (Chapter Strategi Index membahas pola ini untuk data yang “panas”).
View untuk Laporan Berulang #
Dua view membungkus query yang paling sering dipakai. v_peminjaman_aktif menyembunyikan JOIN tiga tabel dari Laporan 2 dan sekaligus menghitung jatuh tempo. v_buku_tersedia menghitung ketersediaan per judul dengan subquery berkorelasi — bandingkan dengan versi LEFT JOIN pada Laporan 5: hasil sama, bentuk berbeda, dan keduanya adalah materi Chapter Subquery dan View.
Kode Lengkap Bagian 4 #
-- optimasi.sql — Katalog Perpustakaan
-- Bagian 4: index untuk query yang sering dipakai, view untuk
-- laporan berulang, dan ANALYZE untuk statistik query planner.
-- Jalankan: sqlite3 perpustakaan.db < optimasi.sql
PRAGMA foreign_keys = ON;
-- Index pada kolom foreign key: mempercepat JOIN dari tabel induk
-- ke tabel anak, misalnya mencari semua peminjaman satu buku/anggota.
CREATE INDEX idx_buku_kategori_id ON buku(kategori_id);
CREATE INDEX idx_buku_rak_kode ON buku(rak_kode);
CREATE INDEX idx_peminjaman_buku_id ON peminjaman(buku_id);
CREATE INDEX idx_peminjaman_anggota_id ON peminjaman(anggota_id);
-- Index pada kolom yang sering menjadi kriteria pencarian.
CREATE INDEX idx_buku_judul ON buku(judul);
CREATE INDEX idx_peminjaman_tanggal ON peminjaman(tanggal_pinjam);
-- Index parsial: hanya mengindeks baris peminjaman yang masih
-- aktif (tanggal_kembali NULL), yaitu baris yang paling sering
-- dicari pustakawan setiap hari.
CREATE INDEX idx_peminjaman_aktif ON peminjaman(buku_id)
WHERE tanggal_kembali IS NULL;
-- View: agregat ketersediaan buku dalam satu SELECT sederhana.
CREATE VIEW v_buku_tersedia AS
SELECT b.id,
b.judul,
k.nama AS kategori,
r.lokasi AS lokasi_rak,
b.jumlah_tersedia,
(SELECT COUNT(*)
FROM peminjaman p
WHERE p.buku_id = b.id
AND p.tanggal_kembali IS NULL) AS sedang_dipinjam
FROM buku b
JOIN kategori k ON k.id = b.kategori_id
JOIN rak r ON r.kode = b.rak_kode;
-- View: peminjaman yang masih berjalan beserta jatuh temponya.
CREATE VIEW v_peminjaman_aktif AS
SELECT p.id,
b.judul,
a.nama AS peminjam,
p.tanggal_pinjam,
date(p.tanggal_pinjam, '+14 days') AS jatuh_tempo
FROM peminjaman p
JOIN buku b ON b.id = p.buku_id
JOIN anggota a ON a.id = p.anggota_id
WHERE p.tanggal_kembali IS NULL;
-- Kumpulkan statistik tabel dan index untuk query planner.
ANALYZE;- Empat index pertama — kolom foreign key dari
bukudanpeminjaman. SetiapJOINdan setiap pencarian “anak dari induk X” kini menyusuri B-tree, bukan memindai tabel. idx_buku_juduldanidx_peminjaman_tanggal— dua kolom yang paling sering menjadi kriteria pencarian pengguna akhir.idx_peminjaman_aktif ... WHERE tanggal_kembali IS NULL— index parsial; isinya hanya baris peminjaman aktif, sehingga jauh lebih ramping daripada index penuh.v_buku_tersedia— subquery berkorelasi(SELECT COUNT(*) FROM peminjaman p WHERE ...)menghitung peminjaman aktif untuk setiap buku luar.v_peminjaman_aktif—JOINtiga tabel plus kolomjatuh_tempoterenkapsulasi; pemakai cukupSELECTdari view.ANALYZE— menutup berkas dengan pemindaian statistik untuk query planner (dibahas setelah ini).
Sebelum dan Sesudah Index #
Efek index dibuktikan, bukan diasumsikan. Jalankan tiga EXPLAIN QUERY PLAN pada database Bagian 3 — sebelum optimasi.sql:
sqlite> EXPLAIN QUERY PLAN SELECT * FROM peminjaman WHERE buku_id = 5;
QUERY PLAN
`--SCAN peminjaman
sqlite> EXPLAIN QUERY PLAN SELECT * FROM buku WHERE judul = 'Kosmos';
QUERY PLAN
`--SCAN buku
sqlite> EXPLAIN QUERY PLAN SELECT COUNT(*) FROM peminjaman WHERE tanggal_kembali IS NULL AND buku_id = 5;
QUERY PLAN
`--SCAN peminjaman
sqlite>Kata SCAN pada ketiganya berarti seluruh tabel dipindai baris demi baris (Chapter Menganalisis Query dengan EXPLAIN QUERY PLAN). Pada 6 baris peminjaman ini tidak terasa; pada ribuan baris, SCAN adalah sumber lambat yang klasik. Sekarang terapkan optimasinya:
sqlite3 perpustakaan.db < optimasi.sqlUlangi ketiga query yang sama:
sqlite> EXPLAIN QUERY PLAN SELECT * FROM peminjaman WHERE buku_id = 5;
QUERY PLAN
`--SEARCH peminjaman USING INDEX idx_peminjaman_buku_id (buku_id=?)
sqlite> EXPLAIN QUERY PLAN SELECT * FROM buku WHERE judul = 'Kosmos';
QUERY PLAN
`--SEARCH buku USING INDEX idx_buku_judul (judul=?)
sqlite> EXPLAIN QUERY PLAN SELECT COUNT(*) FROM peminjaman WHERE tanggal_kembali IS NULL AND buku_id = 5;
QUERY PLAN
`--SEARCH peminjaman USING INDEX idx_peminjaman_aktif (buku_id=?)
sqlite>SCAN berubah menjadi SEARCH ... USING INDEX: query planner menyusuri B-tree dan melompat langsung ke baris yang relevan. Query ketiga mendapat hadiah khusus — query planner memilih idx_peminjaman_aktif, index parsial, karena klausa WHERE tanggal_kembali IS NULL pada query persis cocok dengan syarat index-nya.
Mencoba Kedua View #
Hasil optimasi paling terasa pada pemakaian sehari-hari. Dua laporan yang tadinya butuh JOIN panjang kini berbentuk SELECT sederhana:
sqlite> .headers on
sqlite> .mode column
sqlite> SELECT judul, peminjam, tanggal_pinjam, jatuh_tempo FROM v_peminjaman_aktif;
judul peminjam tanggal_pinjam jatuh_tempo
------ ------------ -------------- -----------
Kosmos Budi Santoso 2025-05-26 2025-06-09
sqlite> SELECT judul, kategori, jumlah_tersedia, sedang_dipinjam FROM v_buku_tersedia
...> WHERE sedang_dipinjam > 0 OR jumlah_tersedia = 0;
judul kategori jumlah_tersedia sedang_dipinjam
------ -------- --------------- ---------------
Kosmos Sains 2 1 Query pertama menampilkan peminjaman aktif beserta jatuh temponya. Query kedua mencari buku yang butuh perhatian pustakawan — sedang_dipinjam > 0 (ada eksemplar keluar) atau jumlah_tersedia = 0 (rak kosong) — dan menemukan Kosmos dengan satu eksemplar beredar.
ANALYZE dan Statistik Query Planner #
ANALYZE pada akhir optimasi.sql memindai seluruh index dan menyimpan ringkasannya ke tabel sqlite_stat1 (Chapter *ANALYZE dan Statistik Query Planner`). Isinya terbaca langsung:
sqlite> SELECT * FROM sqlite_stat1 ORDER BY idx;
tbl idx stat
---------- --------------------------- ----
buku idx_buku_judul 15 1
buku idx_buku_kategori_id 15 3
buku idx_buku_rak_kode 15 4
peminjaman idx_peminjaman_aktif 1 1
peminjaman idx_peminjaman_anggota_id 6 2
peminjaman idx_peminjaman_buku_id 6 1
peminjaman idx_peminjaman_tanggal 6 1
anggota sqlite_autoindex_anggota_1 5 1
buku sqlite_autoindex_buku_1 15 1
kategori sqlite_autoindex_kategori_1 6 1
rak sqlite_autoindex_rak_1 4 1
sqlite>Mari kita bedah beberapa barisnya:
buku|idx_buku_kategori_id|15 3— index berisi 15 entri, dengan perkiraan rata-rata 3 baris per nilaikategori_id(lima belas buku tersebar di enam kategori).peminjaman|idx_peminjaman_aktif|1 1— index parsial hanya berisi 1 entri, karena hanya satu peminjaman yang aktif. Statistik inilah yang membuat query planner menilai index ini sangat murah untuk query baris aktif.sqlite_autoindex_*— index otomatis dari constraintUNIQUE(isbn,email,namakategori,koderak); ikut terdata tanpa kita buat manual.
Verifikasi Akhir #
Proyek ditutup dengan pemeriksaan yang sama seperti Bagian 1, kini pada database lengkap berisi data, index, dan view:
sqlite3 perpustakaan.db "PRAGMA foreign_key_check;"
sqlite3 perpustakaan.db "PRAGMA integrity_check;"Output yang Dihasilkan:
okforeign_key_check kembali tanpa keluaran — tidak ada baris yatim — dan integrity_check menjawab ok. Katalog perpustakaan kita selesai: lima tabel yang dinormalisasi, data awal yang realistis, enam laporan siap pakai, tujuh index yang terbukti mengubah SCAN menjadi SEARCH, dua view yang menyederhanakan hari kerja pustakawan, dan integritas yang terjaga dari schema.sql sampai baris terakhir. Berkas lengkap bagian ini (schema.sql, seed.sql, laporan.sql, dan optimasi.sql) tersedia pada bagian Berkas Sumber di bawah halaman.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
laporan.sql
Unduh berkas-- laporan.sql — Katalog Perpustakaan
-- Bagian 3: kumpulan query laporan untuk pekerjaan harian pustakawan.
-- Jalankan: sqlite3 perpustakaan.db < laporan.sql
.headers on
.mode column
-- Laporan 1: daftar buku per kategori.
.print Laporan 1: Daftar Buku per Kategori
SELECT k.nama AS kategori, b.judul, b.penulis, b.tahun
FROM buku b
JOIN kategori k ON k.id = b.kategori_id
ORDER BY k.nama, b.judul;
.print
-- Laporan 2: buku yang sedang dipinjam beserta peminjamnya.
.print Laporan 2: Buku Sedang Dipinjam
SELECT b.judul, a.nama AS peminjam, p.tanggal_pinjam
FROM peminjaman p
JOIN buku b ON b.id = p.buku_id
JOIN anggota a ON a.id = p.anggota_id
WHERE p.tanggal_kembali IS NULL
ORDER BY p.tanggal_pinjam;
.print
-- Laporan 3: riwayat peminjaman setiap anggota.
.print Laporan 3: Riwayat Peminjaman per Anggota
SELECT a.nama AS anggota, b.judul, p.tanggal_pinjam,
COALESCE(p.tanggal_kembali, 'belum kembali') AS tanggal_kembali
FROM peminjaman p
JOIN anggota a ON a.id = p.anggota_id
JOIN buku b ON b.id = p.buku_id
ORDER BY a.nama, p.tanggal_pinjam;
.print
-- Laporan 4: statistik koleksi per kategori.
.print Laporan 4: Statistik Kategori
SELECT k.nama AS kategori,
COUNT(b.id) AS jumlah_buku,
MIN(b.tahun) AS tahun_tertua,
MAX(b.tahun) AS tahun_terbaru
FROM kategori k
LEFT JOIN buku b ON b.kategori_id = k.id
GROUP BY k.nama
ORDER BY jumlah_buku DESC, k.nama;
.print
-- Laporan 5: eksemplar tersedia dibandingkan total eksemplar.
.print Laporan 5: Eksemplar Tersedia vs Total
SELECT b.judul,
b.jumlah_tersedia AS tersedia,
COUNT(p.id) AS sedang_dipinjam,
b.jumlah_tersedia + COUNT(p.id) AS total
FROM buku b
LEFT JOIN peminjaman p
ON p.buku_id = b.id
AND p.tanggal_kembali IS NULL
GROUP BY b.id
ORDER BY b.judul;
.print
-- Laporan 6: peminjaman yang melewati jatuh tempo 14 hari
-- dihitung terhadap tanggal referensi 2025-06-20.
.print Laporan 6: Peminjaman Terlambat
SELECT b.judul, a.nama AS peminjam, p.tanggal_pinjam,
date(p.tanggal_pinjam, '+14 days') AS jatuh_tempo,
CAST(julianday('2025-06-20') - julianday(p.tanggal_pinjam, '+14 days')
AS INTEGER) AS hari_terlambat
FROM peminjaman p
JOIN buku b ON b.id = p.buku_id
JOIN anggota a ON a.id = p.anggota_id
WHERE p.tanggal_kembali IS NULL
AND julianday('2025-06-20') - julianday(p.tanggal_pinjam) > 14;
optimasi.sql
Unduh berkas-- optimasi.sql — Katalog Perpustakaan
-- Bagian 4: index untuk query yang sering dipakai, view untuk
-- laporan berulang, dan ANALYZE untuk statistik query planner.
-- Jalankan: sqlite3 perpustakaan.db < optimasi.sql
PRAGMA foreign_keys = ON;
-- Index pada kolom foreign key: mempercepat JOIN dari tabel induk
-- ke tabel anak, misalnya mencari semua peminjaman satu buku/anggota.
CREATE INDEX idx_buku_kategori_id ON buku(kategori_id);
CREATE INDEX idx_buku_rak_kode ON buku(rak_kode);
CREATE INDEX idx_peminjaman_buku_id ON peminjaman(buku_id);
CREATE INDEX idx_peminjaman_anggota_id ON peminjaman(anggota_id);
-- Index pada kolom yang sering menjadi kriteria pencarian.
CREATE INDEX idx_buku_judul ON buku(judul);
CREATE INDEX idx_peminjaman_tanggal ON peminjaman(tanggal_pinjam);
-- Index parsial: hanya mengindeks baris peminjaman yang masih
-- aktif (tanggal_kembali NULL), yaitu baris yang paling sering
-- dicari pustakawan setiap hari.
CREATE INDEX idx_peminjaman_aktif ON peminjaman(buku_id)
WHERE tanggal_kembali IS NULL;
-- View: agregat ketersediaan buku dalam satu SELECT sederhana.
CREATE VIEW v_buku_tersedia AS
SELECT b.id,
b.judul,
k.nama AS kategori,
r.lokasi AS lokasi_rak,
b.jumlah_tersedia,
(SELECT COUNT(*)
FROM peminjaman p
WHERE p.buku_id = b.id
AND p.tanggal_kembali IS NULL) AS sedang_dipinjam
FROM buku b
JOIN kategori k ON k.id = b.kategori_id
JOIN rak r ON r.kode = b.rak_kode;
-- View: peminjaman yang masih berjalan beserta jatuh temponya.
CREATE VIEW v_peminjaman_aktif AS
SELECT p.id,
b.judul,
a.nama AS peminjam,
p.tanggal_pinjam,
date(p.tanggal_pinjam, '+14 days') AS jatuh_tempo
FROM peminjaman p
JOIN buku b ON b.id = p.buku_id
JOIN anggota a ON a.id = p.anggota_id
WHERE p.tanggal_kembali IS NULL;
-- Kumpulkan statistik tabel dan index untuk query planner.
ANALYZE;
schema.sql
Unduh berkas-- schema.sql — Katalog Perpustakaan
-- Bagian 1: skema database untuk katalog perpustakaan.
-- Jalankan: sqlite3 perpustakaan.db < schema.sql
PRAGMA foreign_keys = ON;
-- Lokasi fisik buku di ruang perpustakaan.
CREATE TABLE rak (
kode TEXT PRIMARY KEY,
lokasi TEXT NOT NULL
);
-- Kategori buku. Dipisah dari tabel buku agar nama kategori
-- tidak ditulis ulang di setiap baris buku (normalisasi).
CREATE TABLE kategori (
id INTEGER PRIMARY KEY,
nama TEXT NOT NULL UNIQUE
);
-- Data buku. Satu buku masuk tepat satu kategori dan
-- disimpan di tepat satu rak.
CREATE TABLE buku (
id INTEGER PRIMARY KEY,
judul TEXT NOT NULL,
penulis TEXT NOT NULL,
tahun INTEGER NOT NULL CHECK (tahun >= 1900),
isbn TEXT NOT NULL UNIQUE,
kategori_id INTEGER NOT NULL REFERENCES kategori(id)
ON DELETE RESTRICT,
rak_kode TEXT NOT NULL REFERENCES rak(kode)
ON DELETE RESTRICT,
jumlah_tersedia INTEGER NOT NULL CHECK (jumlah_tersedia >= 0)
);
-- Anggota perpustakaan.
CREATE TABLE anggota (
id INTEGER PRIMARY KEY,
nama TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
tanggal_gabung TEXT NOT NULL
);
-- Catatan peminjaman. tanggal_kembali bernilai NULL
-- selama buku masih di tangan anggota.
CREATE TABLE peminjaman (
id INTEGER PRIMARY KEY,
buku_id INTEGER NOT NULL REFERENCES buku(id)
ON DELETE CASCADE,
anggota_id INTEGER NOT NULL REFERENCES anggota(id)
ON DELETE CASCADE,
tanggal_pinjam TEXT NOT NULL,
tanggal_kembali TEXT,
CHECK (tanggal_kembali IS NULL OR tanggal_kembali >= tanggal_pinjam)
);
seed.sql
Unduh berkas-- seed.sql — Katalog Perpustakaan
-- Bagian 2: data awal yang realistis untuk perpustakaan.
-- Jalankan setelah schema.sql: sqlite3 perpustakaan.db < seed.sql
PRAGMA foreign_keys = ON;
BEGIN TRANSACTION;
-- Empat rak tempat buku disimpan.
INSERT INTO rak (kode, lokasi) VALUES
('R1', 'Lantai 1, dekat pintu masuk'),
('R2', 'Lantai 1, sisi kanan'),
('R3', 'Lantai 2, ruang baca'),
('R4', 'Lantai 2, pojok belakang');
-- Enam kategori buku.
INSERT INTO kategori (id, nama) VALUES
(1, 'Fiksi'),
(2, 'Sains'),
(3, 'Sejarah'),
(4, 'Teknologi'),
(5, 'Puisi'),
(6, 'Biografi');
-- Lima belas judul buku di seluruh kategori.
-- jumlah_tersedia = jumlah eksemplar yang ada di rak saat ini.
INSERT INTO buku (id, judul, penulis, tahun, isbn, kategori_id, rak_kode, jumlah_tersedia) VALUES
(1, 'Bumi Manusia', 'Pramoedya Ananta Toer', 1980, '9789799731234', 1, 'R1', 3),
(2, 'Laskar Pelangi', 'Andrea Hirata', 2005, '9789793062792', 1, 'R1', 2),
(3, 'Cantik Itu Luka', 'Eka Kurniawan', 2002, '9789799105251', 1, 'R1', 2),
(4, 'Atheis', 'Achdiat K. Mihardja', 1949, '9789794334017', 1, 'R1', 1),
(5, 'Kosmos', 'Carl Sagan', 1980, '9786024240293', 2, 'R3', 2),
(6, 'A Brief History of Time', 'Stephen Hawking', 1988, '9780553175219', 2, 'R3', 1),
(7, 'Sapiens: Riwayat Singkat Umat Manusia', 'Yuval Noah Harari', 2011, '9786020427433', 3, 'R4', 2),
(8, 'Sejarah Indonesia Modern', 'M.C. Ricklefs', 1981, '9789793780824', 3, 'R4', 1),
(9, 'Clean Code', 'Robert C. Martin', 2008, '9780132350884', 4, 'R3', 2),
(10, 'The Pragmatic Programmer', 'David Thomas', 1999, '9780201616224', 4, 'R3', 1),
(11, 'The Mythical Man-Month', 'Frederick P. Brooks', 1975, '9780201835953', 4, 'R3', 1),
(12, 'Hujan Bulan Juni', 'Sapardi Djoko Damono', 1994, '9789799145359', 5, 'R2', 1),
(13, 'Deru Campur Debu', 'Chairil Anwar', 1949, '9789794336516', 5, 'R2', 1),
(14, 'Steve Jobs', 'Walter Isaacson', 2011, '9781451648546', 6, 'R4', 2),
(15, 'Long Walk to Freedom', 'Nelson Mandela', 1994, '9780349106533', 6, 'R4', 1);
-- Lima anggota perpustakaan.
INSERT INTO anggota (id, nama, email, tanggal_gabung) VALUES
(1, 'Budi Santoso', '[email protected]', '2024-01-15'),
(2, 'Siti Rahayu', '[email protected]', '2024-02-03'),
(3, 'Agus Prasetyo', '[email protected]', '2024-05-20'),
(4, 'Rina Wijaya', '[email protected]', '2025-01-08'),
(5, 'Eko Nugroho', '[email protected]', '2025-03-12');
-- Enam peminjaman; baris ke-5 belum dikembalikan
-- (tanggal_kembali NULL), itulah sebabnya jumlah_tersedia
-- buku Kosmos tinggal 2 dari 3 eksemplar.
INSERT INTO peminjaman (id, buku_id, anggota_id, tanggal_pinjam, tanggal_kembali) VALUES
(1, 1, 1, '2025-05-05', '2025-05-12'),
(2, 2, 2, '2025-05-10', '2025-05-24'),
(3, 9, 3, '2025-05-15', '2025-05-29'),
(4, 7, 4, '2025-06-02', '2025-06-05'),
(5, 5, 1, '2025-05-26', NULL),
(6, 12, 5, '2025-06-10', '2025-06-14');
COMMIT;