Proyek Database Manajemen Tugas mengambil satu domain yang akrab bagi setiap tim kerja: papan tugas. Aplikasi semacam Trello atau Jira di belakang layar menyimpan pengguna, proyek, dan tugas yang berpindah status dari baru menjadi berjalan hingga selesai. Domain ini dipilih karena menuntut dua hal sekaligus — relasi antar-entitas yang rapi (tugas dimiliki proyek, dikerjakan pengguna) dan jejak audit yang bisa dipercaya (setiap perubahan status tercatat waktunya). Kita membangun keduanya di database tugas_db, lalu menutup proyek dengan laporan produktivitas yang lazim dibaca di rapat mingguan.

Yang membedakan proyek ini dari proyek sebelumnya adalah tekanannya pada perubahan data yang aman. Sebuah tugas yang diubah statusnya harus membawa riwayatnya — dua tulisan ke dua tabel yang harus terjadi bersama-sama atau tidak sama sekali. Inilah panggung utama transaksi database: kita akan melihat apa yang terjadi bila kedua tulisan dipisahkan, lalu membetulkannya dengan pola satu blok START TRANSACTION ... COMMIT.

Tujuan Proyek #

Saat proyek selesai, kita memiliki database yang mampu menjawab pertanyaan-pertanyaan berikut:

  • menyimpan pengguna dengan email unik, proyek beserta pemiliknya, dan tugas dengan prioritas serta tenggat;
  • menjaga validitas data lewat foreign key bernama, ENUM status dan prioritas, serta kolom UNIQUE;
  • menjalankan operasi CRUD tugas harian dengan penolakan galat yang informatif;
  • mencatat setiap perubahan status ke tabel riwayat_status secara atomik dalam satu transaksi;
  • menghasilkan laporan: matriks status-prioritas, daftar tugas terlambat, beban kerja per assignee, dan riwayat per tugas.

Prasyarat #

Proyek ini memakai materi dari beberapa section sebelumnya. Sebelum mulai, pastikan kita sudah nyaman dengan hal-hal berikut:

  • CREATE TABLE dengan foreign key dan UNIQUE dari section Relational Database Design;
  • INSERT, UPDATE, DELETE, dan LEFT JOIN dari section Dasar SQL dan Joins;
  • tipe ENUM serta DATETIME dari section Tipe Data;
  • GROUP BY, COUNT, SUM, dan CASE dari section Agregasi Data dan Fungsi SQL;
  • START TRANSACTION, COMMIT, dan ROLLBACK dari section Transactions.

Peta Bagian #

BagianFokusHasil akhir
1Perancangan skemaDatabase tugas_db dengan empat tabel ber-FK, ENUM, dan kolom nullable yang disengaja
2Pengguna dan tugasData awal tim, CRUD tugas, tiga lapis penolakan data rusak
3Status dan riwayat tugasAlur ubah status atomik: satu transaksi untuk riwayat dan tugas
4Query laporanMatriks status, tugas terlambat, beban assignee, riwayat, uji akhir otomatis

Struktur Berkas Proyek #

Berkas pendukung proyek mengikuti struktur folder berikut:

text
database-manajemen-tugas/
├── database/
│   ├── skema.sql         (DROP + CREATE DATABASE + empat CREATE TABLE)
│   └── data-awal.sql     (tim 6 orang, 3 proyek, 10 tugas, 11 riwayat)
├── queries/
│   ├── tugas-per-status.sql
│   ├── tugas-terlambat.sql
│   ├── beban-assignee.sql
│   └── riwayat-per-tugas.sql
└── tests/
    └── pemeriksaan.sql   (uji akhir dengan ekspektasi tertulis)

Berkas database/skema.sql membangun ulang database dari nol, database/data-awal.sql mengisi data contoh fiktif awal 2026, empat berkas di queries/ menyimpan laporan siap pakai, dan tests/pemeriksaan.sql memeriksa seluruh hasil akhir secara otomatis. Hubungan keempat tabelnya dirangkum diagram berikut:

