Sejauh ini data aplikasi kita hidup di variabel dan array — lenyap begitu request selesai. Aplikasi nyata butuh ingatan yang lebih awet: daftar pengguna, katalog produk, riwayat pesanan. Database relasional adalah jawaban yang telah teruji puluhan tahun untuk kebutuhan itu, dan SQL adalah bahasanya. Sebelum menyentuh kode PHP, kita perlu memahami dulu cara pandangnya: data tinggal di tabel, identitas dijaga primary key, hubungan dibangun lewat relasi, dan konsistensi dijamin properti ACID. Chapter ini meletakkan fondasi tersebut dengan bantuan SQLite sebagai tempat berlatih yang ringan.
Tabel, Baris, dan Kolom #
Database relasional menyimpan data dalam tabel — bayangkan lembar spreadsheet yang diberi nama. Setiap baris mewakili satu entitas nyata (satu pengguna, satu pesanan), dan setiap kolom mewakili satu atribut entitas tersebut dengan tipe data yang tetap. Tabel pengguna yang paling sederhana bisa berisi data seperti ini:
| id | nama | |
|---|---|---|
| 1 | Budi | [email protected] |
| 2 | Siti | [email protected] |
Perbedaan mendasar dengan array PHP ada pada penegasan struktur di awal: kolom dan tipenya dideklarasikan lebih dulu lewat CREATE TABLE, lalu baris data menempati struktur tersebut. Deklarasi inilah yang membuat database bisa menolak data yang tidak berbentuk — kolom email bertipe teks tidak akan menerima angka tanpa makna, dan kolom bertipe integer menolak string acak. Kita bebas menambah baris sebanyak apa pun, tetapi bentuknya harus patuh pada skema.
Primary Key Menjaga Identitas #
Setiap baris membutuhkan cara untuk dirujuk secara unik, dan itulah tugas primary key: satu kolom (atau gabungan kolom) yang nilainya unik untuk setiap baris dan tidak boleh kosong. Pada tabel di atas, kolom id menjalankan peran itu — Budi adalah baris id = 1, sekalipun suatu hari ada pengguna lain yang bernama sama. Saat pesanan Budi perlu mencatat pemiliknya, pesanan tersebut cukup menyimpan angka 1 yang ringkas dan abadi.
Praktik yang paling umum adalah integer yang bertambah otomatis (AUTOINCREMENT di SQLite). Ada juga pendekatan kunci alami seperti NIK atau kode ISBN, tetapi kunci semacam itu rapuh: nilainya bisa berubah, salah ketik, atau formatnya berganti — sedangkan primary key idealnya tidak pernah berubah seumur hidup barisnya. Karena itu integer otomatis menjadi pilihan aman bawaan di hampir semua tabel.
Relasi Antar-Tabel #
Kekuatan sebenarnya muncul ketika tabel saling terhubung. Alih-alih menumpuk semua data pengguna di setiap baris pesanan, kita cukup menyimpan pengguna_id — foreign key yang menunjuk ke primary key tabel pengguna. Hubungan antar-tabel datang dalam tiga bentuk dasar:
- Satu-ke-satu (1:1): satu baris A terhubung tepat satu baris B — misalnya
penggunadanprofilyang menyimpan data tambahan yang jarang dibaca. - Satu-ke-banyak (1:N): satu pengguna memiliki banyak pesanan; satu pesanan hanya milik satu pengguna. Ini relasi paling sering dipakai.
- Banyak-ke-banyak (M:N): satu pesanan memuat banyak produk, satu produk masuk banyak pesanan. Relasi ini tidak bisa langsung disimpan, jadi kita perantarai dengan junction table (
item_pesanan) yang setiap barisnya menghubungkan satu pesanan dengan satu produk beserta jumlahnya.
Diagram berikut memetakan kelima tabel tersebut — perhatikan bagaimana relasi M:N antara PESANAN dan PRODUK terwujud lewat ITEM_PESANAN di tengah:
Notasi || berarti “tepat satu”, o{ berarti “nol atau banyak”, dan |{ berarti “satu atau banyak”. Membaca diagram dari kiri ke kanan: satu pengguna memiliki tepat satu profil, membuat nol atau banyak pesanan; setiap pesanan memuat satu atau banyak item; dan setiap item menunjuk satu produk dari satu kategori. Model visual semacam ini (Entity-Relationship diagram) adalah alat baku perancangan skema sebelum satu baris SQL ditulis.
Skema dan Data #
Penting memisahkan dua hal yang berbeda jenis: skema adalah struktur (tabel, kolom, tipe, relasi — diubah jarang dan hati-hati), sedangkan data adalah isi barisnya (berubah setiap detik). SQL juga memisahkan keduanya dengan tegas: CREATE TABLE mendefinisikan skema, INSERT dan SELECT mengolah data. Mari kita lihat keduanya bekerja memakai SQLite — database yang utuh dalam satu file dan tersedia di PHP tanpa instalasi server apa pun:
<?php
$pdo = new PDO('sqlite::memory:');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->exec('CREATE TABLE pengguna (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nama TEXT NOT NULL,
email TEXT NOT NULL UNIQUE
)');
$pdo->exec('CREATE TABLE pesanan (
id INTEGER PRIMARY KEY AUTOINCREMENT,
pengguna_id INTEGER NOT NULL REFERENCES pengguna(id),
total INTEGER NOT NULL
)');
$jumlah = $pdo->query(
"SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'"
)->fetchColumn();
echo "Jumlah tabel: $jumlah\n";Output yang Dihasilkan:
Jumlah tabel: 2PDO di sini adalah extension bawaan PHP untuk berbicara dengan database — untuk saat ini cukup kita anggap sebagai pintu masuk; detail koneksi dan penggunaannya adalah pokok bahasan chapter berikutnya. Mari kita bedah skema di atas:
CREATE TABLE pengguna: mendeklarasikan tiga kolom.INTEGER PRIMARY KEY AUTOINCREMENTmenjadikanidprimary key yang terisi sendiri;NOT NULLmelarang nilai kosong;UNIQUEmenolak email kembar.CREATE TABLE pesanan: kolompengguna_idmerujukpengguna(id)lewatREFERENCES— inilah foreign key yang merealisasikan relasi 1:N.sqlite_master: tabel khusus SQLite yang mencatat seluruh objek database; query menghitung tabel buatan kita (baris internal berawalansqlite_disaring) dan hasilnya2.
JOIN Menggabungkan Tabel yang Berelasi #
Relasi baru bermanfaat bila data dari beberapa tabel bisa dikembalikan sebagai satu hasil — dan itulah pekerjaan JOIN. Query berikut mengambil nama pengguna dari tabel pengguna dan mengagregasi pesanannya dari tabel pesanan, dihubungkan oleh kesamaan pengguna_id dengan pengguna.id:
<?php
$pdo = new PDO('sqlite::memory:');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->exec('CREATE TABLE pengguna (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nama TEXT NOT NULL,
email TEXT NOT NULL UNIQUE
)');
$pdo->exec('CREATE TABLE pesanan (
id INTEGER PRIMARY KEY AUTOINCREMENT,
pengguna_id INTEGER NOT NULL REFERENCES pengguna(id),
total INTEGER NOT NULL
)');
// Data: satu pengguna dengan dua pesanan
$pdo->exec("INSERT INTO pengguna (nama, email) VALUES ('Budi', '[email protected]')");
$stmt = $pdo->prepare('INSERT INTO pesanan (pengguna_id, total) VALUES (?, ?)');
$stmt->execute([1, 150000]);
$stmt->execute([1, 75000]);
// JOIN menggabungkan baris dari dua tabel yang berrelasi
$sql = 'SELECT pengguna.nama, COUNT(pesanan.id) AS jumlah_pesanan, SUM(pesanan.total) AS total
FROM pengguna
JOIN pesanan ON pesanan.pengguna_id = pengguna.id
GROUP BY pengguna.id';
foreach ($pdo->query($sql) as $baris) {
echo "{$baris['nama']}: {$baris['jumlah_pesanan']} pesanan, total Rp{$baris['total']}\n";
}Output yang Dihasilkan:
Budi: 2 pesanan, total Rp225000Hasilnya satu baris ringkasan per pengguna: nama Budi, dua pesanan, total Rp225000 — data yang tinggal di dua tabel berbeda menyatu lewat satu query, tanpa satu pun loop PHP yang menyusunnya sendiri. Inilah alasan utama memilih model relasional: agregasi dan penggabungan adalah operasi kelas satu di SQL, bukan kode yang kita rakit manual. Database juga bekerja lebih cepat karena penggabungan terjadi di mesin yang sama dengan datanya.
Index: Daftar Isi untuk Data #
Tanpa index, SELECT ... WHERE email = '[email protected]' dipenuhi dengan memindai tabel dari baris pertama sampai akhir — lambat saat tabel berisi jutaan baris. Index adalah struktur terurut terpisah yang menyimpan nilai kolom beserta alamat barisnya, bekerja seperti daftar isi buku: mencari lewat kolom terindeks menjadi lompatan langsung, bukan pembacaan halaman demi halaman. Membuatnya sederhana:
CREATE INDEX idx_pesanan_pengguna_id ON pesanan(pengguna_id);Primary key dan kolom UNIQUE otomatis memiliki index. Sebagai ganti kecepatan, setiap index memperlambat penulisan (INSERT/UPDATE harus ikut memperbarui index) dan memakan ruang — maka indexlah kolom yang sering menjadi kondisi pencarian atau penghubung JOIN, seperti foreign key pada contoh di atas, bukan semua kolom.
ACID: Jaminan Konsistensi #
Terakhir, database relasional menjanjikan empat properti yang dirangkum akronim ACID. Keempatnya adalah alasan kita memilihnya untuk data yang tidak boleh setengah jadi, seperti pembayaran dan stok:
| Properti | Artinya | Contoh efeknya |
|---|---|---|
| Atomicity | transaksi berlaku semua-atau-tidak-sama sekali | gagal di tengah transfer dana tidak meninggalkan saldo terpotong sepihak |
| Consistency | data selalu memenuhi aturan skema dan constraint | email kembar tertolak oleh UNIQUE |
| Isolation | transaksi bersamaan tidak saling melihat keadaan setengah jadi | dua pembelian stok terakhir tidak menjual barang yang sama dua kali |
| Durability | data yang tersimpan bertahan walau server mati | pesanan yang dikonfirmasi tetap ada setelah mati listrik |
Transaksi PHP yang memakai jaminan ini (beginTransaction() hingga commit()) akan kita praktikkan pada chapter khusus di section ini. Untuk saat ini, cukup pahami bahwa keempatnya melekat pada database relasional secara bawaan — bukan kode yang kita tulis sendiri.
SQLite untuk Belajar, MySQL dan PostgreSQL untuk Produksi #
SQLite menyimpan seluruh database (skema dan data) dalam satu berkas biasa tanpa server terpisah — ringan, nol konfigurasi, dan didukung PHP lewat driver pdo_sqlite yang selalu tersedia. Karakter ini menjadikannya pilihan utama untuk belajar, prototipe, aplikasi kecil, dan alat berkas-desktop. Untuk aplikasi web yang ditulis banyak proses sekaligus, MySQL dan PostgreSQL unggul: keduanya berjalan sebagai server tersendiri yang menangani ratusan koneksi bersamaan, replikasi, dan penjadwalan akses yang ketat. Kode PDO yang kita tulis di seluruh section ini sengaja dibuat netral — mengganti SQLite dengan MySQL atau PostgreSQL umumnya hanya mengganti DSN koneksi, bukan menulis ulang query. Perbandingan ketiganya dan langkah keamanannya menyusul di chapter driver.
Dengan peta konsep ini — tabel, primary key, relasi, index, ACID — kita siap mengambil kendali: membuka koneksi dari PHP, menjalankan query, dan membaca hasilnya lewat PDO pada chapter berikutnya.