Gudang yang sehat tidak dihitung dari besarnya rak, melainkan dari bisakah petugas menjawab dua pertanyaan sederhana: berapa stok barang tertentu sekarang, dan ke mana saja barang itu bergerak bulan ini. Pada bagian ini kita berperan sebagai perancang database untuk gudang sebuah toko: kita terjemahkan kebutuhan supervisor gudang menjadi daftar entitas, tentukan relasi di antaranya, baru tulis skema inventaris_db dalam SQL. Skema hasil bagian ini menjadi fondasi tiga bagian berikutnya — master data diisi di Bagian 2, mutasi masuk-keluar dijalankan di Bagian 3, dan laporan persediaan disusun di Bagian 4.
Menganalisis Kebutuhan Sistem #
Mari kita dengarkan keluhan khas supervisor gudang. Stok sering “hilang” di atas kertas karena penyesuaian dicatat langsung tanpa jejak; tidak ada yang tahu kapan harus memesan ulang barang; dan nilai persediaan akhir bulan selalu diperdebatkan karena harga barang berubah-ubah. Dari cerita itu kita tarik kebutuhan data yang konkret:
- setiap barang punya kode SKU unik, nama, kategori, satuan hitung (pcs, rim, botol), stok saat ini, batas stok minimum pemicu pemesanan ulang, dan harga satuan untuk penilaian persediaan;
- setiap mutasi stok tercatat sebagai transaksi dengan arah gerak — masuk (pembelian, retur pelanggan) atau keluar (penjualan, pengiriman cabang) — lengkap dengan jumlah, tanggal, dan alasan;
- stok barang harus selalu konsisten dengan riwayat mutasinya: tidak ada angka stok yang berubah tanpa baris transaksi di baliknya;
- stok tidak boleh negatif dan jumlah mutasi tidak boleh nol atau minus.
Pola klasik perancangan relational database kita pakai lagi: kata benda menjadi kandidat tabel, kata kerja menjadi kandidat relasi. Barang jelas satu tabel; mutasi stok menjadi tabel kedua yang merujuk barang mana yang bergerak.
Entitas dan Relasi #
Hubungan antar tabel kita gambar lebih dulu sebelum menulis satu baris SQL pun. Diagram berikut merangkum seluruh relasi sistem:
Relasi barang–transaksi berjenis satu-ke-banyak: satu barang mengalami puluhan mutasi sepanjang hidupnya, tetapi satu baris transaksi hanya bicara tentang satu barang. Foreign key diletakkan di tabel transaksi pada kolom barang_id. Berbeda dengan proyek perpustakaan yang membutuhkan junction table untuk relasi banyak-ke-banyak buku-peminjaman, gudang kita tidak memerlukan tabel perantara: setiap mutasi memang berdiri sendiri, bukan bagian dari transaksi beranak seperti peminjaman multi-judul.
Ringkasan kolom tiap tabel:
| Tabel | Kolom kunci | Kolom data | Catatan |
|---|---|---|---|
barang | id (PK, AUTO_INCREMENT), sku (UNIQUE) | nama, kategori, satuan, stok, stok_minimum, harga | stok tidak boleh negatif (CHECK) |
transaksi | id (PK, AUTO_INCREMENT), barang_id (FK) | tipe, jumlah, tanggal, keterangan | tipe ENUM masuk/keluar, jumlah > 0 (CHECK) |
Keputusan penting pada tabel barang: id menjadi primary key buatan (surrogate key), sedangkan sku hanya kolom unik. Alasannya praktis — kode SKU bisa berubah kebijakan penulisannya (misalnya pindah format AT-001 menjadi ATL-0001), sedangkan relasi ke tabel transaksi tidak boleh ikut berubah. Kita cukup UPDATE kolom sku; kolom id dan semua foreign key yang merujuknya tetap utuh.
Menulis Skema #
Seluruh perancangan di atas kita tuangkan ke berkas database/skema.sql. Blok berikut berisi pernyataan yang sama dengan berkas tersebut — berkas aslinya menyertakan kepala komentar berisi tujuan dan cara pakai, sedangkan penjelasan tiap tabel bisa dibaca di bedah setelah blok:
DROP DATABASE IF EXISTS inventaris_db;
CREATE DATABASE inventaris_db;
USE inventaris_db;
CREATE TABLE barang (
id INT AUTO_INCREMENT PRIMARY KEY,
sku VARCHAR(15) NOT NULL UNIQUE,
nama VARCHAR(100) NOT NULL,
kategori VARCHAR(30) NOT NULL,
satuan VARCHAR(10) NOT NULL,
stok INT NOT NULL DEFAULT 0,
stok_minimum INT NOT NULL DEFAULT 0,
harga DECIMAL(10,2) NOT NULL DEFAULT 0.00,
CONSTRAINT chk_barang_stok CHECK (stok >= 0)
);
CREATE TABLE transaksi (
id INT AUTO_INCREMENT PRIMARY KEY,
tipe ENUM('masuk','keluar') NOT NULL,
barang_id INT NOT NULL,
jumlah INT NOT NULL,
tanggal DATE NOT NULL,
keterangan VARCHAR(100) NOT NULL,
CONSTRAINT chk_transaksi_jumlah CHECK (jumlah > 0),
CONSTRAINT fk_transaksi_barang
FOREIGN KEY (barang_id) REFERENCES barang(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)Mari kita bedah keputusan penting pada skema di atas:
DROP DATABASE IF EXISTSdi baris pembuka membuat skema aman dijalankan ulang: database lama dibuang lebih dulu, sehingga skema selalu menghasilkan kondisi bersih yang sama. Seluruh bagian proyek ini memakai pola yang sama agar tiap bagian bisa diuji mandiri.sku VARCHAR(15) NOT NULL UNIQUE— kolom SKU dipakai manusia untuk mengenali barang, tetapi tidak dijadikan primary key. ConstraintUNIQUEtetap menjamin dua barang tidak berbagi SKU; pelanggarannya akan muncul sebagai error duplikat pada kolombarang.sku.tipe ENUM('masuk','keluar')— arah mutasi dibatasi dua nilai saja. ENUM menyimpan nilai sebagai indeks angka sehingga ringan, dan nilai di luar daftar (misalnyarusak) langsung ditolak MySQL.harga DECIMAL(10,2)— uang tidak pernah disimpan sebagaiFLOATkarena pembulatan biner bisa menggeser sen terakhir.DECIMALmenyimpan angka pasti dengan dua digit di belakang koma, pas untuk rupiah.CONSTRAINT chk_transaksi_jumlah CHECK (jumlah > 0)— mutasi nol atau minus tidak ada artinya di gudang; constraint ini menolaknya sejak level penyimpanan. Bersamachk_barang_stok, keduanya menjadi benteng terakhir konsistensi stok.keterangan VARCHAR(100) NOT NULL— setiap gerakan barang wajib punya alasan tertulis. Kolom ini yang membuat tabeltransaksilayak disebut buku mutasi, bukan sekadar daftar angka.
Memverifikasi Struktur #
Struktur baru saja dibuat, tetapi perancang yang teliti tidak percaya sebelum melihat buktinya. Kita periksa daftar tabel, lalu kerangka kolom kedua tabel — master barang dan buku mutasi transaksi:
SHOW TABLES;Output yang Dihasilkan:
+-------------------------+
| Tables_in_inventaris_db |
+-------------------------+
| barang |
| transaksi |
+-------------------------+
2 rows in set (0.00 sec)DESC barang;Output yang Dihasilkan:
+--------------+---------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+--------------+---------------+------+-----+---------+----------------+
| id | int | NO | PRI | NULL | auto_increment |
| sku | varchar(15) | NO | UNI | NULL | |
| nama | varchar(100) | NO | | NULL | |
| kategori | varchar(30) | NO | | NULL | |
| satuan | varchar(10) | NO | | NULL | |
| stok | int | NO | | 0 | |
| stok_minimum | int | NO | | 0 | |
| harga | decimal(10,2) | NO | | 0.00 | |
+--------------+---------------+------+-----+---------+----------------+
8 rows in set (0.00 sec)DESC transaksi;Output yang Dihasilkan:
+------------+------------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------+------------------------+------+-----+---------+----------------+
| id | int | NO | PRI | NULL | auto_increment |
| tipe | enum('masuk','keluar') | NO | | NULL | |
| barang_id | int | NO | MUL | NULL | |
| jumlah | int | NO | | NULL | |
| tanggal | date | NO | | NULL | |
| keterangan | varchar(100) | NO | | NULL | |
+------------+------------------------+------+-----+---------+----------------+
6 rows in set (0.00 sec)Hasil DESC barang memperlihatkan dua hal yang kita rancang tadi: kolom sku bertanda UNI — indikator constraint UNIQUE yang menjaga kode barang tidak kembar — sementara kolom id memegang tanda PRI sebagai primary key sesungguhnya. Kolom harga tampil sebagai decimal(10,2) dengan Default 0.00, dan kolom stok membawa Default 0 sehingga barang baru bisa dicatat lebih dulu stok fisiknya menyusul. Pada hasil DESC transaksi, kolom tipe menampilkan seluruh daftar nilai enum('masuk','keluar') di kolom Type — dokumentasi struktur yang bisa dibaca langsung dari database. Adapun barang_id bertanda MUL, penanda bahwa foreign key membuat kolom itu menjadi secondary key yang boleh berulang.
Referensi #
Skema inventaris_db kini berdiri: dua tabel, satu relasi satu-ke-banyak dengan foreign key bernama, dua check constraint, dan kolom sku yang dijaga unik. Struktur yang kosong ini menunggu isi — pada Bagian 2 kita mengisi master barang beserta stok minimumnya, lalu membuktikan bahwa constraint di skema ini benar-benar menolak data yang tidak valid.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
database/data-awal.sql
Unduh berkas-- data-awal.sql — Proyek Database Inventaris Barang
-- Tujuan: mengisi data contoh deterministik (10 barang gudang fiktif)
-- sebagai titik awal konsisten untuk seluruh bagian proyek.
-- Cara pakai: jalankan setelah skema.sql:
-- mysql -u root -p inventaris_db < database/data-awal.sql
INSERT INTO barang (sku, nama, kategori, satuan, stok, stok_minimum, harga) VALUES
('AT-001', 'Pulpen Hitam Standard AE7', 'Alat Tulis', 'pcs', 120, 30, 3500.00),
('AT-002', 'Pensil 2B Nusantara', 'Alat Tulis', 'pcs', 85, 25, 2500.00),
('AT-003', 'Buku Tulis 38 Lembar Sekolah', 'Alat Tulis', 'buku', 40, 12, 4500.00),
('KR-001', 'Kertas HVS A4 80gsm', 'Kertas', 'rim', 25, 10, 55000.00),
('KR-002', 'Amplop Cokelat Ukuran F4', 'Kertas', 'pak', 18, 12, 15000.00),
('EL-001', 'Kalkulator Desktop CD-8', 'Elektronik', 'unit', 8, 3, 95000.00),
('EL-002', 'Baterai AA Isi 4', 'Elektronik', 'pack', 60, 20, 18000.00),
('KB-001', 'Sabun Cuci Tangan Lavender', 'Kebersihan', 'botol', 45, 15, 12000.00),
('KB-002', 'Tisu Wajah 250 Sheet', 'Kebersihan', 'pak', 10, 8, 13500.00),
('KP-001', 'Kantong Plastik Kresek M', 'Kemasan', 'pak', 200, 50, 5000.00);
database/skema.sql
Unduh berkas-- skema.sql — Proyek Database Inventaris Barang
-- Tujuan: membuat database inventaris_db beserta kedua tabelnya
-- (barang, transaksi) lengkap dengan primary key, kolom sku yang unik,
-- foreign key bernama, dan check constraint stok/jumlah.
-- Cara pakai: mysql -u root -p < database/skema.sql
DROP DATABASE IF EXISTS inventaris_db;
CREATE DATABASE inventaris_db;
USE inventaris_db;
-- Master barang gudang; sku unik per produk, stok tidak boleh negatif.
CREATE TABLE barang (
id INT AUTO_INCREMENT PRIMARY KEY,
sku VARCHAR(15) NOT NULL UNIQUE,
nama VARCHAR(100) NOT NULL,
kategori VARCHAR(30) NOT NULL,
satuan VARCHAR(10) NOT NULL,
stok INT NOT NULL DEFAULT 0,
stok_minimum INT NOT NULL DEFAULT 0,
harga DECIMAL(10,2) NOT NULL DEFAULT 0.00,
CONSTRAINT chk_barang_stok CHECK (stok >= 0)
);
-- Buku mutasi stok; ENUM tipe membedakan arah gerak barang.
CREATE TABLE transaksi (
id INT AUTO_INCREMENT PRIMARY KEY,
tipe ENUM('masuk','keluar') NOT NULL,
barang_id INT NOT NULL,
jumlah INT NOT NULL,
tanggal DATE NOT NULL,
keterangan VARCHAR(100) NOT NULL,
CONSTRAINT chk_transaksi_jumlah CHECK (jumlah > 0),
CONSTRAINT fk_transaksi_barang
FOREIGN KEY (barang_id) REFERENCES barang(id)
);
queries/barang-stok-minimum.sql
Unduh berkas-- barang-stok-minimum.sql — Proyek Database Inventaris Barang
-- Tujuan: menampilkan barang yang stoknya sudah menyentuh atau turun di
-- bawah stok_minimum beserta kekurangannya, sebagai dasar pemesanan ulang.
-- Cara pakai: mysql -u root -p inventaris_db < queries/barang-stok-minimum.sql
SELECT b.sku, b.nama, b.stok, b.stok_minimum,
b.stok_minimum - b.stok AS kekurangan
FROM barang b
WHERE b.stok <= b.stok_minimum
ORDER BY kekurangan DESC, b.sku;
queries/kartu-stok.sql
Unduh berkas-- kartu-stok.sql — Proyek Database Inventaris Barang
-- Tujuan: menyusun kartu stok satu barang: riwayat transaksi berurutan
-- waktu beserta stok berjalan (running total) memakai window function
-- SUM() OVER dengan frame ROWS eksplisit.
-- Cara pakai: mysql -u root -p inventaris_db < queries/kartu-stok.sql
SELECT t.tanggal, t.tipe, t.jumlah, t.keterangan,
SUM(CASE WHEN t.tipe = 'masuk' THEN t.jumlah ELSE -t.jumlah END)
OVER (ORDER BY t.tanggal, t.id
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS stok_berjalan
FROM transaksi t
JOIN barang b ON b.id = t.barang_id
WHERE b.sku = 'AT-001'
ORDER BY t.tanggal, t.id;
queries/nilai-persediaan.sql
Unduh berkas-- nilai-persediaan.sql — Proyek Database Inventaris Barang
-- Tujuan: menghitung nilai persediaan per kategori (stok x harga),
-- diurutkan dari kategori dengan nilai terbesar.
-- Cara pakai: mysql -u root -p inventaris_db < queries/nilai-persediaan.sql
SELECT b.kategori,
COUNT(*) AS jumlah_item,
SUM(b.stok * b.harga) AS nilai_persediaan
FROM barang b
GROUP BY b.kategori
ORDER BY nilai_persediaan DESC;
queries/ringkasan-masuk-keluar.sql
Unduh berkas-- ringkasan-masuk-keluar.sql — Proyek Database Inventaris Barang
-- Tujuan: merangkum total barang masuk dan keluar per kategori per bulan
-- dengan agregasi bersyarat (SUM + CASE) dan DATE_FORMAT.
-- Cara pakai: mysql -u root -p inventaris_db < queries/ringkasan-masuk-keluar.sql
SELECT b.kategori,
DATE_FORMAT(t.tanggal, '%Y-%m') AS bulan,
SUM(CASE WHEN t.tipe = 'masuk' THEN t.jumlah ELSE 0 END) AS total_masuk,
SUM(CASE WHEN t.tipe = 'keluar' THEN t.jumlah ELSE 0 END) AS total_keluar
FROM transaksi t
JOIN barang b ON b.id = t.barang_id
WHERE t.tanggal BETWEEN '2026-01-01' AND '2026-02-28'
GROUP BY b.kategori, DATE_FORMAT(t.tanggal, '%Y-%m')
ORDER BY b.kategori, bulan;
tests/pemeriksaan.sql
Unduh berkas-- pemeriksaan.sql — Proyek Database Inventaris Barang
-- Tujuan: pemeriksaan akhir seluruh fitur proyek (mutasi masuk-keluar,
-- konsistensi stok dengan buku mutasi, ringkasan bulanan, nilai
-- persediaan, batas stok minimum, dan kartu stok).
-- Berkas ini MANDIRI: membangun skenario uji kecilnya sendiri,
-- lalu membersihkan database setelah selesai.
-- Cara pakai: mysql -u root -p < tests/pemeriksaan.sql
-- Ekspektasi tiap hasil ditulis sebagai komentar di atas query terkait.
DROP DATABASE IF EXISTS inventaris_db;
CREATE DATABASE inventaris_db;
USE inventaris_db;
CREATE TABLE barang (
id INT AUTO_INCREMENT PRIMARY KEY,
sku VARCHAR(15) NOT NULL UNIQUE,
nama VARCHAR(100) NOT NULL,
kategori VARCHAR(30) NOT NULL,
satuan VARCHAR(10) NOT NULL,
stok INT NOT NULL DEFAULT 0,
stok_minimum INT NOT NULL DEFAULT 0,
harga DECIMAL(10,2) NOT NULL DEFAULT 0.00,
CONSTRAINT chk_barang_stok CHECK (stok >= 0)
);
CREATE TABLE transaksi (
id INT AUTO_INCREMENT PRIMARY KEY,
tipe ENUM('masuk','keluar') NOT NULL,
barang_id INT NOT NULL,
jumlah INT NOT NULL,
tanggal DATE NOT NULL,
keterangan VARCHAR(100) NOT NULL,
CONSTRAINT chk_transaksi_jumlah CHECK (jumlah > 0),
CONSTRAINT fk_transaksi_barang
FOREIGN KEY (barang_id) REFERENCES barang(id)
);
INSERT INTO barang (sku, nama, kategori, satuan, stok, stok_minimum, harga) VALUES
('AT-001', 'Pulpen Hitam Standard AE7', 'Alat Tulis', 'pcs', 100, 30, 3500.00),
('KR-001', 'Kertas HVS A4 80gsm', 'Kertas', 'rim', 20, 10, 55000.00),
('KB-001', 'Sabun Cuci Tangan Lavender', 'Kebersihan', 'botol', 30, 15, 12000.00);
-- Baris saldo awal: stok data awal dibukukan sebagai transaksi masuk
-- sehingga seluruh mutasi bisa direkonstruksi dari tabel transaksi.
INSERT INTO transaksi (tipe, barang_id, jumlah, tanggal, keterangan) VALUES
('masuk', 1, 100, '2025-12-31', 'Saldo awal periode'),
('masuk', 2, 20, '2025-12-31', 'Saldo awal periode'),
('masuk', 3, 30, '2025-12-31', 'Saldo awal periode');
-- Pemeriksaan 1: barang masuk menambah stok secara atomik.
START TRANSACTION;
INSERT INTO transaksi (tipe, barang_id, jumlah, tanggal, keterangan)
VALUES ('masuk', 1, 50, '2026-01-10', 'Pembelian PO-2601-01');
UPDATE barang SET stok = stok + 50 WHERE id = 1;
COMMIT;
-- Ekspektasi: AT-001 stok 150
SELECT sku, stok FROM barang WHERE id = 1;
-- Pemeriksaan 2: barang keluar mengurangi stok secara atomik.
START TRANSACTION;
INSERT INTO transaksi (tipe, barang_id, jumlah, tanggal, keterangan)
VALUES ('keluar', 1, 30, '2026-01-15', 'Penjualan borongan');
UPDATE barang SET stok = stok - 30 WHERE id = 1;
COMMIT;
-- Ekspektasi: AT-001 stok 120
SELECT sku, stok FROM barang WHERE id = 1;
-- Pemeriksaan 3: mutasi barang lain (kertas turun ke batas minimum).
START TRANSACTION;
INSERT INTO transaksi (tipe, barang_id, jumlah, tanggal, keterangan)
VALUES ('keluar', 2, 10, '2026-01-18', 'Pengiriman cabang Bandung');
UPDATE barang SET stok = stok - 10 WHERE id = 2;
COMMIT;
START TRANSACTION;
INSERT INTO transaksi (tipe, barang_id, jumlah, tanggal, keterangan)
VALUES ('keluar', 3, 18, '2026-02-05', 'Pengiriman cabang Surabaya');
UPDATE barang SET stok = stok - 18 WHERE id = 3;
COMMIT;
-- Ekspektasi: KR-001 stok 10, KB-001 stok 12
SELECT sku, stok FROM barang ORDER BY sku;
-- Pemeriksaan 4: stok sama dengan saldo buku mutasi (konsistensi ledger).
-- Ekspektasi: 3 baris - stok dan saldo_buku sama: 120, 10, 12
SELECT b.sku, b.stok,
COALESCE(SUM(CASE WHEN t.tipe = 'masuk' THEN t.jumlah ELSE -t.jumlah END), 0) AS saldo_buku
FROM barang b
LEFT JOIN transaksi t ON t.barang_id = b.id
GROUP BY b.id, b.sku, b.stok
ORDER BY b.sku;
-- Pemeriksaan 5: ringkasan masuk/keluar per kategori per bulan.
-- Ekspektasi: 3 baris - Alat Tulis 2026-01 (50, 30), Kertas 2026-01 (0, 10),
-- Kebersihan 2026-02 (0, 18)
SELECT b.kategori,
DATE_FORMAT(t.tanggal, '%Y-%m') AS bulan,
SUM(CASE WHEN t.tipe = 'masuk' THEN t.jumlah ELSE 0 END) AS total_masuk,
SUM(CASE WHEN t.tipe = 'keluar' THEN t.jumlah ELSE 0 END) AS total_keluar
FROM transaksi t
JOIN barang b ON b.id = t.barang_id
WHERE t.tanggal BETWEEN '2026-01-01' AND '2026-02-28'
GROUP BY b.kategori, DATE_FORMAT(t.tanggal, '%Y-%m')
ORDER BY b.kategori, bulan;
-- Pemeriksaan 6: nilai persediaan total (stok x harga).
-- Ekspektasi: 1114000.00
SELECT SUM(stok * harga) AS nilai_persediaan FROM barang;
-- Pemeriksaan 7: barang yang menyentuh atau di bawah stok_minimum.
-- Ekspektasi: 2 baris - KB-001 (kekurangan 3), KR-001 (kekurangan 0)
SELECT sku, stok, stok_minimum, stok_minimum - stok AS kekurangan
FROM barang
WHERE stok <= stok_minimum
ORDER BY kekurangan DESC, sku;
-- Pemeriksaan 8: kartu stok AT-001; stok berjalan berakhir di angka stok.
-- Ekspektasi: 3 baris - 100, 150, 120
SELECT t.tanggal, t.tipe, t.jumlah,
SUM(CASE WHEN t.tipe = 'masuk' THEN t.jumlah ELSE -t.jumlah END)
OVER (ORDER BY t.tanggal, t.id
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS stok_berjalan
FROM transaksi t
JOIN barang b ON b.id = t.barang_id
WHERE b.sku = 'AT-001'
ORDER BY t.tanggal, t.id;
DROP DATABASE inventaris_db;