Setiap database yang baik lahir dari daftar pertanyaan, bukan dari daftar tabel. Pada bagian ini kita memulai proyek keuangan pribadi dengan menanyakan apa yang ingin diketahui dari data — berapa saldo sekarang, kategori mana yang boros, apakah anggaran bulan ini aman — lalu menurunkan jawabannya menjadi entitas, relasi, dan akhirnya skema keuangan_db yang siap dipakai empat bagian ke depan. Proses ini meniru pekerjaan seorang database designer sungguhan: kebutuhan bisnis dulu, CREATE TABLE belakangan.
Menganalisis Kebutuhan Pencatat Keuangan #
Kita mulai dari perilaku aplikasi yang diinginkan. Seorang pengguna pencatat keuangan ingin melakukan lima hal berikut:
- mencatat setiap pemasukan dan pengeluaran beserta tanggal, jumlah, dan keterangan singkat;
- mengelompokkan transaksi ke kategori seperti
Makan,Transportasi, atauGaji; - menyimpan uang di beberapa tempat — dompet tunai, rekening bank, dompet digital;
- menetapkan batas belanja bulanan per kategori dan tahu kategori mana yang melampauinya;
- melihat ringkasan bulanan dan saldo terkini tanpa menghitung manual.
Dari daftar tersebut kita bisa mengenali kata benda yang berulang: transaksi, kategori, tempat uang (kita sebut akun), dan batas bulanan (kita sebut anggaran). Keempat kata benda itulah kandidat entitas kita. Perhatikan juga kata kerja yang menghubungkan mereka: transaksi berada di satu akun dan berlabel satu kategori, sedangkan anggaran membatasi satu kategori pada satu bulan tertentu.
Entitas dan Relasi #
Hubungan antar-entitas mengikuti pola yang sudah kita kenal dari section Relational Database Design: satu transaksi hanya milik satu kategori dan satu akun, tetapi satu kategori dipakai banyak transaksi — relasi satu-ke-banyak. Anggaran juga mengacu ke satu kategori, dan satu kategori bisa punya anggaran berbeda tiap bulannya.
| Entitas | Atribut kunci | Relasi |
|---|---|---|
akun | nama, jenis | 1 ke banyak dengan transaksi |
kategori | nama, tipe masuk/keluar | 1 ke banyak dengan transaksi dan anggaran |
transaksi | tanggal, tipe, jumlah, keterangan | mengacu kategori dan akun |
anggaran | bulan, batas | mengacu kategori, unik per kategori-bulan |
Diagram berikut merangkum keseluruhan struktur:
Dua keputusan desain di sini layak ditegaskan. Pertama, tipe transaksi (masuk/keluar) tetap disimpan di tabel transaksi meskipun kategori juga punya tipe — duplikasi ini membuat query arus kas jauh lebih ringkas karena tidak harus join dulu ke kategori. Kedua, kolom bulan pada anggaran memakai tipe DATE yang selalu diisi tanggal pertama bulan (misalnya 2026-02-01) sehingga tetap bisa dibandingkan dan diformat dengan fungsi tanggal.
Membangun Database dan Tabelnya #
Skema lengkap berikut adalah isi berkas database/skema.sql — kita jalankan langsung agar setiap barisnya teruji. Blok ini juga menjadi blok pembuka setiap bagian berikutnya karena sifatnya yang membangun ulang keuangan_db dari nol:
DROP DATABASE IF EXISTS keuangan_db;
CREATE DATABASE keuangan_db;
USE keuangan_db;
CREATE TABLE akun (
id INT AUTO_INCREMENT PRIMARY KEY,
nama VARCHAR(40) NOT NULL UNIQUE,
jenis ENUM('tunai', 'rekening', 'dompet digital') NOT NULL
);
CREATE TABLE kategori (
id INT AUTO_INCREMENT PRIMARY KEY,
nama VARCHAR(40) NOT NULL UNIQUE,
tipe ENUM('masuk', 'keluar') NOT NULL
);
CREATE TABLE transaksi (
id INT AUTO_INCREMENT PRIMARY KEY,
tanggal DATE NOT NULL,
tipe ENUM('masuk', 'keluar') NOT NULL,
kategori_id INT NOT NULL,
akun_id INT NOT NULL,
jumlah DECIMAL(12, 2) NOT NULL,
keterangan VARCHAR(100),
CONSTRAINT fk_transaksi_kategori
FOREIGN KEY (kategori_id) REFERENCES kategori (id),
CONSTRAINT fk_transaksi_akun
FOREIGN KEY (akun_id) REFERENCES akun (id),
CONSTRAINT chk_transaksi_jumlah CHECK (jumlah > 0)
);
CREATE TABLE anggaran (
id INT AUTO_INCREMENT PRIMARY KEY,
kategori_id INT NOT NULL,
bulan DATE NOT NULL,
batas DECIMAL(12, 2) NOT NULL,
CONSTRAINT fk_anggaran_kategori
FOREIGN KEY (kategori_id) REFERENCES kategori (id),
CONSTRAINT chk_anggaran_batas CHECK (batas > 0),
CONSTRAINT uq_anggaran_kategori_bulan UNIQUE (kategori_id, bulan)
);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)Mari kita bedah bagian-bagian pentingnya:
DROP DATABASE IF EXISTSmembuat skema idempoten — berkas ini bisa dijalankan berulang kali dan selalu berakhir pada kondisi bersih yang sama, baik saat pertama dipasang maupun saat dijalankan ulang untuk mengembalikan data contoh.ENUM('masuk', 'keluar')membatasi nilai kolomtipepada dua pilihan sah. Nilai di luar daftar ditolak oleh server, bukan disimpan diam-diam.DECIMAL(12, 2)dipilih untukjumlahdanbataskarena uang menuntut tipe presisi tetap.FLOATatauDOUBLEboleh lebih cepat, tetapi pembulatannya bisa membuat total laporan selisih beberapa rupiah dari penjumlahan manual.- Foreign key bernama (
fk_transaksi_kategori,fk_transaksi_akun,fk_anggaran_kategori) membuat pesan galat mudah dibaca: nama constraint langsung menunjuk tabel dan kolom bermasalah. CHECK (jumlah > 0)menjaga nilai transaksi selalu positif — arus uang ditentukan kolomtipe, bukan tanda negatif padajumlah.UNIQUE (kategori_id, bulan)padaanggaranmencegah dua batas berbeda untuk kategori dan bulan yang sama, kondisi yang akan membuat laporan anggaran ambigu.
Verifikasi Struktur #
Setelah skema dibuat, kita pastikan keempat tabel hadir dengan struktur yang diharapkan. SHOW TABLES mendaftar tabel dalam database aktif, sedangkan DESC menampilkan kolom beserta tipe dan kuncinya:
SHOW TABLES;Output yang Dihasilkan:
+-----------------------+
| Tables_in_keuangan_db |
+-----------------------+
| akun |
| anggaran |
| kategori |
| transaksi |
+-----------------------+
4 rows in set (0.00 sec)DESC transaksi;Output yang Dihasilkan:
+-------------+------------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------------+------------------------+------+-----+---------+----------------+
| id | int | NO | PRI | NULL | auto_increment |
| tanggal | date | NO | | NULL | |
| tipe | enum('masuk','keluar') | NO | | NULL | |
| kategori_id | int | NO | MUL | NULL | |
| akun_id | int | NO | MUL | NULL | |
| jumlah | decimal(12,2) | NO | | NULL | |
| keterangan | varchar(100) | YES | | NULL | |
+-------------+------------------------+------+-----+---------+----------------+
7 rows in set (0.00 sec)DESC anggaran;Output yang Dihasilkan:
+-------------+---------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------------+---------------+------+-----+---------+----------------+
| id | int | NO | PRI | NULL | auto_increment |
| kategori_id | int | NO | MUL | NULL | |
| bulan | date | NO | | NULL | |
| batas | decimal(12,2) | NO | | NULL | |
+-------------+---------------+------+-----+---------+----------------+
4 rows in set (0.00 sec)Perhatikan baris kategori_id dan akun_id pada DESC transaksi: kolom Key bernilai MUL — penanda bahwa kolom tersebut menjadi kolom pertama sebuah index non-unik. Index ini dibuat otomatis oleh InnoDB untuk setiap foreign key agar pemeriksaan referensi tetap cepat, jadi kita tidak perlu membuat index yang sama dua kali.
Mengisi Data Awal #
Tabel kosong tidak bisa diverifikasi lebih jauh, jadi kita muat data contoh — isi berkas database/data-awal.sql. Tiga akun, sembilan kategori, 28 transaksi sepanjang Januari sampai Maret 2026, dan 12 baris anggaran untuk Januari-Februari:
INSERT INTO akun (id, nama, jenis) VALUES
(1, 'Dompet Tunai', 'tunai'),
(2, 'Rekening Bank Sejahtera', 'rekening'),
(3, 'Dompet Digital PayKuy', 'dompet digital');
INSERT INTO kategori (id, nama, tipe) VALUES
(1, 'Gaji', 'masuk'),
(2, 'Bonus', 'masuk'),
(3, 'Makan', 'keluar'),
(4, 'Transportasi', 'keluar'),
(5, 'Belanja', 'keluar'),
(6, 'Hiburan', 'keluar'),
(7, 'Tagihan', 'keluar'),
(8, 'Kesehatan', 'keluar'),
(9, 'Pendidikan', 'keluar');
INSERT INTO transaksi (id, tanggal, tipe, kategori_id, akun_id, jumlah, keterangan) VALUES
(1, '2026-01-02', 'masuk', 1, 2, 8500000.00, 'Gaji bulan Desember'),
(2, '2026-01-03', 'keluar', 7, 2, 1200000.00, 'Sewa kos bulan Januari'),
(3, '2026-01-05', 'keluar', 3, 1, 58000.00, 'Makan siang di warung'),
(4, '2026-01-08', 'keluar', 3, 1, 62000.00, 'Belanja bahan makanan'),
(5, '2026-01-10', 'keluar', 4, 1, 18000.00, 'Ojek ke kantor'),
(6, '2026-01-12', 'keluar', 5, 1, 350000.00, 'Seragam kerja'),
(7, '2026-01-15', 'keluar', 3, 1, 95000.00, 'Makan malam keluarga'),
(8, '2026-01-18', 'keluar', 6, 1, 120000.00, 'Tiket bioskop'),
(9, '2026-01-22', 'keluar', 4, 1, 22000.00, 'Bus pulang kerja'),
(10, '2026-01-25', 'keluar', 3, 1, 55000.00, 'Jajan kopi dan kue'),
(11, '2026-01-28', 'keluar', 4, 3, 65000.00, 'Isi saldo transportasi online'),
(12, '2026-02-02', 'masuk', 1, 2, 8500000.00, 'Gaji bulan Januari'),
(13, '2026-02-03', 'keluar', 7, 2, 1200000.00, 'Sewa kos bulan Februari'),
(14, '2026-02-05', 'keluar', 3, 1, 75000.00, 'Makan siang rapat tim'),
(15, '2026-02-07', 'keluar', 5, 1, 420000.00, 'Belanja pakaian diskon'),
(16, '2026-02-09', 'keluar', 3, 1, 88000.00, 'Makan malam ulang tahun teman'),
(17, '2026-02-11', 'keluar', 4, 1, 20000.00, 'Ojek ke klien'),
(18, '2026-02-13', 'keluar', 6, 1, 145000.00, 'Konser musik indie'),
(19, '2026-02-15', 'masuk', 2, 2, 1500000.00, 'Bonus penyelesaian proyek'),
(20, '2026-02-17', 'keluar', 3, 1, 68000.00, 'Makan siang di kafe'),
(21, '2026-02-19', 'keluar', 6, 1, 60000.00, 'Langganan streaming bulanan'),
(22, '2026-02-21', 'keluar', 8, 1, 180000.00, 'Konsultasi dokter gigi'),
(23, '2026-02-26', 'keluar', 4, 1, 35000.00, 'Bensin motor'),
(24, '2026-02-28', 'keluar', 6, 1, 95000.00, 'Nonton bola di stadion'),
(25, '2026-03-01', 'masuk', 1, 2, 8500000.00, 'Gaji bulan Februari'),
(26, '2026-03-02', 'keluar', 7, 2, 1200000.00, 'Sewa kos bulan Maret'),
(27, '2026-03-04', 'keluar', 3, 1, 66000.00, 'Makan siang'),
(28, '2026-03-06', 'keluar', 4, 1, 24000.00, 'Ojek ke kantor');
INSERT INTO anggaran (id, kategori_id, bulan, batas) VALUES
(1, 3, '2026-01-01', 300000.00),
(2, 4, '2026-01-01', 150000.00),
(3, 5, '2026-01-01', 500000.00),
(4, 6, '2026-01-01', 250000.00),
(5, 7, '2026-01-01', 1200000.00),
(6, 8, '2026-01-01', 200000.00),
(7, 3, '2026-02-01', 300000.00),
(8, 4, '2026-02-01', 150000.00),
(9, 5, '2026-02-01', 500000.00),
(10, 6, '2026-02-01', 250000.00),
(11, 7, '2026-02-01', 1200000.00),
(12, 8, '2026-02-01', 200000.00);Output yang Dihasilkan:
Query OK, 3 rows affected (0.00 sec)
Records: 3 Duplicates: 0 Warnings: 0
Query OK, 9 rows affected (0.00 sec)
Records: 9 Duplicates: 0 Warnings: 0
Query OK, 28 rows affected (0.00 sec)
Records: 28 Duplicates: 0 Warnings: 0
Query OK, 12 rows affected (0.00 sec)
Records: 12 Duplicates: 0 Warnings: 0Semua tanggal ditulis sebagai literal tetap ('2026-01-02') — kebiasaan yang kita pegang di seluruh proyek agar setiap output bisa direproduksi persis. Nama-nama seperti Rekening Bank Sejahtera dan PayKuy sengaja fiktif. Sekarang kita buktikan foreign key benar-benar menghubungkan tabel lewat satu query join:
SELECT t.id, t.tanggal, t.tipe, k.nama AS kategori, a.nama AS akun, t.jumlah
FROM transaksi t
JOIN kategori k ON k.id = t.kategori_id
JOIN akun a ON a.id = t.akun_id
ORDER BY t.id
LIMIT 5;Output yang Dihasilkan:
+----+------------+--------+--------------+-------------------------+------------+
| id | tanggal | tipe | kategori | akun | jumlah |
+----+------------+--------+--------------+-------------------------+------------+
| 1 | 2026-01-02 | masuk | Gaji | Rekening Bank Sejahtera | 8500000.00 |
| 2 | 2026-01-03 | keluar | Tagihan | Rekening Bank Sejahtera | 1200000.00 |
| 3 | 2026-01-05 | keluar | Makan | Dompet Tunai | 58000.00 |
| 4 | 2026-01-08 | keluar | Makan | Dompet Tunai | 62000.00 |
| 5 | 2026-01-10 | keluar | Transportasi | Dompet Tunai | 18000.00 |
+----+------------+--------+--------------+-------------------------+------------+
5 rows in set (0.00 sec)Kolom kategori dan akun pada hasil berasal dari tabel induk, bukan dari transaksi — bukti bahwa relasi bekerja: id angka yang tidak bermakna bagi pembaca diterjemahkan menjadi nama yang terbaca.
Membersihkan Database Latihan #
Bagian ini selesai dan keuangan_db boleh kita bongkar agar bagian berikutnya mulai dari kondisi bersih:
DROP DATABASE IF EXISTS keuangan_db;Output yang Dihasilkan:
Query OK, 4 rows affected (0.00 sec)Perintah DROP DATABASE menghapus database beserta seluruh tabel di dalamnya tanpa konfirmasi. Karena tiap bagian proyek ini membuka bloknya dengan pembangunan ulang dari nol, kebiasaan membersihkan di akhir tidak pernah membuat kita kehilangan apa pun.
Referensi #
- CREATE TABLE Statement — MySQL 8.4 Reference Manual
- FOREIGN KEY Constraints — MySQL 8.4 Reference Manual
- CHECK Constraints — MySQL 8.4 Reference Manual
Skema sudah berdiri dan datanya terbukti terhubung. Bagian 2 melanjutkan ke denyut harian aplikasi: bagaimana transaksi baru dicatat, diperbaiki, dan dihapus dengan aman — termasuk apa yang terjadi ketika constraint menolak data yang salah.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
database/data-awal.sql
Unduh berkas-- =============================================================
-- Nama berkas : data-awal.sql
-- Proyek : Database Keuangan Pribadi (Course MySQL)
-- Tujuan : Mengisi keuangan_db dengan data contoh deterministik
-- (3 akun, 9 kategori, 28 transaksi Januari-Maret 2026,
-- 12 baris anggaran Januari-Februari 2026).
-- Cara pakai : mysql -u root -p keuangan_db < database/data-awal.sql
-- (jalankan setelah database/skema.sql)
-- =============================================================
USE keuangan_db;
INSERT INTO akun (id, nama, jenis) VALUES
(1, 'Dompet Tunai', 'tunai'),
(2, 'Rekening Bank Sejahtera', 'rekening'),
(3, 'Dompet Digital PayKuy', 'dompet digital');
INSERT INTO kategori (id, nama, tipe) VALUES
(1, 'Gaji', 'masuk'),
(2, 'Bonus', 'masuk'),
(3, 'Makan', 'keluar'),
(4, 'Transportasi', 'keluar'),
(5, 'Belanja', 'keluar'),
(6, 'Hiburan', 'keluar'),
(7, 'Tagihan', 'keluar'),
(8, 'Kesehatan', 'keluar'),
(9, 'Pendidikan', 'keluar');
INSERT INTO transaksi (id, tanggal, tipe, kategori_id, akun_id, jumlah, keterangan) VALUES
(1, '2026-01-02', 'masuk', 1, 2, 8500000.00, 'Gaji bulan Desember'),
(2, '2026-01-03', 'keluar', 7, 2, 1200000.00, 'Sewa kos bulan Januari'),
(3, '2026-01-05', 'keluar', 3, 1, 58000.00, 'Makan siang di warung'),
(4, '2026-01-08', 'keluar', 3, 1, 62000.00, 'Belanja bahan makanan'),
(5, '2026-01-10', 'keluar', 4, 1, 18000.00, 'Ojek ke kantor'),
(6, '2026-01-12', 'keluar', 5, 1, 350000.00, 'Seragam kerja'),
(7, '2026-01-15', 'keluar', 3, 1, 95000.00, 'Makan malam keluarga'),
(8, '2026-01-18', 'keluar', 6, 1, 120000.00, 'Tiket bioskop'),
(9, '2026-01-22', 'keluar', 4, 1, 22000.00, 'Bus pulang kerja'),
(10, '2026-01-25', 'keluar', 3, 1, 55000.00, 'Jajan kopi dan kue'),
(11, '2026-01-28', 'keluar', 4, 3, 65000.00, 'Isi saldo transportasi online'),
(12, '2026-02-02', 'masuk', 1, 2, 8500000.00, 'Gaji bulan Januari'),
(13, '2026-02-03', 'keluar', 7, 2, 1200000.00, 'Sewa kos bulan Februari'),
(14, '2026-02-05', 'keluar', 3, 1, 75000.00, 'Makan siang rapat tim'),
(15, '2026-02-07', 'keluar', 5, 1, 420000.00, 'Belanja pakaian diskon'),
(16, '2026-02-09', 'keluar', 3, 1, 88000.00, 'Makan malam ulang tahun teman'),
(17, '2026-02-11', 'keluar', 4, 1, 20000.00, 'Ojek ke klien'),
(18, '2026-02-13', 'keluar', 6, 1, 145000.00, 'Konser musik indie'),
(19, '2026-02-15', 'masuk', 2, 2, 1500000.00, 'Bonus penyelesaian proyek'),
(20, '2026-02-17', 'keluar', 3, 1, 68000.00, 'Makan siang di kafe'),
(21, '2026-02-19', 'keluar', 6, 1, 60000.00, 'Langganan streaming bulanan'),
(22, '2026-02-21', 'keluar', 8, 1, 180000.00, 'Konsultasi dokter gigi'),
(23, '2026-02-26', 'keluar', 4, 1, 35000.00, 'Bensin motor'),
(24, '2026-02-28', 'keluar', 6, 1, 95000.00, 'Nonton bola di stadion'),
(25, '2026-03-01', 'masuk', 1, 2, 8500000.00, 'Gaji bulan Februari'),
(26, '2026-03-02', 'keluar', 7, 2, 1200000.00, 'Sewa kos bulan Maret'),
(27, '2026-03-04', 'keluar', 3, 1, 66000.00, 'Makan siang'),
(28, '2026-03-06', 'keluar', 4, 1, 24000.00, 'Ojek ke kantor');
INSERT INTO anggaran (id, kategori_id, bulan, batas) VALUES
(1, 3, '2026-01-01', 300000.00),
(2, 4, '2026-01-01', 150000.00),
(3, 5, '2026-01-01', 500000.00),
(4, 6, '2026-01-01', 250000.00),
(5, 7, '2026-01-01', 1200000.00),
(6, 8, '2026-01-01', 200000.00),
(7, 3, '2026-02-01', 300000.00),
(8, 4, '2026-02-01', 150000.00),
(9, 5, '2026-02-01', 500000.00),
(10, 6, '2026-02-01', 250000.00),
(11, 7, '2026-02-01', 1200000.00),
(12, 8, '2026-02-01', 200000.00);
database/skema.sql
Unduh berkas-- =============================================================
-- Nama berkas : skema.sql
-- Proyek : Database Keuangan Pribadi (Course MySQL)
-- Tujuan : Membangun ulang database keuangan_db beserta empat
-- tabel (akun, kategori, transaksi, anggaran) lengkap
-- dengan foreign key, constraint CHECK, dan UNIQUE.
-- Cara pakai : mysql -u root -p < database/skema.sql
-- (lanjutkan dengan database/data-awal.sql)
-- =============================================================
DROP DATABASE IF EXISTS keuangan_db;
CREATE DATABASE keuangan_db;
USE keuangan_db;
CREATE TABLE akun (
id INT AUTO_INCREMENT PRIMARY KEY,
nama VARCHAR(40) NOT NULL UNIQUE,
jenis ENUM('tunai', 'rekening', 'dompet digital') NOT NULL
);
CREATE TABLE kategori (
id INT AUTO_INCREMENT PRIMARY KEY,
nama VARCHAR(40) NOT NULL UNIQUE,
tipe ENUM('masuk', 'keluar') NOT NULL
);
CREATE TABLE transaksi (
id INT AUTO_INCREMENT PRIMARY KEY,
tanggal DATE NOT NULL,
tipe ENUM('masuk', 'keluar') NOT NULL,
kategori_id INT NOT NULL,
akun_id INT NOT NULL,
jumlah DECIMAL(12, 2) NOT NULL,
keterangan VARCHAR(100),
CONSTRAINT fk_transaksi_kategori
FOREIGN KEY (kategori_id) REFERENCES kategori (id),
CONSTRAINT fk_transaksi_akun
FOREIGN KEY (akun_id) REFERENCES akun (id),
CONSTRAINT chk_transaksi_jumlah CHECK (jumlah > 0)
);
CREATE TABLE anggaran (
id INT AUTO_INCREMENT PRIMARY KEY,
kategori_id INT NOT NULL,
bulan DATE NOT NULL,
batas DECIMAL(12, 2) NOT NULL,
CONSTRAINT fk_anggaran_kategori
FOREIGN KEY (kategori_id) REFERENCES kategori (id),
CONSTRAINT chk_anggaran_batas CHECK (batas > 0),
CONSTRAINT uq_anggaran_kategori_bulan UNIQUE (kategori_id, bulan)
);
queries/arus-kas-bulanan.sql
Unduh berkas-- =============================================================
-- Nama berkas : arus-kas-bulanan.sql
-- Proyek : Database Keuangan Pribadi (Course MySQL)
-- Tujuan : Merangkum arus kas per bulan — total masuk, total
-- keluar, dan selisih bersihnya.
-- Cara pakai : mysql -u root -p keuangan_db < queries/arus-kas-bulanan.sql
-- =============================================================
USE keuangan_db;
SELECT
DATE_FORMAT(tanggal, '%Y-%m') AS bulan,
SUM(CASE WHEN tipe = 'masuk' THEN jumlah ELSE 0 END) AS total_masuk,
SUM(CASE WHEN tipe = 'keluar' THEN jumlah ELSE 0 END) AS total_keluar,
SUM(CASE WHEN tipe = 'masuk' THEN jumlah ELSE -jumlah END) AS arus_kas_bersih
FROM transaksi
GROUP BY bulan
ORDER BY bulan;
queries/cek-anggaran.sql
Unduh berkas-- =============================================================
-- Nama berkas : cek-anggaran.sql
-- Proyek : Database Keuangan Pribadi (Course MySQL)
-- Tujuan : Menampilkan kategori keluar yang pemakaiannya sudah
-- melampaui batas anggaran pada bulan anggaran itu.
-- Cara pakai : mysql -u root -p keuangan_db < queries/cek-anggaran.sql
-- =============================================================
USE keuangan_db;
SELECT
DATE_FORMAT(a.bulan, '%Y-%m') AS periode,
k.nama AS kategori,
a.batas,
SUM(t.jumlah) AS terpakai,
SUM(t.jumlah) - a.batas AS kelebihan
FROM anggaran a
JOIN kategori k ON k.id = a.kategori_id
JOIN transaksi t
ON t.kategori_id = a.kategori_id
AND t.tipe = 'keluar'
AND DATE_FORMAT(t.tanggal, '%Y-%m') = DATE_FORMAT(a.bulan, '%Y-%m')
GROUP BY a.id, periode, k.nama, a.batas
HAVING SUM(t.jumlah) > a.batas
ORDER BY periode, k.nama;
queries/pengeluaran-per-kategori.sql
Unduh berkas-- =============================================================
-- Nama berkas : pengeluaran-per-kategori.sql
-- Proyek : Database Keuangan Pribadi (Course MySQL)
-- Tujuan : Merangkum pengeluaran per kategori keluar beserta
-- jumlah transaksi dan persentase terhadap total.
-- Cara pakai : mysql -u root -p keuangan_db < queries/pengeluaran-per-kategori.sql
-- =============================================================
USE keuangan_db;
SELECT
k.nama AS kategori,
COUNT(t.id) AS jumlah_transaksi,
COALESCE(SUM(t.jumlah), 0) AS total_pengeluaran,
ROUND(COALESCE(SUM(t.jumlah), 0)
/ (SELECT SUM(jumlah) FROM transaksi WHERE tipe = 'keluar') * 100, 1) AS persen
FROM kategori k
LEFT JOIN transaksi t
ON t.kategori_id = k.id AND t.tipe = 'keluar'
WHERE k.tipe = 'keluar'
GROUP BY k.id, k.nama
ORDER BY total_pengeluaran DESC, k.nama;
queries/saldo-kumulatif.sql
Unduh berkas-- =============================================================
-- Nama berkas : saldo-kumulatif.sql
-- Proyek : Database Keuangan Pribadi (Course MySQL)
-- Tujuan : Menghitung saldo berjalan dari seluruh transaksi —
-- pemasukan menambah, pengeluaran mengurangi.
-- Cara pakai : mysql -u root -p keuangan_db < queries/saldo-kumulatif.sql
-- =============================================================
USE keuangan_db;
SELECT
id,
tanggal,
keterangan,
jumlah,
SUM(CASE WHEN tipe = 'masuk' THEN jumlah ELSE -jumlah END)
OVER (ORDER BY tanggal, id ROWS UNBOUNDED PRECEDING) AS saldo_kumulatif
FROM transaksi
ORDER BY tanggal, id;
tests/pemeriksaan.sql
Unduh berkas-- =============================================================
-- Nama berkas : pemeriksaan.sql
-- Proyek : Database Keuangan Pribadi (Course MySQL)
-- Tujuan : Uji akhir otomatis — memeriksa struktur, data awal,
-- deteksi anggaran terlampaui, arus kas bulanan, dan
-- konsistensi saldo kumulatif. Ekspektasi tiap query
-- dituliskan sebagai komentar di atasnya.
-- Cara pakai : mysql -h 127.0.0.1 -P 3306 -u root < tests/pemeriksaan.sql
-- (jalankan setelah database/skema.sql dan data-awal.sql)
-- =============================================================
USE keuangan_db;
-- Pemeriksaan 1: struktur database.
-- Ekspektasi: jumlah_tabel = 4 (akun, kategori, transaksi, anggaran).
SELECT COUNT(*) AS jumlah_tabel
FROM information_schema.tables
WHERE table_schema = 'keuangan_db';
-- Pemeriksaan 2: data awal termuat lengkap.
-- Ekspektasi: akun = 3, kategori = 9, transaksi = 28, anggaran = 12.
SELECT
(SELECT COUNT(*) FROM akun) AS akun,
(SELECT COUNT(*) FROM kategori) AS kategori,
(SELECT COUNT(*) FROM transaksi) AS transaksi,
(SELECT COUNT(*) FROM anggaran) AS anggaran;
-- Pemeriksaan 3: deteksi anggaran terlampaui pada data awal.
-- Ekspektasi: 1 baris — periode 2026-02, kategori Hiburan,
-- batas 250000.00, terpakai 300000.00, kelebihan 50000.00.
SELECT
DATE_FORMAT(a.bulan, '%Y-%m') AS periode,
k.nama AS kategori,
a.batas,
SUM(t.jumlah) AS terpakai,
SUM(t.jumlah) - a.batas AS kelebihan
FROM anggaran a
JOIN kategori k ON k.id = a.kategori_id
JOIN transaksi t
ON t.kategori_id = a.kategori_id
AND t.tipe = 'keluar'
AND DATE_FORMAT(t.tanggal, '%Y-%m') = DATE_FORMAT(a.bulan, '%Y-%m')
GROUP BY a.id, periode, k.nama, a.batas
HAVING SUM(t.jumlah) > a.batas
ORDER BY periode, k.nama;
-- Pemeriksaan 4: arus kas bulanan lengkap tiga bulan.
-- Ekspektasi: 3 baris — 2026-01, 2026-02, 2026-03.
SELECT
DATE_FORMAT(tanggal, '%Y-%m') AS bulan,
SUM(CASE WHEN tipe = 'masuk' THEN jumlah ELSE 0 END) AS total_masuk,
SUM(CASE WHEN tipe = 'keluar' THEN jumlah ELSE 0 END) AS total_keluar,
SUM(CASE WHEN tipe = 'masuk' THEN jumlah ELSE -jumlah END) AS arus_kas_bersih
FROM transaksi
GROUP BY bulan
ORDER BY bulan;
-- Pemeriksaan 5: saldo akhir window function sama dengan total bersih.
-- Ekspektasi: total_bersih = saldo_terakhir = 21279000.00.
SELECT
(SELECT SUM(CASE WHEN tipe = 'masuk' THEN jumlah ELSE -jumlah END)
FROM transaksi) AS total_bersih,
(SELECT saldo FROM (
SELECT id,
SUM(CASE WHEN tipe = 'masuk' THEN jumlah ELSE -jumlah END)
OVER (ORDER BY tanggal, id ROWS UNBOUNDED PRECEDING) AS saldo
FROM transaksi
) running ORDER BY id DESC LIMIT 1) AS saldo_terakhir;
-- Pemeriksaan 6: kategori tanpa pengeluaran tetap tampil dengan nol.
-- Ekspektasi: 1 baris — kategori Pendidikan, 0 transaksi, 0.00, 0.0 persen.
SELECT
k.nama AS kategori,
COUNT(t.id) AS jumlah_transaksi,
COALESCE(SUM(t.jumlah), 0) AS total_pengeluaran,
ROUND(COALESCE(SUM(t.jumlah), 0)
/ (SELECT SUM(jumlah) FROM transaksi WHERE tipe = 'keluar') * 100, 1) AS persen
FROM kategori k
LEFT JOIN transaksi t
ON t.kategori_id = k.id AND t.tipe = 'keluar'
WHERE k.tipe = 'keluar'
GROUP BY k.id, k.nama
HAVING COUNT(t.id) = 0
ORDER BY k.nama;