Normalisasi mengajarkan memecah fakta ke banyak tabel; kunci adalah kontrak yang membuat hasil pecahan tetap utuh. Primary key memberi setiap baris pengenal yang dijamin unik, dan foreign key memastikan rujukan antar tabel selalu menunjuk baris yang benar-benar ada. Tanpa keduanya, tidak ada yang melarang pesanan menunjuk pelanggan id 99 yang tidak pernah terdaftar — data rusak diam-diam tanpa satu pun galat. Chapter ini menguji seluruh kontrak tersebut secara nyata di SQLite.
Primary Key: Natural vs Surrogate #
Kunci natural adalah kolom yang unik karena sifat dunia nyata: nomor SKU produk, alamat email, nomor polisi. Kunci surrogate adalah pengenal buatan (id bertambah otomatis) yang tidak punya makna di luar database. Pilihan di antara keduanya adalah trade-off klasik:
| Aspek | Natural key | Surrogate key |
|---|---|---|
| Makna bagi bisnis | ada, langsung terbaca | tidak ada |
| Kestabilan | bisa berubah (email ganti, SKU rombak) | tidak pernah berubah |
| Ukuran pada join | bisa panjang (teks) | kecil dan seragam (integer) |
| Jaminan keunikan | bergantung proses bisnis | dijamin database |
Praktik umum untuk tabel dimensi toko online: pakai id integer sebagai primary key, lalu lindungi kunci natural dengan UNIQUE — sku TEXT NOT NULL UNIQUE. Kunci join jadi ringkas tanpa kehilangan jaminan keunikan SKU.
Foreign Key Menjaga Rujukan #
Berikut skema uji lengkap. Perhatikan klausa ON DELETE pada tiap foreign key — perilakunya kita uji sebentar lagi:
import sqlite3
koneksi = sqlite3.connect(":memory:")
koneksi.execute("PRAGMA foreign_keys = ON")
koneksi.executescript("""
CREATE TABLE kategori (
id INTEGER PRIMARY KEY,
nama TEXT NOT NULL UNIQUE
);
CREATE TABLE pelanggan (
id INTEGER PRIMARY KEY,
nama TEXT NOT NULL,
email TEXT NOT NULL UNIQUE
);
CREATE TABLE produk (
id INTEGER PRIMARY KEY,
sku TEXT NOT NULL UNIQUE,
nama TEXT NOT NULL,
kategori_id INTEGER REFERENCES kategori(id) ON DELETE SET NULL,
harga INTEGER NOT NULL CHECK (harga >= 0)
);
CREATE TABLE pesanan (
id INTEGER PRIMARY KEY,
pelanggan_id INTEGER NOT NULL REFERENCES pelanggan(id) ON DELETE RESTRICT,
tanggal TEXT NOT NULL
);
CREATE TABLE detail_pesanan (
pesanan_id INTEGER REFERENCES pesanan(id) ON DELETE CASCADE,
produk_id INTEGER REFERENCES produk(id) ON DELETE RESTRICT,
jumlah INTEGER NOT NULL CHECK (jumlah > 0),
PRIMARY KEY (pesanan_id, produk_id)
);
INSERT INTO kategori VALUES (1, 'elektronik'), (2, 'fashion');
INSERT INTO pelanggan VALUES
(1, 'Budi Santoso', '[email protected]'),
(2, 'Siti Rahayu', '[email protected]');
INSERT INTO produk VALUES
(1, 'ELK-001', 'Smartphone X1', 1, 3500000),
(2, 'FSH-001', 'Kemeja Batik', 2, 250000);
INSERT INTO pesanan VALUES
(1, 1, '2024-06-01'), (2, 2, '2024-06-03');
INSERT INTO detail_pesanan VALUES
(1, 1, 1), (1, 2, 2), (2, 2, 1);
""")
def uji(nama, sql):
try:
koneksi.execute(sql)
print(f"{nama}: BERHASIL")
except sqlite3.IntegrityError as galat:
print(f"{nama}: DITOLAK -> {galat}")
uji("email duplikat",
"INSERT INTO pelanggan VALUES (3, 'Dewi Lestari', '[email protected]')")
uji("pesanan ke pelanggan yang tidak ada",
"INSERT INTO pesanan VALUES (3, 99, '2024-07-01')")
uji("hapus pelanggan yang masih punya pesanan",
"DELETE FROM pelanggan WHERE id = 1")
uji("jumlah nol pada detail",
"INSERT INTO detail_pesanan VALUES (2, 1, 0)")Output yang Dihasilkan:
email duplikat: DITOLAK -> UNIQUE constraint failed: pelanggan.email
pesanan ke pelanggan yang tidak ada: DITOLAK -> FOREIGN KEY constraint failed
hapus pelanggan yang masih punya pesanan: DITOLAK -> FOREIGN KEY constraint failed
jumlah nol pada detail: DITOLAK -> CHECK constraint failed: jumlah > 0Empat penolakan di atas memperlihatkan empat constraint bekerja sekaligus. Baris pertama: UNIQUE menolak email milik Budi dipakai Dewi. Baris kedua: foreign key menolak pesanan baru menunjuk pelanggan 99 yang tidak pernah terdaftar. Baris ketiga: foreign key yang sama menolak penghapusan pelanggan 1 karena pesanan 1 masih merujuk pelanggan tersebut — rujukan menggantung dicegah dari dua arah. Baris keempat: CHECK menolak jumlah pembelian nol yang tidak masuk akal untuk bisnis.
Perilaku ON DELETE: CASCADE, RESTRICT, dan SET NULL #
Ketika baris induk dihapus, anak-anaknya yang merujuk harus ada keputusannya. Tiga perilaku yang dipakai skema di atas diuji satu per satu:
print("-- CASCADE: hapus pesanan 1, detail ikut terhapus --")
koneksi.execute("DELETE FROM pesanan WHERE id = 1")
for b in koneksi.execute("SELECT * FROM detail_pesanan"):
print(b)
print("-- RESTRICT: hapus produk 2 yang masih dirujuk --")
uji("hapus produk 2", "DELETE FROM produk WHERE id = 2")
print("-- SET NULL: hapus kategori 2, kategori_id produk jadi NULL --")
koneksi.execute("DELETE FROM kategori WHERE id = 2")
for b in koneksi.execute("SELECT id, sku, nama, kategori_id FROM produk"):
print(b)Output yang Dihasilkan:
-- CASCADE: hapus pesanan 1, detail ikut terhapus --
(2, 2, 1)
-- RESTRICT: hapus produk 2 yang masih dirujuk --
hapus produk 2: DITOLAK -> FOREIGN KEY constraint failed
-- SET NULL: hapus kategori 2, kategori_id produk jadi NULL --
(1, 'ELK-001', 'Smartphone X1', 1)
(2, 'FSH-001', 'Kemeja Batik', None)Mari kita telaah ketiga hasilnya:
ON DELETE CASCADE(pesanan → detail): menghapus pesanan 1 otomatis menghapus dua baris detailnya; yang tersisa hanya detail pesanan 2. Cocok untuk data anak yang tak bermakna tanpa induknya.ON DELETE RESTRICT(produk → detail): produk 2 masih dibeli di pesanan 2, jadi penghapusannya ditolak sampai riwayat detail dipindahkan atau dihapus manual. Cocok untuk data yang tidak boleh kehilangan konteks sejarah.ON DELETE SET NULL(kategori → produk): kategori 2 dihapus, dankategori_idproduk 2 berubah menjadiNULL— produk tetap ada, hanya kehilangan kategorinya. Cocok untuk relasi opsional.
SQLite juga mengenal ON DELETE NO ACTION (perilaku bawaan) yang secara efek mirip RESTRICT: keduanya menolak penghapusan induk yang masih dirujuk. Perbedaannya baru terasa pada constraint yang ditunda (deferred) — tanpa penundaan, keduanya menolak seketika. Rincian seluruh perilaku foreign key ada di dukungan foreign key SQLite
.
Rangkuman Constraint #
| Constraint | Yang dijaga | Contoh pada skema |
|---|---|---|
PRIMARY KEY | keunikan dan keberadaan pengenal | id tiap tabel, PK komposit detail_pesanan |
FOREIGN KEY | rujukan menunjuk baris yang ada | pelanggan_id pada pesanan |
NOT NULL | kolom wajib terisi | nama, tanggal |
UNIQUE | tanpa duplikat | email, sku, nama kategori |
CHECK | aturan nilai khusus | harga >= 0, jumlah > 0 |
Constraint adalah filter kualitas di pintu masuk data — baris yang melanggar ditolak sebelum tersimpan. Namun constraint menjaga kebenaran, bukan kecepatan: mencari satu baris di antara jutaan baris tetap lambat bila database memeriksanya satu per satu. Struktur yang membuat pencarian melompat langsung ke baris tujuan adalah materi chapter Indexing.