Database relasional menyimpan data dalam bentuk tabel, dan SQL (Structured Query Language) adalah bahasa yang kita pakai untuk berbicara dengan tabel tersebut. PHP tidak memanipulasi baris data secara langsung; PHP menyusun perintah SQL, mengirimkannya ke database, lalu membaca hasil yang dikembalikan. Pada chapter ini kita fokus pada perintah SQL yang paling sering dipakai sehari-hari: membaca data (SELECT), menambah (INSERT), mengubah (UPDATE), menghapus (DELETE), menggabungkan dua tabel (JOIN), dan meringkas banyak baris menjadi satu nilai (fungsi agregat). Seluruh contoh berjalan di atas SQLite melalui PDO dengan database di memori (sqlite::memory:), sehingga Anda dapat mencobanya tanpa memasang server database apa pun.

Menyiapkan Tabel Latihan #

Kita butuh dua tabel kecil sebagai arena latihan: kategori yang menyimpan jenis barang, dan produk yang menyimpan barang dagangan beserta harga dan stoknya. Setiap baris produk merujuk ke satu baris kategori melalui kolom kategori_id — pola hubungan antartabel yang sudah kita kenal pada chapter Dasar Database Relasional . Database sqlite::memory: tercipta saat skrip berjalan dan hilang otomatis saat skrip selesai, jadi tidak ada berkas yang perlu dibersihkan.

Dua panggilan setAttribute() di awal cukup untuk memuluskan seluruh contoh: PDO::ATTR_ERRMODE bernilai PDO::ERRMODE_EXCEPTION membuat kesalahan SQL langsung melempar PDOException, sedangkan PDO::ATTR_DEFAULT_FETCH_MODE bernilai PDO::FETCH_ASSOC membuat setiap baris hasil query berupa array asosiatif. Kedua pengaturan ini akan dibedah tuntas pada chapter PDO dan PDO Connections ; untuk sekarang kita cukup pakai keduanya dan fokus ke SQL-nya.

php
<?php
$pdo = new PDO("sqlite::memory:");
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);