Dua relasi menghubungkan pengguna ke tugas lewat jalur berbeda: pemilik_id pada proyek dan assignee_id pada tugas — keduanya menunjuk tabel yang sama, pola umum untuk “pembuat” dan “pelaksana”. Kolom assignee_id sengaja boleh NULL karena tugas baru belum tentu sudah diambil seseorang. Mari kita mulai dari Bagian 1: menurunkan kebutuhan tim menjadi empat tabel yang saling terhubung.

Berkas Sumber

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

database/data-awal.sql

Unduh berkas
sql
-- data-awal.sql — Proyek: Database Manajemen Tugas
-- Data contoh deterministik (6 pengguna, 3 proyek, 10 tugas, riwayat awal).
-- Cara pakai: mysql -u root -p tugas_db < data-awal.sql
-- (jalankan skema.sql terlebih dahulu)

USE tugas_db;

INSERT INTO pengguna (id, nama, email) VALUES
  (1, 'Budi Santoso', '[email protected]'),
  (2, 'Siti Rahma', '[email protected]'),
  (3, 'Andi Wijaya', '[email protected]'),
  (4, 'Dewi Lestari', '[email protected]'),
  (5, 'Tono Pratama', '[email protected]'),
  (6, 'Rina Kusuma', '[email protected]');

INSERT INTO proyek (id, nama, pemilik_id) VALUES
  (1, 'Rilis Aplikasi Mobile', 1),
  (2, 'Migrasi Data Gudang', 2),
  (3, 'Kampanye Pemasaran Q1', 4);

INSERT INTO tugas (id, judul, proyek_id, assignee_id, prioritas, tenggat, status) VALUES
  (1,  'Desain mockup halaman utama',  1, 3,    'tinggi', '2026-02-10', 'selesai'),
  (2,  'Implementasi API autentikasi', 1, 1,    'tinggi', '2026-02-10', 'berjalan'),
  (3,  'Uji coba beta di Play Store',  1, NULL, 'sedang', '2026-03-15', 'baru'),
  (4,  'Pemetaan tabel lama ke baru',  2, 2,    'tinggi', '2026-02-05', 'selesai'),
  (5,  'Skrip migrasi batch pertama', 2, 5,    'sedang', '2026-02-25', 'berjalan'),
  (6,  'Validasi hasil migrasi',      2, 4,    'rendah', '2026-03-01', 'baru'),
  (7,  'Menyusun materi iklan',       3, 4,    'sedang', '2026-02-12', 'selesai'),
  (8,  'Riset kata kunci iklan',      3, 3,    'rendah', '2026-02-08', 'selesai'),
  (9,  'Negosiasi anggaran media',    3, 1,    'tinggi', '2026-02-12', 'berjalan'),
  (10, 'Laporan performa kampanye',   3, NULL, 'sedang', '2026-04-10', 'baru');

INSERT INTO riwayat_status (id, tugas_id, status_lama, status_baru, waktu, catatan) VALUES
  (1,  1, 'baru',     'berjalan', '2026-01-06 10:15:00', 'Mulai kerjakan mockup'),
  (2,  1, 'berjalan', 'selesai',  '2026-02-09 16:40:00', 'Mockup disetujui tim'),
  (3,  2, 'baru',     'berjalan', '2026-01-20 11:00:00', 'Sprint 1 dimulai'),
  (4,  4, 'baru',     'berjalan', '2026-01-07 09:00:00', 'Pemetaan awal berjalan'),
  (5,  4, 'berjalan', 'selesai',  '2026-02-04 17:00:00', 'Pemetaan tervalidasi'),
  (6,  5, 'baru',     'berjalan', '2026-01-15 10:30:00', 'Skrip versi awal mulai dikerjakan'),
  (7,  7, 'baru',     'berjalan', '2026-01-12 10:00:00', 'Brief iklan diterima'),
  (8,  7, 'berjalan', 'selesai',  '2026-02-11 15:30:00', 'Materi disetujui manajemen'),
  (9,  8, 'baru',     'berjalan', '2026-01-13 09:10:00', 'Riset kata kunci dimulai'),
  (10, 8, 'berjalan', 'selesai',  '2026-02-06 12:00:00', 'Riset selesai, 40 kata kunci'),
  (11, 9, 'baru',     'berjalan', '2026-01-25 10:30:00', 'Rapat awal dengan vendor');

