Pencatatan dan anggaran menghasilkan tumpukan baris; nilai sebenarnya muncul ketika tumpukan itu menjawab pertanyaan. Bagian penutup proyek berpindah ke kursi manajer keuangan: berapa arus kas tiap bulan, bagaimana saldo bergerak dari hari ke hari, dan kategori mana yang menyerap porsi terbesar pengeluaran. Semua laporan dibangun dari query murni — tanpa mengubah satu baris data — dan tiap laporan tersimpan sebagai berkas siap pakai di queries/. Proyek ditutup dengan tests/pemeriksaan.sql yang memverifikasi seluruh hasil secara otomatis dari nol.

Membangun Ulang Kondisi Awal #

Blok pembuka standar proyek ini — skema lengkap plus data awal tiga bulan:

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)
);

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:

text
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, 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: 0

Arus Kas Bulanan #

Laporan pertama merangkum seluruh transaksi menjadi tiga baris — satu per bulan — dengan kolom masuk, keluar, dan selisih bersih. Trik utamanya memakai CASE di dalam SUM untuk memilah arah uang tanpa join tambahan:

sql
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;

Output yang Dihasilkan:

text
+---------+-------------+--------------+-----------------+
| bulan   | total_masuk | total_keluar | arus_kas_bersih |
+---------+-------------+--------------+-----------------+
| 2026-01 |  8500000.00 |   2045000.00 |      6455000.00 |
| 2026-02 | 10000000.00 |   2386000.00 |      7614000.00 |
| 2026-03 |  8500000.00 |   1290000.00 |      7210000.00 |
+---------+-------------+--------------+-----------------+
3 rows in set (0.00 sec)

Ekspresi SUM(CASE WHEN tipe = 'masuk' THEN jumlah ELSE 0 END) membaca setiap baris: bila tipe masuk, nilainya ikut dijumlahkan; bila bukan, berkontribusi nol. Kolom arus_kas_bersih memakai varian ketiga — ELSE -jumlah — sehingga pengeluaran otomatis dikurangkan. Dua bulan pertama memperlihatkan pola sehat: arus kas bersih positif karena gaji jauh di atas total belanja, meski Februari lebih boros akibat belanja pakaian, konsultasi dokter, dan tiga transaksi hiburan.

Saldo Kumulatif dengan Window Function #

Arus kas bulanan menjawab “berapa per bulan”; saldo kumulatif menjawab “berapa kesejahteraan total hari ini”. GROUP BY tidak bisa membantu karena laporan ini justru butuh semua baris tetap utuh — tugas sempurna untuk window function:

sql
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;

Output yang Dihasilkan:

text
+----+------------+-------------------------------+------------+-----------------+
| id | tanggal    | keterangan                    | jumlah     | saldo_kumulatif |
+----+------------+-------------------------------+------------+-----------------+
|  1 | 2026-01-02 | Gaji bulan Desember           | 8500000.00 |      8500000.00 |
|  2 | 2026-01-03 | Sewa kos bulan Januari        | 1200000.00 |      7300000.00 |
|  3 | 2026-01-05 | Makan siang di warung         |   58000.00 |      7242000.00 |
|  4 | 2026-01-08 | Belanja bahan makanan         |   62000.00 |      7180000.00 |
|  5 | 2026-01-10 | Ojek ke kantor                |   18000.00 |      7162000.00 |
|  6 | 2026-01-12 | Seragam kerja                 |  350000.00 |      6812000.00 |
|  7 | 2026-01-15 | Makan malam keluarga          |   95000.00 |      6717000.00 |
|  8 | 2026-01-18 | Tiket bioskop                 |  120000.00 |      6597000.00 |
|  9 | 2026-01-22 | Bus pulang kerja              |   22000.00 |      6575000.00 |
| 10 | 2026-01-25 | Jajan kopi dan kue            |   55000.00 |      6520000.00 |
| 11 | 2026-01-28 | Isi saldo transportasi online |   65000.00 |      6455000.00 |
| 12 | 2026-02-02 | Gaji bulan Januari            | 8500000.00 |     14955000.00 |
| 13 | 2026-02-03 | Sewa kos bulan Februari       | 1200000.00 |     13755000.00 |
| 14 | 2026-02-05 | Makan siang rapat tim         |   75000.00 |     13680000.00 |
| 15 | 2026-02-07 | Belanja pakaian diskon        |  420000.00 |     13260000.00 |
| 16 | 2026-02-09 | Makan malam ulang tahun teman |   88000.00 |     13172000.00 |
| 17 | 2026-02-11 | Ojek ke klien                 |   20000.00 |     13152000.00 |
| 18 | 2026-02-13 | Konser musik indie            |  145000.00 |     13007000.00 |
| 19 | 2026-02-15 | Bonus penyelesaian proyek     | 1500000.00 |     14507000.00 |
| 20 | 2026-02-17 | Makan siang di kafe           |   68000.00 |     14439000.00 |
| 21 | 2026-02-19 | Langganan streaming bulanan   |   60000.00 |     14379000.00 |
| 22 | 2026-02-21 | Konsultasi dokter gigi        |  180000.00 |     14199000.00 |
| 23 | 2026-02-26 | Bensin motor                  |   35000.00 |     14164000.00 |
| 24 | 2026-02-28 | Nonton bola di stadion        |   95000.00 |     14069000.00 |
| 25 | 2026-03-01 | Gaji bulan Februari           | 8500000.00 |     22569000.00 |
| 26 | 2026-03-02 | Sewa kos bulan Maret          | 1200000.00 |     21369000.00 |
| 27 | 2026-03-04 | Makan siang                   |   66000.00 |     21303000.00 |
| 28 | 2026-03-06 | Ojek ke kantor                |   24000.00 |     21279000.00 |
+----+------------+-------------------------------+------------+-----------------+
28 rows in set (0.00 sec)

