Sepuluh chapter di belakang kita saling menyambung menjadi satu alur kerja: kebutuhan menjadi entitas, entitas menjadi tabel berelasi, relasi banyak-ke-banyak menjadi junction table, dan bentuk normal menjaga setiap fakta tinggal di rumahnya. Sekarang seluruh perangkat itu kita pakai sekaligus pada satu studi kasus utuh — merancang skema database aplikasi manajemen tugas dari nol. Kita akan mulai dari paragraf kebutuhan pengguna, mengidentifikasi empat entitas dan tiga relasi, menuliskan CREATE TABLE lengkap dengan foreign key dan constraint, mengisi data contoh yang sengaja memuat kasus tepi, lalu memverifikasi desain dengan query join, agregat, dan relasi banyak-ke-banyak. Chapter ini sekaligus menjadi review menyeluruh section: setiap keputusan yang kita ambil merujuk pada konsep yang sudah dibedah sebelumnya.
Langkah 1: Pahami Kebutuhan #
Desain dimulai dari suara pengguna, bukan dari CREATE TABLE. Tim kecil mendeskripsikan aplikasi yang mereka inginkan sebagai berikut:
“Kami butuh aplikasi manajemen tugas untuk tim kecil. Setiap anggota mendaftar sebagai pengguna dengan nama dan email; email tidak boleh dipakai dua orang. Pengguna membuat proyek dan menjadi pemiliknya; satu proyek bisa berisi banyak tugas, dan setiap tugas punya judul, status (belum, berjalan, atau selesai), serta tenggat yang belum wajib ditentukan. Setiap tugas dikerjakan oleh paling banyak satu anggota — bisa saja belum ditunjuk siapa pun. Selain itu, tugas boleh diberi beberapa label seperti desain, server, atau uji, dan satu label dipakai banyak tugas agar kami bisa memfilter pekerjaan lintas proyek.”
Paragraf ini memuat semua bahan: entitas, atribut, relasi, bahkan aturan keunikan dan nilai yang boleh kosong. Tugas kita menerjemahkannya menjadi struktur — dengan cara yang sama seperti chapter Entitas dan Atribut: menandai kata benda penting, lalu mengklasifikasikannya.
Langkah 2: Identifikasi Entitas dan Atribut #
Empat entitas lolos ujian “datanya perlu disimpan dan dicari kembali”:
| Entitas | Deskripsi | Atribut kunci |
|---|---|---|
pengguna | anggota tim yang menggunakan aplikasi | nama, email (unik) |
proyek | wadah pekerjaan yang dibuat satu pengguna | nama, pemilik_id, tanggal_buat |
tugas | satu pekerjaan di dalam proyek | judul, status, tenggat, pelaksana_id, proyek_id |
label | penanda lintas proyek untuk memfilter tugas | nama (unik) |
Kata benda lain tidak lolos: “tim” hanyalah kelompok pengguna tanpa data sendiri, dan “anggota” adalah sebutan untuk baris pengguna. Dua kolom pada tabel di atas sengaja masih menunggu keputusan relasi — pemilik_id, pelaksana_id, dan proyek_id baru bermakna setelah kita menentukan arah penunjuknya di langkah berikutnya.
Langkah 3: Tentukan Relasi #
Setiap pasangan entitas kita uji kalimat kebutuhannya lalu ditentukan bentuk relasinya:
| Pasangan | Kalimat kebutuhan | Bentuk relasi | Penempatan kunci |
|---|---|---|---|
pengguna–proyek | satu pengguna memiliki banyak proyek | satu-ke-banyak | proyek.pemilik_id sebagai FK |
proyek–tugas | satu proyek memuat banyak tugas | satu-ke-banyak | tugas.proyek_id sebagai FK |
pengguna–tugas | satu tugas dikerjakan paling banyak satu anggota | satu-ke-banyak (opsional) | tugas.pelaksana_id sebagai FK yang boleh NULL |
tugas–label | satu tugas berlabel banyak, satu label dipakai banyak tugas | banyak-ke-banyak | tabel perantara tugas_label |
Relasi ketiga menunjukkan pola FK NULL: “paling banyak satu” berarti tugas tetap menjadi tabel anak yang menyimpan penunjuk, tetapi penunjuknya boleh kosong sampai pelaksana ditunjuk. Relasi keempat mengikuti pola junction table: composite primary key (tugas_id, label_id) menolak pasangan ganda, dan dua foreign key mengarah ke masing-masing induk. Diagram entity-relationship lengkapnya terlihat sebagai berikut:
Langkah 4: Tulis Skema Lengkap #
Terjemahan diagram menjadi CREATE TABLE — lengkap dengan primary key, foreign key bernama, constraint UNIQUE, NOT NULL, dan DEFAULT:
DROP DATABASE IF EXISTS belajar_rdb;
CREATE DATABASE belajar_rdb;
USE belajar_rdb;
CREATE TABLE pengguna (
id INT AUTO_INCREMENT PRIMARY KEY,
nama VARCHAR(50) NOT NULL,
email VARCHAR(80) NOT NULL UNIQUE
);
CREATE TABLE proyek (
id INT AUTO_INCREMENT PRIMARY KEY,
nama VARCHAR(60) NOT NULL,
pemilik_id INT NOT NULL,
tanggal_buat DATE NOT NULL,
CONSTRAINT fk_proyek_pemilik
FOREIGN KEY (pemilik_id) REFERENCES pengguna(id)
);
CREATE TABLE tugas (
id INT AUTO_INCREMENT PRIMARY KEY,
judul VARCHAR(100) NOT NULL,
proyek_id INT NOT NULL,
pelaksana_id INT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'belum',
tenggat DATE NULL,
CONSTRAINT fk_tugas_proyek
FOREIGN KEY (proyek_id) REFERENCES proyek(id),
CONSTRAINT fk_tugas_pelaksana
FOREIGN KEY (pelaksana_id) REFERENCES pengguna(id)
);
CREATE TABLE label (
id INT AUTO_INCREMENT PRIMARY KEY,
nama VARCHAR(30) NOT NULL UNIQUE
);
CREATE TABLE tugas_label (
tugas_id INT NOT NULL,
label_id INT NOT NULL,
PRIMARY KEY (tugas_id, label_id),
CONSTRAINT fk_tl_tugas FOREIGN KEY (tugas_id) REFERENCES tugas(id),
CONSTRAINT fk_tl_label FOREIGN KEY (label_id) REFERENCES label(id)
);Output yang Dihasilkan:
Query OK, 0 rows affected, 1 warning (0.00 sec)
Query OK, 1 row affected (0.00 sec)
Database changed
Query OK, 0 rows affected (0.00 sec)
Query OK, 0 rows affected (0.00 sec)
Query OK, 0 rows affected (0.00 sec)
Query OK, 0 rows affected (0.00 sec)
Query OK, 0 rows affected (0.00 sec)Keputusan-keputusan constraint pada skema tersebut:
email ... UNIQUEdanlabel.nama ... UNIQUE— dua aturan keunikan langsung dari kalimat kebutuhan: satu email satu akun, satu nama label satu label.pemilik_id INT NOT NULL— proyek tanpa pemilik tidak diperbolehkan; relasi pengguna-proyek memang wajib terjadi saat proyek dibuat.pelaksana_id INT NULL— kebalikannya, tugas boleh lahir sebelum ada pelaksana; kolom penunjuk yang boleh kosong persis pola “paling banyak satu” dari chapter relasi satu-ke-banyak.status ... DEFAULT 'belum'— tugas baru tidak perlu mengisi status; nilai awalnyabelumhingga diperbarui.- Urutan pembuatan tabel mengikuti urutan relasi —
penggunalebih dulu karena dua tabel merujuknya;tugas_labelpaling akhir karena merujuktugasdanlabel. UrutanDROP DATABASE IF EXISTSdi pembuka memastikan tiap pembaca mulai dari kondisi bersih.
Langkah 5: Isi Data Contoh #
Data contoh sengaja memuat kasus tepi, bukan hanya jalur bahagia: ada tugas tanpa pelaksana (NULL), label yang belum dipakai oleh tugas mana pun, dan dua relasi label pada satu tugas:
INSERT INTO pengguna (nama, email) VALUES
('Budi Santoso', '[email protected]'),
('Siti Rahayu', '[email protected]'),
('Andi Wijaya', '[email protected]');
INSERT INTO proyek (nama, pemilik_id, tanggal_buat) VALUES
('Rilis Aplikasi Mobile', 1, '2026-01-02'),
('Migrasi Database', 2, '2026-01-10');
INSERT INTO tugas (judul, proyek_id, pelaksana_id, status, tenggat) VALUES
('Desain halaman login', 1, 1, 'selesai', '2026-01-08'),
('Implementasi notifikasi', 1, 2, 'berjalan', '2026-01-20'),
('Uji beta internal', 1, NULL, 'belum', '2026-02-01'),
('Backup database lama', 2, 3, 'selesai', '2026-01-15'),
('Uji replikasi', 2, 2, 'berjalan', '2026-01-30'),
('Pembaruan dokumentasi', 2, NULL, 'belum', '2026-02-10');
INSERT INTO label (nama) VALUES
('desain'), ('server'), ('uji'), ('dokumentasi'), ('prioritas');
INSERT INTO tugas_label (tugas_id, label_id) VALUES
(1, 1),
(2, 2),
(3, 3),
(4, 2),
(4, 3),
(5, 2),
(5, 3),
(6, 4);Output yang Dihasilkan:
Query OK, 3 rows affected (0.00 sec)
Records: 3 Duplicates: 0 Warnings: 0
Query OK, 2 rows affected (0.00 sec)
Records: 2 Duplicates: 0 Warnings: 0
Query OK, 6 rows affected (0.00 sec)
Records: 6 Duplicates: 0 Warnings: 0
Query OK, 5 rows affected (0.00 sec)
Records: 5 Duplicates: 0 Warnings: 0
Query OK, 8 rows affected (0.00 sec)
Records: 8 Duplicates: 0 Warnings: 0Langkah 6: Verifikasi dengan Query #
Query pertama adalah layar utama aplikasi: daftar tugas lengkap dengan proyek, pemilik, dan pelaksananya. Perhatikan pengguna di-join dua kali dengan alias berbeda — sekali untuk pemilik proyek, sekali untuk pelaksana tugas:
SELECT t.id, t.judul, pr.nama AS proyek,
pem.nama AS pemilik, plk.nama AS pelaksana, t.status
FROM tugas t
JOIN proyek pr ON t.proyek_id = pr.id
JOIN pengguna pem ON pr.pemilik_id = pem.id
LEFT JOIN pengguna plk ON t.pelaksana_id = plk.id
ORDER BY t.id;Output yang Dihasilkan:
+----+-------------------------+-----------------------+--------------+--------------+----------+
| id | judul | proyek | pemilik | pelaksana | status |
+----+-------------------------+-----------------------+--------------+--------------+----------+
| 1 | Desain halaman login | Rilis Aplikasi Mobile | Budi Santoso | Budi Santoso | selesai |
| 2 | Implementasi notifikasi | Rilis Aplikasi Mobile | Budi Santoso | Siti Rahayu | berjalan |
| 3 | Uji beta internal | Rilis Aplikasi Mobile | Budi Santoso | NULL | belum |
| 4 | Backup database lama | Migrasi Database | Siti Rahayu | Andi Wijaya | selesai |
| 5 | Uji replikasi | Migrasi Database | Siti Rahayu | Siti Rahayu | berjalan |
| 6 | Pembaruan dokumentasi | Migrasi Database | Siti Rahayu | NULL | belum |
+----+-------------------------+-----------------------+--------------+--------------+----------+
6 rows in set (0.00 sec)Enam tugas tampil lengkap; LEFT JOIN pada pelaksana menjaga dua tugas tanpa pelaksana tetap terlihat dengan kolom pelaksana berisi NULL. Query kedua merangkum kemajuan tiap proyek — agregat yang membaca tiga tabel sekaligus:
SELECT pr.nama AS proyek,
COUNT(t.id) AS jumlah_tugas,
SUM(t.status = 'selesai') AS jumlah_selesai
FROM proyek pr
LEFT JOIN tugas t ON t.proyek_id = pr.id
GROUP BY pr.id, pr.nama
ORDER BY pr.id;Output yang Dihasilkan:
+-----------------------+--------------+----------------+
| proyek | jumlah_tugas | jumlah_selesai |
+-----------------------+--------------+----------------+
| Rilis Aplikasi Mobile | 3 | 1 |
| Migrasi Database | 3 | 1 |
+-----------------------+--------------+----------------+
2 rows in set (0.00 sec)Ekspresi SUM(t.status = 'selesai') memanfaatkan perbandingan yang bernilai 1 saat cocok dan 0 saat tidak — menjumlahkannya sama dengan menghitung baris yang selesai, tanpa WHERE tambahan. Query ketiga membuktikan relasi banyak-ke-banyak: tugas beserta seluruh labelnya, dirangkai lewat tugas_label:
SELECT t.judul, l.nama AS label
FROM tugas t
JOIN tugas_label tl ON tl.tugas_id = t.id
JOIN label l ON l.id = tl.label_id
ORDER BY t.id, l.id;Output yang Dihasilkan:
+-------------------------+-------------+
| judul | label |
+-------------------------+-------------+
| Desain halaman login | desain |
| Implementasi notifikasi | server |
| Uji beta internal | uji |
| Backup database lama | server |
| Backup database lama | uji |
| Uji replikasi | server |
| Uji replikasi | uji |
| Pembaruan dokumentasi | dokumentasi |
+-------------------------+-------------+
8 rows in set (0.00 sec)Tugas “Backup database lama” dan “Uji replikasi” masing-masing tampil dua baris — satu per labelnya — persis perilaku junction table yang kita bangun di chapter sebelumnya. Rekap penggunaan label melengkapi verifikasi relasi M:N dari arah sebaliknya:
SELECT l.nama AS label, COUNT(tl.tugas_id) AS jumlah_tugas
FROM label l
JOIN tugas_label tl ON tl.label_id = l.id
GROUP BY l.id, l.nama
ORDER BY l.id;Output yang Dihasilkan:
+-------------+--------------+
| label | jumlah_tugas |
+-------------+--------------+
| desain | 1 |
| server | 3 |
| uji | 3 |
| dokumentasi | 1 |
+-------------+--------------+
4 rows in set (0.00 sec)Empat baris label yang memang terpakai tampil dengan hitungan masing-masing. Label prioritas sengaja tidak muncul karena query recap memakai JOIN (inner) — laporan penggunaan hanya menarik label yang punya riwayat; daftar seluruh label tinggal mengganti JOIN menjadi LEFT JOIN, dan baris prioritas akan tampil dengan hitungan 0.
Checklist Perancangan Skema #
Alur kerja pada studi kasus ini bisa dipadatkan menjadi checklist yang dipakai ulang untuk aplikasi apa pun:
- Tulis kebutuhan dalam kalimat pengguna, lalu tandai kata benda penting.
- Klasifikasikan: entitas, atribut milik entitas, atau konteks yang diabaikan.
- Tentukan tipe data tiap atribut mengikuti sifat nilainya.
- Uji tiap pasangan entitas: satu-ke-satu (kolom pelengkap terpisah), satu-ke-banyak (FK di tabel anak), atau banyak-ke-banyak (junction table).
- Tulis
CREATE TABLElengkap: primary key, foreign key bernama,UNIQUE,NOT NULL,DEFAULT— dan tulis tabel induk sebelum tabel anak. - Isi data contoh yang memuat kasus tepi yang diizinkan skema.
- Verifikasi dengan query asli aplikasi: join lintas tabel, agregat, dan relasi banyak-ke-banyak.
- Review ulang desain sebelum data nyata masuk — perubahan struktur paling murah dilakukan saat tabel masih kosong.
Review Ulang Desain #
Review terakhir memeriksa keputusan yang selama ini kita ambil diam-diam. Kolom status berupa VARCHAR(20) bebas menerima teks apa pun — typo seperti 'berjalann' akan tersimpan tanpa protes; opsi pengetatannya adalah CHECK constraint atau tipe ENUM, keduanya sudah kita kenal di section table-design. Kolom tenggat yang boleh NULL sudah teruji di data contoh, dan keputusan tanpa ON DELETE CASCADE berarti menghapus proyek harus menunggu tugasnya dibersihkan lebih dulu — pilihan konservatif yang aman untuk data kerja. Penamaan konsisten (_id untuk penunjuk, tabel tunggal untuk entitas) membuat skema terbaca seperti satu narasi. Terakhir, review performa: filter WHERE t.proyek_id = ? dan tl.label_id = ? sudah terlayani index otomatis milik foreign key, sementara filter t.status dan t.tenggat belum — itulah pekerjaan section berikutnya.
Membersihkan Database Latihan #
DROP DATABASE belajar_rdb;Output yang Dihasilkan:
Query OK, 5 rows affected (0.00 sec)Referensi #
- CREATE TABLE Statement — MySQL 8.4 Reference Manual
- FOREIGN KEY Constraints — MySQL 8.4 Reference Manual
- JOIN Clause — MySQL 8.4 Reference Manual
Dari paragraf kebutuhan sampai query verifikasi, skema aplikasi manajemen tugas berdiri utuh: empat entitas, empat relasi dengan penempatan kunci yang tepat, constraint yang menegakkan aturan bisnis, dan data contoh yang menguji seluruh kasus tepi. Perangkat desain section ini — entitas, relasi, junction table, normalisasi, dan denormalisasi terukur — kini menjadi proses yang bisa diulang untuk database berikutnya. Desain yang benar adalah fondasi; kecepatan datang dari lapisan berikutnya. Section indexes melanjutkan persis di titik review tadi: kolom yang sering difilter dan belum terlayani index.