database/skema.sql

Unduh berkas
sql
-- skema.sql — Proyek: Database Manajemen Tugas
-- Membangun database tugas_db beserta 4 tabel berelasi (pengguna, proyek,
-- tugas, riwayat_status) lengkap dengan foreign key bernama.
-- Cara pakai: mysql -u root -p < skema.sql

DROP DATABASE IF EXISTS tugas_db;
CREATE DATABASE tugas_db;
USE tugas_db;

CREATE TABLE pengguna (
  id INT AUTO_INCREMENT PRIMARY KEY,
  nama VARCHAR(50) NOT NULL,
  email VARCHAR(60) NOT NULL UNIQUE
);

CREATE TABLE proyek (
  id INT AUTO_INCREMENT PRIMARY KEY,
  nama VARCHAR(60) NOT NULL,
  pemilik_id INT NOT NULL,
  CONSTRAINT fk_proyek_pemilik
    FOREIGN KEY (pemilik_id) REFERENCES pengguna (id)
);

CREATE TABLE tugas (
  id INT AUTO_INCREMENT PRIMARY KEY,
  judul VARCHAR(80) NOT NULL,
  proyek_id INT NOT NULL,
  assignee_id INT NULL,
  prioritas ENUM('rendah', 'sedang', 'tinggi') NOT NULL,
  tenggat DATE NULL,
  status ENUM('baru', 'berjalan', 'selesai') NOT NULL,
  CONSTRAINT fk_tugas_proyek
    FOREIGN KEY (proyek_id) REFERENCES proyek (id),
  CONSTRAINT fk_tugas_assignee
    FOREIGN KEY (assignee_id) REFERENCES pengguna (id)
);

CREATE TABLE riwayat_status (
  id INT AUTO_INCREMENT PRIMARY KEY,
  tugas_id INT NOT NULL,
  status_lama ENUM('baru', 'berjalan', 'selesai') NOT NULL,
  status_baru ENUM('baru', 'berjalan', 'selesai') NOT NULL,
  waktu DATETIME NOT NULL,
  catatan VARCHAR(200),
  CONSTRAINT fk_riwayat_tugas
    FOREIGN KEY (tugas_id) REFERENCES tugas (id)
);

queries/laporan-beban-kerja.sql

Unduh berkas
sql
-- laporan-beban-kerja.sql — Beban kerja tiap anggota (total/selesai/berjalan/baru).
-- Cara pakai: mysql -u root -p tugas_db < queries/laporan-beban-kerja.sql
USE tugas_db;

SELECT
  p.nama,
  COUNT(t.id) AS total_tugas,
  SUM(CASE WHEN t.status = 'selesai' THEN 1 ELSE 0 END) AS selesai,
  SUM(CASE WHEN t.status = 'berjalan' THEN 1 ELSE 0 END) AS berjalan,
  SUM(CASE WHEN t.status = 'baru' THEN 1 ELSE 0 END) AS baru
FROM pengguna p
LEFT JOIN tugas t ON t.assignee_id = p.id
GROUP BY p.id, p.nama
ORDER BY total_tugas DESC, p.nama;

queries/laporan-overdue.sql

Unduh berkas
sql
-- laporan-overdue.sql — Tugas menembus tenggat namun belum selesai.
-- Ganti tanggal acuan sesuai kebutuhan (di aplikasi: CURRENT_DATE()).
-- Cara pakai: mysql -u root -p tugas_db < queries/laporan-overdue.sql
USE tugas_db;

SELECT
  t.id,
  t.judul,
  pr.nama AS proyek,
  p.nama AS penanggung,
  t.tenggat,
  DATEDIFF('2026-02-15', t.tenggat) AS hari_terlambat
FROM tugas t
JOIN proyek pr ON pr.id = t.proyek_id
LEFT JOIN pengguna p ON p.id = t.assignee_id
WHERE t.status <> 'selesai'
  AND t.tenggat < '2026-02-15'
ORDER BY t.tenggat, t.id;

queries/laporan-status-proyek.sql

Unduh berkas
sql
-- laporan-status-proyek.sql — Distribusi status + persentase selesai per proyek.
-- Cara pakai: mysql -u root -p tugas_db < queries/laporan-status-proyek.sql
USE tugas_db;