Bedah elemennya:

  1. SUM(...) OVER (...) mengubah fungsi agregat menjadi window function: setiap baris mendapatkan hasil penjumlahan jendela baris, bukan satu ringkasan global.
  2. ORDER BY tanggal, id di dalam OVER menentukan urutan penghitungan. Kolom id berperan sebagai tiebreaker agar dua transaksi pada tanggal sama tidak saling bertukar posisi antar-eksekusi.
  3. ROWS UNBOUNDED PRECEDING membatasi frame dari baris pertama partisi hingga baris sekarang — bentuk eksplisit dari running total. Tanpa klausa frame, MySQL memakai RANGE yang menghitung per nilai sama; dengan urutan unik seperti di sini keduanya setara, tetapi menulis ROWS membuat maksud tak terbantahkan.
  4. ORDER BY tanggal, id di luar menjaga hasil tampil persis mengikuti urutan perhitungan.

Angka saldo_kumulatif pada baris terakhir adalah posisi keuangan terkini — total seluruh pemasukan dikurangi seluruh pengeluaran sejak awal data. Sebagai pemeriksaan silang, hitung saldo akhir per akun dengan agregasi biasa:

sql
SELECT
  a.nama AS akun,
  SUM(CASE WHEN t.tipe = 'masuk' THEN t.jumlah ELSE -t.jumlah END) AS saldo_akhir
FROM akun a
LEFT JOIN transaksi t ON t.akun_id = a.id
GROUP BY a.id, a.nama
ORDER BY a.nama;

Output yang Dihasilkan:

text
+-------------------------+-------------+
| akun                    | saldo_akhir |
+-------------------------+-------------+
| Dompet Digital PayKuy   |   -65000.00 |
| Dompet Tunai            | -2056000.00 |
| Rekening Bank Sejahtera | 23400000.00 |
+-------------------------+-------------+
3 rows in set (0.00 sec)

Penjumlahan saldo ketiga akun pada query ini harus sama dengan baris terakhir saldo kumulatif — dua jalan berbeda menuju satu angka, dan keduanya memang menuju 21279000.00. Angka negatif pada Dompet Tunai dan Dompet Digital bukan galat hitung, melainkan cermin data contoh kita: seluruh pemasukan mendarat di rekening, sementara belanja harian diambil dari tunai dan dompet digital. Merekam perpindahan uang antar-akun menuntut dua baris — keluar dari akun sumber, masuk ke akun tujuan — ciri pembukuan berpasangan (double-entry) yang menjadi pengembangan alami dari skema ini.

Pengeluaran per Kategori #

Laporan ketiga menjawab pertanyaan klasik “uang saya habis untuk apa?”. Semua kategori keluar ditampilkan berurutan dari yang terbesar, lengkap dengan persentase terhadap total:

sql
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;

Output yang Dihasilkan:

text
+--------------+------------------+-------------------+--------+
| kategori     | jumlah_transaksi | total_pengeluaran | persen |
+--------------+------------------+-------------------+--------+
| Tagihan      |                3 |        3600000.00 |   62.9 |
| Belanja      |                2 |         770000.00 |   13.5 |
| Makan        |                8 |         567000.00 |    9.9 |
| Hiburan      |                4 |         420000.00 |    7.3 |
| Transportasi |                6 |         184000.00 |    3.2 |
| Kesehatan    |                1 |         180000.00 |    3.1 |
| Pendidikan   |                0 |              0.00 |    0.0 |
+--------------+------------------+-------------------+--------+
7 rows in set (0.00 sec)

Dua detail layak dicermati. COUNT(t.id) — bukan COUNT(*) — hanya menghitung transaksi yang benar-benar cocok, sehingga kategori tanpa transaksi seperti Pendidikan melaporkan 0 alih-alih 1 (nilai NULL dari LEFT JOIN tidak dihitung COUNT kolom). Subquery skalar (SELECT SUM(jumlah) FROM transaksi WHERE tipe = 'keluar') dihitung sekali lalu menjadi penyebut tetap untuk seluruh baris; karena setiap persen dibulatkan ke satu desimal, totalnya bisa meleset tipis — 99,9 pada laporan ini — bukan tanda baris yang hilang, melainkan efek pembulatan yang wajar.