$pdo->exec("CREATE TABLE kategori (
    id   INTEGER PRIMARY KEY,
    nama TEXT NOT NULL
)");
$pdo->exec("CREATE TABLE produk (
    id          INTEGER PRIMARY KEY,
    nama        TEXT NOT NULL,
    harga       INTEGER NOT NULL,
    stok        INTEGER NOT NULL DEFAULT 0,
    kategori_id INTEGER NOT NULL REFERENCES kategori(id)
)");
$pdo->exec(
    "INSERT INTO kategori (nama) VALUES ('Elektronik'), ('Buku'), ('Alat Tulis')"
);
$pdo->exec("INSERT INTO produk (nama, harga, stok, kategori_id) VALUES
    ('Mouse Wireless', 85000, 12, 1),
    ('Keyboard Mekanik', 450000, 5, 1),
    ('Novel Laskar Pelangi', 95000, 20, 2),
    ('Kamus Bahasa Inggris', 120000, 8, 2),
    ('Pulpen Gel Hitam', 15000, 50, 3),
    ('Buku Tulis 58 Halaman', 5000, 100, 3)");

echo "Isi tabel kategori:\n";
foreach ($pdo->query("SELECT * FROM kategori ORDER BY id") as $row) {
    echo "  #{$row['id']} {$row['nama']}\n";
}

echo "Isi tabel produk:\n";
foreach ($pdo->query("SELECT * FROM produk ORDER BY id") as $row) {
    echo "  {$row['nama']} - Rp{$row['harga']} (stok {$row['stok']})\n";
}

Output yang Dihasilkan:

text
Isi tabel kategori:
  #1 Elektronik
  #2 Buku
  #3 Alat Tulis
Isi tabel produk:
  Mouse Wireless - Rp85000 (stok 12)
  Keyboard Mekanik - Rp450000 (stok 5)
  Novel Laskar Pelangi - Rp95000 (stok 20)
  Kamus Bahasa Inggris - Rp120000 (stok 8)
  Pulpen Gel Hitam - Rp15000 (stok 50)
  Buku Tulis 58 Halaman - Rp5000 (stok 100)

Mari kita telaah kode di atas baris per baris:

  1. new PDO("sqlite::memory:"): membuka koneksi ke database SQLite yang seluruh isinya hanya hidup di memori. Objek $pdo menjadi pintu tunggal untuk mengirim perintah SQL.
  2. CREATE TABLE kategori: membuat tabel dengan kolom id bertipe INTEGER PRIMARY KEY — di SQLite, kolom ini terisi otomatis bertambah setiap kali baris baru disisipkan tanpa id eksplisit.
  3. CREATE TABLE produk: kolom kategori_id dideklarasikan REFERENCES kategori(id), artinya nilainya harus merujuk ke baris yang ada di tabel kategori (kunci tamu).
  4. INSERT INTO ... VALUES: menyisipkan baris baru. Satu perintah INSERT boleh menyisipkan beberapa baris sekaligus dengan memisahkan tiap kelompok nilai menggunakan tanda kurung dan koma.
  5. foreach ($pdo->query(...) as $row): metode query() menjalankan SELECT dan mengembalikan objek hasil yang bisa langsung diiterasi; setiap $row berupa array asosiatif seperti ['id' => 1, 'nama' => 'Elektronik'].

Membaca Data dengan SELECT #

Perintah SELECT menentukan kolom mana yang dibaca dan FROM menentukan tabel sumbernya. Klausa WHERE menyaring baris agar hanya yang memenuhi kondisi yang dikembalikan. Berikut operator penyaring yang paling sering dipakai:

OperatorMakna
=, <>sama dengan, tidak sama dengan
<, <=, >, >=perbandingan angka atau teks
BETWEEN a AND bnilai berada di dalam rentang a sampai b
IN (x, y, z)nilai cocok dengan salah satu daftar
LIKE 'M%'pencocokan pola teks (% mewakili teks apa pun)
AND, OR, NOTmenggabungkan beberapa kondisi

Mari kita gabungkan SELECT, WHERE, dan ORDER BY dalam satu contoh:

php
<?php
require __DIR__ . "/persiapan.php";

$sql = "SELECT nama, harga FROM produk
    WHERE kategori_id = 1 AND harga >= 80000
    ORDER BY harga DESC";

foreach ($pdo->query($sql) as $row) {
    echo "{$row['nama']} - Rp{$row['harga']}\n";
}

Output yang Dihasilkan:

text
Keyboard Mekanik - Rp450000
Mouse Wireless - Rp85000

Query di atas membaca dua kolom dari tabel produk, menyaring baris dengan dua kondisi yang digabung AND (kategori 1 dan harga minimal 80000), lalu mengurutkan hasil dengan ORDER BY harga DESC dari yang termahal. Kata kunci DESC berarti menurun; tanpa DESC, urutan naik (ASC) yang berlaku. Klausa LIMIT memotong jumlah baris yang dikembalikan — pola yang umum dipakai untuk menampilkan beberapa item teratas:

php
<?php
require __DIR__ . "/persiapan.php";

$sql = "SELECT nama, harga FROM produk ORDER BY harga DESC LIMIT 3";

echo "Tiga produk termahal:\n";
foreach ($pdo->query($sql) as $row) {
    echo "  {$row['nama']} - Rp{$row['harga']}\n";
}

Output yang Dihasilkan:

text
Tiga produk termahal:
  Keyboard Mekanik - Rp450000
  Kamus Bahasa Inggris - Rp120000
  Novel Laskar Pelangi - Rp95000

Menambah Data dengan INSERT #

Perintah INSERT INTO nama_tabel (kolom1, kolom2, ...) VALUES (nilai1, nilai2, ...) menyisipkan baris baru ke tabel. Kolom yang tidak disebutkan akan terisi nilai defaultnya — pada tabel produk, kolom stok memiliki DEFAULT 0, sedangkan id terisi otomatis oleh SQLite. Metode exec() mengembalikan jumlah baris yang terpengaruh, dan lastInsertId() melaporkan id yang baru dibuat:

php
<?php
require __DIR__ . "/persiapan.php";

$jumlah = $pdo->exec("INSERT INTO produk (nama, harga, stok, kategori_id)
    VALUES ('Powerbank 10000mAh', 199000, 25, 1)");
echo "$jumlah baris ditambahkan\n";

$idBaru = $pdo->lastInsertId();
echo "ID produk baru: $idBaru\n";

$row = $pdo->query("SELECT nama, harga FROM produk WHERE id = $idBaru")->fetch();
echo "Tersimpan: {$row['nama']} - Rp{$row['harga']}\n";

Output yang Dihasilkan:

text
1 baris ditambahkan
ID produk baru: 7
Tersimpan: Powerbank 10000mAh - Rp199000

Contoh di atas menyisipkan satu baris (exec() mengembalikan 1), membaca id baru lewat lastInsertId(), lalu memverifikasi hasilnya dengan SELECT yang dipanggil fetch() untuk mengambil satu baris pertama. Nilai $idBaru pada contoh ini berasal dari database itu sendiri, bukan dari input luar; ketika data berasal dari pengguna, cara yang benar adalah prepared statements yang dibahas pada chapter Prepared Statements — jangan pernah merangkai nilai eksternal langsung ke dalam string SQL.

Mengubah Data dengan UPDATE #

Perintah UPDATE nama_tabel SET kolom = nilai WHERE kondisi mengubah kolom pada baris-baris yang memenuhi kondisi. Nilai baru boleh merujuk ke nilai lama baris itu sendiri, misalnya stok = stok - 2 yang mengurangi stok saat ini sebesar dua:

php
<?php
require __DIR__ . "/persiapan.php";

$jumlah = $pdo->exec("UPDATE produk SET harga = 79000, stok = stok - 2 WHERE id = 1");
echo "$jumlah baris diperbarui\n";

$row = $pdo->query("SELECT nama, harga, stok FROM produk WHERE id = 1")->fetch();
echo "{$row['nama']} sekarang Rp{$row['harga']} dengan stok {$row['stok']}\n";

Output yang Dihasilkan:

text
1 baris diperbarui
Mouse Wireless sekarang Rp79000 dengan stok 10

Query tersebut menurunkan harga produk bernomor id 1 menjadi 79000 dan stoknya berkurang dari 12 menjadi 10. Ekspresi stok = stok - 2 dievaluasi per baris: database membaca nilai stok baris tersebut, menguranginya, lalu menulis hasilnya kembali.

Menghapus Data dengan DELETE #

Perintah DELETE FROM nama_tabel WHERE kondisi membuang baris yang memenuhi kondisi. Penghapusan bersifat permanen pada database berkas; tidak ada tempat sampah untuk mengembalikan baris yang sudah terhapus. Contoh berikut mensimulasikan produk yang stoknya habis lalu menghapusnya:

php
<?php
require __DIR__ . "/persiapan.php";

// Simulasi: stok Kamus Bahasa Inggris habis terjual
$pdo->exec("UPDATE produk SET stok = 0 WHERE id = 4");

$jumlah = $pdo->exec("DELETE FROM produk WHERE stok = 0");
echo "$jumlah baris dihapus\n";

$sisa = $pdo->query("SELECT COUNT(*) FROM produk")->fetchColumn();
echo "Sisa produk: $sisa\n";

Output yang Dihasilkan:

text
1 baris dihapus
Sisa produk: 5

Setelah DELETE menghapus satu baris, SELECT COUNT(*) melaporkan tersisa lima produk. Fungsi agregat COUNT(*) di sini sekaligus menjadi jembatan ke bagian terakhir chapter: meringkas banyak baris menjadi satu angka.

Menggabungkan Tabel dengan JOIN #

Data yang sudah dinormalisasi tersebar di beberapa tabel, dan JOIN adalah cara menyatukannya kembali saat membaca. INNER JOIN mengembalikan hanya baris yang punya pasangan cocok di kedua tabel berdasarkan kondisi ON:

php
<?php
require __DIR__ . "/persiapan.php";

$sql = "SELECT p.nama AS produk, k.nama AS kategori
    FROM produk p
    INNER JOIN kategori k ON k.id = p.kategori_id
    ORDER BY p.id";

foreach ($pdo->query($sql) as $row) {
    echo "{$row['produk']} ({$row['kategori']})\n";
}

Output yang Dihasilkan:

text
Mouse Wireless (Elektronik)
Keyboard Mekanik (Elektronik)
Novel Laskar Pelangi (Buku)
Kamus Bahasa Inggris (Buku)
Pulpen Gel Hitam (Alat Tulis)
Buku Tulis 58 Halaman (Alat Tulis)

Tabel produk diberi alias p dan kategori diberi alias k agar referensi kolom ringkas. Kondisi ON k.id = p.kategori_id memasangkan setiap produk dengan kategorinya; kata kunci AS memberi nama baru pada kolom hasil (produk dan kategori) sehingga mudah diakses dari $row. LEFT JOIN bekerja mirip, tetapi mempertahankan semua baris tabel kiri meskipun tidak punya pasangan — kolom dari tabel kanan terisi NULL:

php
<?php
require __DIR__ . "/persiapan.php";

// Tambahkan kategori baru yang belum memiliki produk
$pdo->exec("INSERT INTO kategori (nama) VALUES ('Olahraga')");

$sql = "SELECT k.nama AS kategori, p.nama AS produk
    FROM kategori k
    LEFT JOIN produk p ON p.kategori_id = k.id
    ORDER BY k.id, p.id";

foreach ($pdo->query($sql) as $row) {
    $namaProduk = $row["produk"] ?? "(belum ada produk)";
    echo "{$row['kategori']}: $namaProduk\n";
}

Output yang Dihasilkan:

text
Elektronik: Mouse Wireless
Elektronik: Keyboard Mekanik
Buku: Novel Laskar Pelangi
Buku: Kamus Bahasa Inggris
Alat Tulis: Pulpen Gel Hitam
Alat Tulis: Buku Tulis 58 Halaman
Olahraga: (belum ada produk)

Kategori Olahraga tetap muncul di hasil walau belum memiliki satu pun produk; nilai NULL pada kolom p.nama ditangani operator ?? di sisi PHP agar tercetak sebagai teks pengganti. Dengan pola ini, laporan “kategori mana saja yang kosong” dapat dibaca langsung dari hasil LEFT JOIN.

Fungsi Agregat dan GROUP BY #

Fungsi agregat meringkas banyak baris menjadi satu nilai: COUNT() menghitung jumlah baris, SUM() menjumlahkan, AVG() merata-ratakan, sementara MAX() dan MIN() mencari nilai terbesar dan terkecil. Contoh berikut meringkas seluruh tabel produk dalam satu baris hasil:

php
<?php
require __DIR__ . "/persiapan.php";

$sql = "SELECT COUNT(*) AS jumlah,
        SUM(harga * stok) AS totalNilai,
        AVG(harga) AS rataRata
    FROM produk";

$row = $pdo->query($sql)->fetch();
echo "Jumlah produk    : {$row['jumlah']}\n";
echo "Total nilai stok : Rp{$row['totalNilai']}\n";
echo "Harga rata-rata  : Rp" . round($row['rataRata']) . "\n";

Output yang Dihasilkan:

text
Jumlah produk    : 6
Total nilai stok : Rp7380000
Harga rata-rata  : Rp128333

Ekspresi harga * stok dihitung per baris sebelum dijumlahkan oleh SUM(), menghasilkan total nilai seluruh stok toko. AVG() mengembalikan bilangan pecahan, sehingga contoh membulatkannya dengan round() di sisi PHP agar mudah dibaca. Klausa GROUP BY membagi baris ke dalam kelompok lalu menerapkan agregat per kelompok — inilah pola laporan per kategori:

php
<?php
require __DIR__ . "/persiapan.php";

$sql = "SELECT k.nama AS kategori, COUNT(*) AS jumlah, MAX(p.harga) AS termahal
    FROM produk p
    INNER JOIN kategori k ON k.id = p.kategori_id
    GROUP BY k.nama
    ORDER BY termahal DESC";

foreach ($pdo->query($sql) as $row) {
    echo "{$row['kategori']}: {$row['jumlah']} produk, termahal Rp{$row['termahal']}\n";
}

Output yang Dihasilkan:

text
Elektronik: 2 produk, termahal Rp450000
Buku: 2 produk, termahal Rp120000
Alat Tulis: 2 produk, termahal Rp15000

GROUP BY k.nama mengelompokkan baris hasil JOIN berdasarkan nama kategori, lalu COUNT(*) dan MAX() dihitung di dalam tiap kelompok. Alias termahal dari SELECT bahkan bisa dipakai ulang oleh ORDER BY. Jika kelompoknya sendiri perlu disaring (misalnya hanya kategori dengan minimal dua produk), SQL menyediakan klausa HAVING jumlah >= 2 yang ditulis setelah GROUP BY — seperti WHERE, tetapi bekerja pada hasil agregat.

Ringkasan #

Kita kini menguasai perintah SQL inti untuk bekerja sehari-hari: SELECT dengan WHERE, ORDER BY, dan LIMIT untuk membaca; INSERT, UPDATE, dan DELETE untuk mengubah; JOIN untuk menggabungkan tabel; serta fungsi agregat dengan GROUP BY untuk meringkas. Semuanya kita jalankan dari PHP melalui satu pintu bernama PDO. Pertanyaan berikutnya adalah bagaimana PHP benar-benar berbicara dengan server database pilihan kita — jawabannya ada pada ekstensi dan driver yang akan kita petakan di chapter Database Drivers .

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

contoh.php

Unduh berkas
php
<?php
/**
 * contoh.php - rangkuman Chapter Dasar SQL.
 *
 * Program ini menjalankan alur lengkap: membaca data (SELECT + JOIN),
 * menambah (INSERT), mengubah (UPDATE), menghapus (DELETE), lalu
 * meringkas dengan fungsi agregat. Seluruhnya berjalan pada database
 * SQLite di memori melalui PDO.
 */

require __DIR__ . "/persiapan.php";

// 1. SELECT dengan JOIN: produk beserta kategorinya
echo "== Daftar produk ==\n";
$sql = "SELECT p.nama AS produk, k.nama AS kategori, p.harga, p.stok
    FROM produk p
    INNER JOIN kategori k ON k.id = p.kategori_id
    ORDER BY p.id";
foreach ($pdo->query($sql) as $row) {
    echo "  {$row['produk']} ({$row['kategori']}) - Rp{$row['harga']}, stok {$row['stok']}\n";
}

// 2. INSERT: produk baru
$pdo->exec("INSERT INTO produk (nama, harga, stok, kategori_id)
    VALUES ('Powerbank 10000mAh', 199000, 25, 1)");
echo "Produk baru ditambahkan dengan ID " . $pdo->lastInsertId() . "\n";

// 3. UPDATE: diskon 10% untuk seluruh produk Elektronik
$jumlah = $pdo->exec("UPDATE produk
    SET harga = harga * 0.9
    WHERE kategori_id = (SELECT id FROM kategori WHERE nama = 'Elektronik')");
echo "$jumlah baris diperbarui oleh UPDATE\n";

// 4. DELETE: hapus produk yang stoknya di bawah 6
$jumlah = $pdo->exec("DELETE FROM produk WHERE stok < 6");
echo "$jumlah baris dihapus oleh DELETE\n";

// 5. Agregat: ringkasan nilai stok per kategori
echo "== Ringkasan per kategori ==\n";
$sql = "SELECT k.nama AS kategori, COUNT(*) AS jumlah, SUM(p.harga * p.stok) AS totalNilai
    FROM produk p
    INNER JOIN kategori k ON k.id = p.kategori_id
    GROUP BY k.nama
    ORDER BY k.nama";
foreach ($pdo->query($sql) as $row) {
    echo "  {$row['kategori']}: {$row['jumlah']} produk, total nilai Rp{$row['totalNilai']}\n";
}

persiapan.php

Unduh berkas
php
<?php
/**
 * persiapan.php - basis data latihan untuk Chapter Dasar SQL.
 *
 * Berkas ini membuat database SQLite di memori (sqlite::memory:) lalu
 * mengisi dua tabel: kategori dan produk. Contoh-contoh pada chapter
 * ini memuat berkas ini dengan require agar setiap blok kode dapat
 * dijalankan secara mandiri.
 */

$pdo = new PDO("sqlite::memory:");
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);

$pdo->exec("CREATE TABLE kategori (
    id   INTEGER PRIMARY KEY,
    nama TEXT NOT NULL
)");

$pdo->exec("CREATE TABLE produk (
    id          INTEGER PRIMARY KEY,
    nama        TEXT NOT NULL,
    harga       INTEGER NOT NULL,
    stok        INTEGER NOT NULL DEFAULT 0,
    kategori_id INTEGER NOT NULL REFERENCES kategori(id)
)");

$pdo->exec(
    "INSERT INTO kategori (nama) VALUES ('Elektronik'), ('Buku'), ('Alat Tulis')"
);

$pdo->exec("INSERT INTO produk (nama, harga, stok, kategori_id) VALUES
    ('Mouse Wireless', 85000, 12, 1),
    ('Keyboard Mekanik', 450000, 5, 1),
    ('Novel Laskar Pelangi', 95000, 20, 2),
    ('Kamus Bahasa Inggris', 120000, 8, 2),
    ('Pulpen Gel Hitam', 15000, 50, 3),
    ('Buku Tulis 58 Halaman', 5000, 100, 3)");