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:

IndexKolomAlasan
idx_buku_kategori_idbuku.kategori_idJOIN kategori ke buku pada Laporan 1 dan 4
idx_buku_rak_kodebuku.rak_kodepencarian buku menurut rak
idx_peminjaman_buku_idpeminjaman.buku_idriwayat peminjaman satu buku
idx_peminjaman_anggota_idpeminjaman.anggota_idriwayat peminjaman satu anggota
idx_buku_judulbuku.judulpencarian judul, kolom yang paling sering diketik pengguna
idx_peminjaman_tanggalpeminjaman.tanggal_pinjamrentang tanggal seperti “peminjaman bulan ini”
idx_peminjaman_aktifpeminjaman(buku_id) WHERE tanggal_kembali IS NULLindex 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 #

sql
-- 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;
  1. Empat index pertama — kolom foreign key dari buku dan peminjaman. Setiap JOIN dan setiap pencarian “anak dari induk X” kini menyusuri B-tree, bukan memindai tabel.
  2. idx_buku_judul dan idx_peminjaman_tanggal — dua kolom yang paling sering menjadi kriteria pencarian pengguna akhir.
  3. idx_peminjaman_aktif ... WHERE tanggal_kembali IS NULL — index parsial; isinya hanya baris peminjaman aktif, sehingga jauh lebih ramping daripada index penuh.
  4. v_buku_tersedia — subquery berkorelasi (SELECT COUNT(*) FROM peminjaman p WHERE ...) menghitung peminjaman aktif untuk setiap buku luar.
  5. v_peminjaman_aktif — JOIN tiga tabel plus kolom jatuh_tempo terenkapsulasi; pemakai cukup SELECT dari view.
  6. 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:

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

bash
sqlite3 perpustakaan.db < optimasi.sql

Ulangi ketiga query yang sama:

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

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

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

  1. buku|idx_buku_kategori_id|15 3 — index berisi 15 entri, dengan perkiraan rata-rata 3 baris per nilai kategori_id (lima belas buku tersebar di enam kategori).
  2. 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.
  3. sqlite_autoindex_* — index otomatis dari constraint UNIQUE (isbn, email, nama kategori, kode rak); 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:

bash
sqlite3 perpustakaan.db "PRAGMA foreign_key_check;"
sqlite3 perpustakaan.db "PRAGMA integrity_check;"

Output yang Dihasilkan:

text
ok

foreign_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
sql
-- 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
sql
-- 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
sql
-- 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
sql
-- 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;