Ketiga laporan ini hidup sebagai berkas di bundle proyek dan bisa dijalankan ulang kapan saja:

bash
mysql -u root -p keuangan_db < queries/arus-kas-bulanan.sql
mysql -u root -p keuangan_db < queries/saldo-kumulatif.sql
mysql -u root -p keuangan_db < queries/pengeluaran-per-kategori.sql

Uji Akhir dengan Berkas Pemeriksaan #

Setiap laporan yang kita bangun sepanjang proyek kini diuji dari kondisi benar-benar bersih. Tiga perintah berikut membangun ulang database dari berkas database/skema.sql, mengisinya dengan database/data-awal.sql, lalu menjalankan tests/pemeriksaan.sql yang memeriksa struktur, data awal, anggaran terlampaui, arus kas, konsistensi saldo, dan kategori tanpa transaksi — lengkap dengan komentar ekspektasi di tiap langkah:

bash
-- @test-bash
mysql -h 127.0.0.1 -P 3306 -u root < content/mysql/proyek/database-keuangan-pribadi/database/skema.sql
mysql -h 127.0.0.1 -P 3306 -u root < content/mysql/proyek/database-keuangan-pribadi/database/data-awal.sql
mysql -h 127.0.0.1 -P 3306 -u root < content/mysql/proyek/database-keuangan-pribadi/tests/pemeriksaan.sql

Output yang Dihasilkan:

text
jumlah_tabel
4
akun	kategori	transaksi	anggaran
3	9	28	12
periode	kategori	batas	terpakai	kelebihan
2026-02	Hiburan	250000.00	300000.00	50000.00
bulan	total_masuk	total_keluar	arus_kas_bersih
2026-01	8500000.00	2045000.00	6455000.00
2026-02	10000000.00	2386000.00	7614000.00
2026-03	8500000.00	1290000.00	7210000.00
total_bersih	saldo_terakhir
21279000.00	21279000.00
kategori	jumlah_transaksi	total_pengeluaran	persen
Pendidikan	0	0.00	0.0

Enam pemeriksaan lolos semua: empat tabel hadir, jumlah baris tepat (3 akun, 9 kategori, 28 transaksi, 12 anggaran), satu-satunya pelanggaran anggaran adalah Hiburan Februari, arus kas tiga bulan utuh, saldo window function cocok dengan total bersih, dan kategori Pendidikan tampil dengan nol transaksi. Karena mode batch mysql menampilkan hasil dengan pemisah tab, angka-angka itu sama persis dengan output interaktif — hanya formatnya yang lebih ramping.

Membersihkan Database Latihan #

Proyek tuntas dan keuangan_db yang dibangun uji akhir tadi bisa kita lepaskan:

sql
DROP DATABASE IF EXISTS keuangan_db;

Output yang Dihasilkan:

text
Query OK, 4 rows affected (0.00 sec)

Ringkasan Proyek #

Empat bagian proyek Database Keuangan Pribadi menghasilkan satu aplikasi database utuh. Capaian kita:

  • skema empat tabel dengan foreign key bernama, CHECK, ENUM, dan UNIQUE komposit yang dijaga sejak perancangan;
  • pencatatan transaksi harian dengan CRUD lengkap dan tiga lapis penolakan data rusak (galat 1452, 3819, 1265);
  • anggaran bulanan per kategori yang unik serta laporan pemakaian dengan LEFT JOIN, COALESCE, dan HAVING;
  • alur catat-transaksi-plus-cek-anggaran yang atomik lewat START TRANSACTION, COMMIT, dan ROLLBACK;
  • laporan arus kas bulanan, saldo kumulatif ber-frame ROWS UNBOUNDED PRECEDING, dan kontribusi pengeluaran per kategori;
  • berkas tests/pemeriksaan.sql yang memverifikasi seluruh hasil dari kondisi bersih.

Referensi #

Proyek keuangan pribadi menutup satu pola: data transaksional yang rapi lalu dianalisis. Proyek berikutnya, Database Analisis Penjualan, membalik arah kerja — kita mulai dari data mentah yang kotor dan berduplikasi, membersihkannya lewat proses staging, lalu mengekstraksi laporan analitik dengan CTE dan window function sebagai perangkat utama.

Berkas Sumber

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

database/data-awal.sql

Unduh berkas
sql
-- =============================================================
-- 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
sql
-- =============================================================
-- 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
sql
-- =============================================================
-- 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
sql
-- =============================================================
-- 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
sql
-- =============================================================
-- 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
sql
-- =============================================================
-- 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
sql
-- =============================================================
-- 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;