SELECT
  pr.nama AS proyek,
  COUNT(t.id) AS total_tugas,
  SUM(CASE WHEN t.status = 'selesai' THEN 1 ELSE 0 END) AS selesai,
  ROUND(
    SUM(CASE WHEN t.status = 'selesai' THEN 1 ELSE 0 END) / COUNT(t.id) * 100,
    1
  ) AS persen_selesai
FROM proyek pr
LEFT JOIN tugas t ON t.proyek_id = pr.id
GROUP BY pr.id, pr.nama
ORDER BY persen_selesai DESC, pr.nama;

tests/pemeriksaan.sql

Unduh berkas
sql
-- pemeriksaan.sql — Proyek: Database Manajemen Tugas
-- Rangkaian pemeriksaan otomatis: skema, jumlah data, hasil alur Bagian 3,
-- dan angka laporan Bagian 4. Ekspektasi tertulis di komentar tiap blok.
-- Cara pakai (urutan lengkap membangun kondisi akhir proyek):
--   mysql -u root -p < database/skema.sql
--   mysql -u root -p < database/data-awal.sql        (kondisi awal Bagian 2)
--   ...jalankan alur transaksi Bagian 3...           (atau lihat Bagian 4)
--   mysql -u root -p < tests/pemeriksaan.sql
-- Pemeriksaan ini mengasumsikan kondisi AKHIR (13 baris riwayat).
USE tugas_db;

-- [1] Struktur: 4 tabel harus ada
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'tugas_db'
ORDER BY table_name;
-- Ekspektasi: pengguna, proyek, riwayat_status, tugas (4 baris)

-- [2] Jumlah data kondisi akhir: 6 pengguna, 3 proyek, 10 tugas, 13 riwayat
SELECT
  (SELECT COUNT(*) FROM pengguna) AS pengguna,
  (SELECT COUNT(*) FROM proyek) AS proyek,
  (SELECT COUNT(*) FROM tugas) AS tugas,
  (SELECT COUNT(*) FROM riwayat_status) AS riwayat;
-- Ekspektasi: 6, 3, 10, 13

-- [3] Integritas ENUM: email unik dijaga (duplikat ditolak)
-- (dibuktikan di Bagian 2 dengan ERROR 1062; di sini cukup hitung unik)
SELECT COUNT(DISTINCT email) AS email_unik FROM pengguna;
-- Ekspektasi: 6

-- [4] Alur Bagian 3 terekam: tugas 3 kini milik Rina dan berjalan
SELECT judul, assignee_id, status FROM tugas WHERE id = 3;
-- Ekspektasi: Uji coba beta di Play Store | 6 | berjalan

-- [5] Riwayat tugas 3: tepat 1 baris baru → berjalan
SELECT COUNT(*) AS perubahan_tugas3 FROM riwayat_status WHERE tugas_id = 3;
-- Ekspektasi: 1

-- [6] Durasi siklus tugas 2: selesai 19 hari setelah mulai
SELECT
  TIMESTAMPDIFF(DAY, MIN(waktu), MAX(waktu)) AS durasi_hari
FROM riwayat_status
WHERE tugas_id = 2;
-- Ekspektasi: 19

-- [7] Laporan overdue (acuan 2026-02-15): 4 tugas belum selesai menembus tenggat
SELECT COUNT(*) AS tugas_terlambat
FROM tugas
WHERE status <> 'selesai' AND tenggat < '2026-02-15';
-- Ekspektasi: 4

-- [8] Laporan status proyek: Kampanye Pemasaran Q1 paling tinggi persentase selesainya
SELECT
  pr.nama AS proyek,
  ROUND(
    SUM(CASE WHEN t.status = 'selesai' THEN 1 ELSE 0 END) / COUNT(t.id) * 100,
    1
  ) AS persen_selesai
FROM proyek pr
LEFT JOIN tugas t ON t.proyek_id = pr.id
GROUP BY pr.id, pr.nama
ORDER BY persen_selesai DESC;
-- Ekspektasi: Kampanye Pemasaran Q1 (83.3) teratas, lalu Migrasi Data Gudang, Rilis Aplikasi Mobile