Inilah bagian di mana database toko benar-benar bernyawa: pelanggan menekan tombol checkout, pesanan lahir lengkap dengan rinciannya, stok produk berkurang, uang masuk lalu paket dikirim. Satu klik checkout menyentuh tiga tabel sekaligus — pesanan mendapat baris baru, detail_pesanan mendapat rincian produknya, dan produk kehilangan stoknya — dan di situlah transaksi MySQL menunjukkan fungsinya: ketiga perubahan harus terjadi bersama-sama atau tidak sama sekali. Pada bagian ini kita jalankan alur lengkapnya dari pesanan pertama sampai pembatalan, batalkan salah catat dengan ROLLBACK, lalu sengaja mencoba menjual melebihi stok untuk melihat CHECK constraint menahan garis batas terakhir.

Menyiapkan Ulang Database #

Sesuai kebiasaan proyek ini, kita mulai dari keadaan bersih: skema dan data awal dibangun ulang sehingga angka-angka pada bagian ini dapat diikuti ulang kapan pun. Dua blok berikut menjalankan pernyataan yang sama dengan database/skema.sql dan database/data-awal.sql:

sql
DROP DATABASE IF EXISTS penjualan_db;
CREATE DATABASE penjualan_db;
USE penjualan_db;

CREATE TABLE pelanggan (
  id INT AUTO_INCREMENT PRIMARY KEY,
  nama VARCHAR(60) NOT NULL,
  email VARCHAR(80) NOT NULL UNIQUE,
  kota VARCHAR(30) NOT NULL
);

CREATE TABLE produk (
  id INT AUTO_INCREMENT PRIMARY KEY,
  sku VARCHAR(15) NOT NULL UNIQUE,
  nama VARCHAR(100) NOT NULL,
  kategori VARCHAR(30) NOT NULL,
  harga DECIMAL(10,2) NOT NULL DEFAULT 0.00,
  stok INT NOT NULL DEFAULT 0,
  CONSTRAINT chk_produk_stok CHECK (stok >= 0)
);

CREATE TABLE pesanan (
  no_pesanan INT AUTO_INCREMENT PRIMARY KEY,
  pelanggan_id INT NOT NULL,
  tanggal DATE NOT NULL,
  status ENUM('baru','dibayar','dikirim','selesai','dibatalkan') NOT NULL DEFAULT 'baru',
  CONSTRAINT fk_pesanan_pelanggan
    FOREIGN KEY (pelanggan_id) REFERENCES pelanggan(id)
);

CREATE TABLE detail_pesanan (
  pesanan_id INT NOT NULL,
  produk_id INT NOT NULL,
  jumlah INT NOT NULL,
  harga_satuan DECIMAL(10,2) NOT NULL,
  PRIMARY KEY (pesanan_id, produk_id),
  CONSTRAINT chk_detail_jumlah CHECK (jumlah > 0),
  CONSTRAINT fk_detail_pesanan
    FOREIGN KEY (pesanan_id) REFERENCES pesanan(no_pesanan),
  CONSTRAINT fk_detail_produk
    FOREIGN KEY (produk_id) REFERENCES produk(id)
);

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)
sql
INSERT INTO pelanggan (nama, email, kota) VALUES
  ('Budi Santoso', '[email protected]', 'Bandung'),
  ('Siti Rahmawati', '[email protected]', 'Jakarta'),
  ('Andi Wijaya', '[email protected]', 'Surabaya'),
  ('Dewi Lestari', '[email protected]', 'Yogyakarta'),
  ('Rina Marlina', '[email protected]', 'Bandung'),
  ('Tono Sutrisno', '[email protected]', 'Medan'),
  ('Sari Puspita', '[email protected]', 'Semarang'),
  ('Joko Prasetyo', '[email protected]', 'Denpasar');

INSERT INTO produk (sku, nama, kategori, harga, stok) VALUES
  ('TS-001', 'Kaos Polos Katun Combed 30s', 'Pakaian', 45000.00, 50),
  ('TS-002', 'Kemeja Flanel Lengan Panjang', 'Pakaian', 95000.00, 30),
  ('JA-001', 'Jaket Hoodie Zipper Unisex', 'Pakaian', 125000.00, 20),
  ('CP-001', 'Celana Chino Slim Fit', 'Pakaian', 85000.00, 35),
  ('SN-001', 'Sneakers Kanvas Klasik', 'Sepatu', 120000.00, 25),
  ('SN-002', 'Sandal Gunung Dewasa', 'Sepatu', 65000.00, 40),
  ('TA-001', 'Tas Ransel Laptop 15.6 inch', 'Tas', 145000.00, 15),
  ('TA-002', 'Totebag Kanvas Belajar', 'Tas', 35000.00, 60),
  ('AK-001', 'Topi Baseball Hitam', 'Aksesoris', 28000.00, 45),
  ('AK-002', 'Ikat Pinggang Kulit Sintetis', 'Aksesoris', 55000.00, 25);

Output yang Dihasilkan:

text
Query OK, 8 rows affected (0.00 sec)
Records: 8  Duplicates: 0  Warnings: 0

Query OK, 10 rows affected (0.00 sec)
Records: 10  Duplicates: 0  Warnings: 0

Membuat Pesanan Pertama #

Satu pesanan menyentuh tiga tabel sekaligus: pesanan dicatat, detail_pesanan memuat rincian produknya, dan stok di produk berkurang. Diagram berikut memetakan urutannya:

Siti Rahmawati (pelanggan id 2) memesan pada 5 Maret 2026: dua kaos polos dan satu totebag. Keenam pernyataan berikut berjalan sebagai satu unit kerja:

sql
START TRANSACTION;
INSERT INTO pesanan (pelanggan_id, tanggal, status)
  VALUES (2, '2026-03-05', 'baru');
INSERT INTO detail_pesanan (pesanan_id, produk_id, jumlah, harga_satuan) VALUES
  (LAST_INSERT_ID(), 1, 2, 45000.00),
  (LAST_INSERT_ID(), 8, 1, 35000.00);
UPDATE produk SET stok = stok - 2 WHERE id = 1;
UPDATE produk SET stok = stok - 1 WHERE id = 8;
COMMIT;

Output yang Dihasilkan:

text
Query OK, 0 rows affected (0.00 sec)

Query OK, 1 row affected (0.00 sec)

Query OK, 2 rows affected (0.00 sec)
Records: 2  Duplicates: 0  Warnings: 0

Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

Query OK, 0 rows affected (0.00 sec)

Mari kita bedah alur di atas:

  1. START TRANSACTION membuka unit kerja: sejak titik ini, perubahan hanya terlihat oleh sesi kita dan belum permanen.
  2. INSERT INTO pesanan membuat baris pesanan untuk Siti. Kolom no_pesanan terisi otomatis oleh AUTO_INCREMENT — bernilai 1 karena tabel baru dibangun ulang — dan status tidak disebut sehingga memakai nilai bawaan baru.
  3. LAST_INSERT_ID() mengambil nomor pesanan yang barusan dihasilkan pernyataan kedua, sehingga dua baris detail_pesanan terhubung ke pesanan yang tepat tanpa menuliskan angka manual.
  4. harga_satuan ditulis eksplisit (45000.00 dan 35000.00) — nilainya sama dengan produk.harga saat ini, tetapi disimpan sebagai snapshot. Bila harga kaos naik besok, baris ini tetap memuat harga yang disepakati hari ini.
  5. Dua UPDATE stok mengurangi stok sesuai jumlah tiap produk — pola stok = stok - N menjaga kebenaran walau stok berubah beberapa kali.
  6. COMMIT mengukuhkan seluruh perubahan sekaligus.

Bukti setelah transaksi kita kumpulkan dalam satu blok pemeriksaan — pesanan, rinciannya, dan stok kedua produk:

sql
SELECT p.no_pesanan, pl.nama, p.tanggal, p.status
FROM pesanan p
JOIN pelanggan pl ON pl.id = p.pelanggan_id
ORDER BY p.no_pesanan;
SELECT d.pesanan_id, pr.sku, pr.nama, d.jumlah, d.harga_satuan
FROM detail_pesanan d
JOIN produk pr ON pr.id = d.produk_id
ORDER BY d.pesanan_id, pr.sku;
SELECT id, sku, stok FROM produk WHERE id IN (1, 8) ORDER BY id;

Output yang Dihasilkan:

text
+------------+----------------+------------+--------+
| no_pesanan | nama           | tanggal    | status |
+------------+----------------+------------+--------+
|          1 | Siti Rahmawati | 2026-03-05 | baru   |
+------------+----------------+------------+--------+
1 row in set (0.00 sec)

+------------+--------+-----------------------------+--------+--------------+
| pesanan_id | sku    | nama                        | jumlah | harga_satuan |
+------------+--------+-----------------------------+--------+--------------+
|          1 | TA-002 | Totebag Kanvas Belajar      |      1 |     35000.00 |
|          1 | TS-001 | Kaos Polos Katun Combed 30s |      2 |     45000.00 |
+------------+--------+-----------------------------+--------+--------------+
2 rows in set (0.00 sec)

+----+--------+------+
| id | sku    | stok |
+----+--------+------+
|  1 | TS-001 |   48 |
|  8 | TA-002 |   59 |
+----+--------+------+
2 rows in set (0.00 sec)

Hasilnya konsisten satu sama lain: pesanan 1 milik Siti berstatus baru, dua baris rincian berisi kaos sebanyak 2 dan totebag sebanyak 1, serta stok kaos turun dari 50 ke 48 dan totebag dari 60 ke 59. Satu angka tidak bisa berbohong tanpa angka lain ikut berubah — itulah konsistensi yang dijaga transaksi.

Menghitung Total Pesanan #

Total pesanan tidak pernah disimpan sebagai kolom — ia dihitung dari rinciannya dengan SUM, sehingga tidak mungkin basi atau tidak sinkron:

sql
SELECT p.no_pesanan, pl.nama, p.status,
       SUM(d.jumlah * d.harga_satuan) AS total
FROM pesanan p
JOIN pelanggan pl ON pl.id = p.pelanggan_id
JOIN detail_pesanan d ON d.pesanan_id = p.no_pesanan
GROUP BY p.no_pesanan, pl.nama, p.status
ORDER BY p.no_pesanan;

Output yang Dihasilkan:

text
+------------+----------------+--------+-----------+
| no_pesanan | nama           | status | total     |
+------------+----------------+--------+-----------+
|          1 | Siti Rahmawati | baru   | 125000.00 |
+------------+----------------+--------+-----------+
1 row in set (0.00 sec)

Pesanan 1 bernilai 125000.00: dua kaos (2 × 45000) ditambah satu totebag (35000). Tiga tabel digabung untuk satu baris hasil — pesanan menyediakan nomor dan status, pelanggan menyediakan nama, dan detail_pesanan menyediakan angka yang dijumlahkan. Total bertipe DECIMAL dengan dua digit desimal karena dihitung dari kolom DECIMAL.

Mengubah Status Pesanan #

Uang masuk, jadi pesanan naik tahap dari baru ke dibayar. Perubahan status memakai guard — syarat status lama disebut eksplisit supaya perubahan hanya sah bila pesanan memang berada di tahap sebelumnya:

sql
UPDATE pesanan SET status = 'dibayar' WHERE no_pesanan = 1 AND status = 'baru';
SELECT no_pesanan, tanggal, status FROM pesanan ORDER BY no_pesanan;

Output yang Dihasilkan:

text
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

+------------+------------+---------+
| no_pesanan | tanggal    | status  |
+------------+------------+---------+
|          1 | 2026-03-05 | dibayar |
+------------+------------+---------+
1 row in set (0.00 sec)

Status pesanan 1 kini dibayar. Guard AND status = 'baru' membuat perubahan ini idempoten: bila aplikasi tidak sengaja menjalankan UPDATE yang sama dua kali, pemanggilan kedua menemukan Rows matched: 0 dan tidak menggerakkan status dua tahap. Tahap berikutnya — dikirim lalu selesai — memakai pola UPDATE yang sama dengan guard tahap masing-masing.

Pesanan Kedua dan Pembatalan #

Andi Wijaya (pelanggan id 3) memesan satu jaket hoodie pada 8 Maret 2026. Polanya sama persis dengan pesanan pertama:

sql
START TRANSACTION;
INSERT INTO pesanan (pelanggan_id, tanggal, status)
  VALUES (3, '2026-03-08', 'baru');
INSERT INTO detail_pesanan (pesanan_id, produk_id, jumlah, harga_satuan)
  VALUES (LAST_INSERT_ID(), 3, 1, 125000.00);
UPDATE produk SET stok = stok - 1 WHERE id = 3;
COMMIT;
SELECT id, sku, stok FROM produk WHERE id = 3;

Output yang Dihasilkan:

text
Query OK, 0 rows affected (0.00 sec)

Query OK, 1 row affected (0.00 sec)

Query OK, 1 row affected (0.00 sec)

Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

Query OK, 0 rows affected (0.00 sec)

+----+--------+------+
| id | sku    | stok |
+----+--------+------+
|  3 | JA-001 |   19 |
+----+--------+------+
1 row in set (0.00 sec)

Pesanan 2 lahir dengan nomor otomatis dan stok jaket turun dari 20 menjadi 19. Sehari kemudian Andi membatalkan pesanannya sebelum membayar — pembatalan mencatat status dibatalkan dan memulihkan stok dalam satu transaksi:

sql
START TRANSACTION;
UPDATE pesanan SET status = 'dibatalkan' WHERE no_pesanan = 2 AND status = 'baru';
UPDATE produk SET stok = stok + 1 WHERE id = 3;
COMMIT;
SELECT no_pesanan, status FROM pesanan WHERE no_pesanan = 2;
SELECT id, sku, stok FROM produk WHERE id = 3;

Output yang Dihasilkan:

text
Query OK, 0 rows affected (0.00 sec)

Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

Query OK, 0 rows affected (0.00 sec)

+------------+------------+
| no_pesanan | status     |
+------------+------------+
|          2 | dibatalkan |
+------------+------------+
1 row in set (0.00 sec)

+----+--------+------+
| id | sku    | stok |
+----+--------+------+
|  3 | JA-001 |   20 |
+----+--------+------+
1 row in set (0.00 sec)

Status pesanan 2 menjadi dibatalkan dan stok jaket pulih ke 20. Pesanan tidak dihapus — barisnya tetap sebagai riwayat, dan status dibatalkan inilah yang nanti dipakai Bagian 4 untuk mengecualikan pesanan batal dari perhitungan omzet.

Membatalkan Salah Catat dengan ROLLBACK #

Staf keliru: pesanan Dewi Lestari (pelanggan id 4) tercatat dengan 20 kaos, padahal ia memesan 2. Kesalahan yang belum di-COMMIT masih bisa dibatalkan seluruhnya. Blok berikut membuat pesanan yang salah itu lalu membatalkannya — perhatikan dua SELECT yang memperlihatkan keadaan di dalam transaksi dan sesudah pembatalan:

sql
START TRANSACTION;
INSERT INTO pesanan (pelanggan_id, tanggal, status) VALUES (4, '2026-03-10', 'baru');
INSERT INTO detail_pesanan (pesanan_id, produk_id, jumlah, harga_satuan)
  VALUES (LAST_INSERT_ID(), 1, 20, 45000.00);
UPDATE produk SET stok = stok - 20 WHERE id = 1;
SELECT id, sku, stok FROM produk WHERE id = 1;
ROLLBACK;
SELECT id, sku, stok FROM produk WHERE id = 1;
SELECT COUNT(*) AS jumlah_pesanan FROM pesanan;

Output yang Dihasilkan:

text
Query OK, 0 rows affected (0.00 sec)

Query OK, 1 row affected (0.00 sec)

Query OK, 1 row affected (0.00 sec)

Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

+----+--------+------+
| id | sku    | stok |
+----+--------+------+
|  1 | TS-001 |   28 |
+----+--------+------+
1 row in set (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

+----+--------+------+
| id | sku    | stok |
+----+--------+------+
|  1 | TS-001 |   48 |
+----+--------+------+
1 row in set (0.00 sec)

+----------------+
| jumlah_pesanan |
+----------------+
|              2 |
+----------------+
1 row in set (0.00 sec)

SELECT pertama melihat stok kaos 28 — sesi kita sendiri membaca perubahan yang belum dikukuhkan. Setelah ROLLBACK, stok kembali ke 48 dan COUNT(*) mengembalikan 2: hanya dua pesanan yang benar-benar terjadi (pesanan Siti dan pesanan Andi yang dibatalkan), pesanan keliru Dewi terhapus seolah tidak pernah ditulis. Satu fakta teknis menarik tersembunyi di sini: penomoran AUTO_INCREMENT tidak ikut dibatalkan — nomor 3 sudah terpakai oleh pesanan yang di-ROLLBACK, sehingga pesanan berikutnya akan bernomor 4. Celah nomor seperti ini normal dan tidak merusak apa pun, karena nomor hanya berperan sebagai identitas, bukan hitungan.

Menolak Penjualan Melebihi Stok #

Dua benteng terakhir kita uji dengan sengaja. Pertama, staf mencoba mengubah status dengan nilai yang salah ketik — dibayarr tidak ada dalam daftar ENUM:

sql
-- @error
INSERT INTO pesanan (pelanggan_id, tanggal, status) VALUES (1, '2026-03-12', 'dibayarr');

Output yang Dihasilkan:

text
Query OK, 0 rows affected (0.00 sec)

ERROR 1265 (01000): Data truncated for column 'status' at row 1
Bye

MySQL menolak nilai di luar daftar ENUM sebelum baris apa pun ditulis — salah ketik status tidak akan pernah masuk penyimpanan. Kedua, skenario penutup yang paling menekan: menjual 500 kaos padahal stok tersisa 48. Kode aplikasi semestinya menolak lebih dulu, tetapi bila aplikasi punya bug, CHECK constraint menjadi benteng terakhir:

sql
-- @error
UPDATE produk SET stok = stok - 500 WHERE id = 1;

Output yang Dihasilkan:

text
Query OK, 0 rows affected (0.00 sec)

ERROR 3819 (HY000): Check constraint 'chk_produk_stok' is violated.
Bye

Constraint chk_produk_stok menolak UPDATE itu sebelum satu baris pun berubah, sehingga stok tetap 48. Karena pernyataan ini berdiri sendiri di luar transaksi, penolakan terjadi total: penjualan bermasalah tidak tercatat dan stok tidak tersentuh.

Referensi #

Alur pesanan kini berjalan dengan jaminan konsistensi: transaksi menjaga kesatuan tiga tabel, guard menjaga transisi status, dan CHECK menjaga batas stok. Satu hal yang belum kita miliki adalah pandangan bisnis — pada Bagian 4 kita menyusun analisis dari dua bulan penjualan: omzet per bulan, produk terlaris, pelanggan teratas, dan sebaran status.

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 Penjualan
-- Tujuan: mengisi data contoh deterministik (10 produk dan 8 pelanggan
--   fiktif) sebagai titik awal konsisten untuk seluruh bagian proyek.
-- Cara pakai: jalankan setelah skema.sql:
--   mysql -u root -p penjualan_db < database/data-awal.sql

INSERT INTO pelanggan (nama, email, kota) VALUES
  ('Budi Santoso', '[email protected]', 'Bandung'),
  ('Siti Rahmawati', '[email protected]', 'Jakarta'),
  ('Andi Wijaya', '[email protected]', 'Surabaya'),
  ('Dewi Lestari', '[email protected]', 'Yogyakarta'),
  ('Rina Marlina', '[email protected]', 'Bandung'),
  ('Tono Sutrisno', '[email protected]', 'Medan'),
  ('Sari Puspita', '[email protected]', 'Semarang'),
  ('Joko Prasetyo', '[email protected]', 'Denpasar');

INSERT INTO produk (sku, nama, kategori, harga, stok) VALUES
  ('TS-001', 'Kaos Polos Katun Combed 30s', 'Pakaian', 45000.00, 50),
  ('TS-002', 'Kemeja Flanel Lengan Panjang', 'Pakaian', 95000.00, 30),
  ('JA-001', 'Jaket Hoodie Zipper Unisex', 'Pakaian', 125000.00, 20),
  ('CP-001', 'Celana Chino Slim Fit', 'Pakaian', 85000.00, 35),
  ('SN-001', 'Sneakers Kanvas Klasik', 'Sepatu', 120000.00, 25),
  ('SN-002', 'Sandal Gunung Dewasa', 'Sepatu', 65000.00, 40),
  ('TA-001', 'Tas Ransel Laptop 15.6 inch', 'Tas', 145000.00, 15),
  ('TA-002', 'Totebag Kanvas Belajar', 'Tas', 35000.00, 60),
  ('AK-001', 'Topi Baseball Hitam', 'Aksesoris', 28000.00, 45),
  ('AK-002', 'Ikat Pinggang Kulit Sintetis', 'Aksesoris', 55000.00, 25);

database/skema.sql

Unduh berkas
sql
-- skema.sql — Proyek Database Penjualan
-- Tujuan: membuat database penjualan_db beserta keempat tabelnya
--   (pelanggan, produk, pesanan, detail_pesanan) lengkap dengan primary key,
--   kolom unik email/sku, foreign key bernama, ENUM status, PK komposit,
--   dan check constraint stok/jumlah.
-- Cara pakai: mysql -u root -p < database/skema.sql

DROP DATABASE IF EXISTS penjualan_db;
CREATE DATABASE penjualan_db;
USE penjualan_db;

-- Pelanggan toko online; email unik sebagai identitas kontak.
CREATE TABLE pelanggan (
  id INT AUTO_INCREMENT PRIMARY KEY,
  nama VARCHAR(60) NOT NULL,
  email VARCHAR(80) NOT NULL UNIQUE,
  kota VARCHAR(30) NOT NULL
);

-- Katalog produk; harga DECIMAL agar nilai rupiah pasti.
CREATE TABLE produk (
  id INT AUTO_INCREMENT PRIMARY KEY,
  sku VARCHAR(15) NOT NULL UNIQUE,
  nama VARCHAR(100) NOT NULL,
  kategori VARCHAR(30) NOT NULL,
  harga DECIMAL(10,2) NOT NULL DEFAULT 0.00,
  stok INT NOT NULL DEFAULT 0,
  CONSTRAINT chk_produk_stok CHECK (stok >= 0)
);

-- Satu baris per pesanan; status mengikuti daur hidup pesanan.
CREATE TABLE pesanan (
  no_pesanan INT AUTO_INCREMENT PRIMARY KEY,
  pelanggan_id INT NOT NULL,
  tanggal DATE NOT NULL,
  status ENUM('baru','dibayar','dikirim','selesai','dibatalkan') NOT NULL DEFAULT 'baru',
  CONSTRAINT fk_pesanan_pelanggan
    FOREIGN KEY (pelanggan_id) REFERENCES pelanggan(id)
);

-- Rincian produk per pesanan; harga_satuan adalah snapshot harga saat
-- pesanan dibuat, bukan harga terbaru di tabel produk.
CREATE TABLE detail_pesanan (
  pesanan_id INT NOT NULL,
  produk_id INT NOT NULL,
  jumlah INT NOT NULL,
  harga_satuan DECIMAL(10,2) NOT NULL,
  PRIMARY KEY (pesanan_id, produk_id),
  CONSTRAINT chk_detail_jumlah CHECK (jumlah > 0),
  CONSTRAINT fk_detail_pesanan
    FOREIGN KEY (pesanan_id) REFERENCES pesanan(no_pesanan),
  CONSTRAINT fk_detail_produk
    FOREIGN KEY (produk_id) REFERENCES produk(id)
);

queries/distribusi-status.sql

Unduh berkas
sql
-- distribusi-status.sql — Proyek Database Penjualan
-- Tujuan: menghitung banyaknya pesanan pada tiap status daur hidup
--   (baru, dibayar, dikirim, selesai, dibatalkan).
-- Cara pakai: mysql -u root -p penjualan_db < queries/distribusi-status.sql

SELECT status, COUNT(*) AS jumlah_pesanan
FROM pesanan
GROUP BY status
ORDER BY status;

queries/omzet-per-bulan.sql

Unduh berkas
sql
-- omzet-per-bulan.sql — Proyek Database Penjualan
-- Tujuan: menjumlahkan omzet per bulan dari rincian pesanan yang statusnya
--   sudah pasti menghasilkan uang (dibayar, dikirim, selesai) memakai
--   DATE_FORMAT atas tanggal literal dan harga_satuan snapshot.
-- Cara pakai: mysql -u root -p penjualan_db < queries/omzet-per-bulan.sql

SELECT DATE_FORMAT(p.tanggal, '%Y-%m') AS bulan,
       SUM(d.jumlah * d.harga_satuan) AS omzet
FROM pesanan p
JOIN detail_pesanan d ON d.pesanan_id = p.no_pesanan
WHERE p.status IN ('dibayar','dikirim','selesai')
GROUP BY DATE_FORMAT(p.tanggal, '%Y-%m')
ORDER BY bulan;

queries/pelanggan-teratas.sql

Unduh berkas
sql
-- pelanggan-teratas.sql — Proyek Database Penjualan
-- Tujuan: menampilkan pelanggan berdasarkan jumlah pesanan aktif dan total
--   belanja (jumlah x harga_satuan snapshot); pesanan dibatalkan dikecualikan.
--   Pelanggan tanpa transaksi tetap tampil dengan nilai nol.
-- Cara pakai: mysql -u root -p penjualan_db < queries/pelanggan-teratas.sql

SELECT pl.nama, pl.kota,
       COUNT(DISTINCT p.no_pesanan) AS jumlah_pesanan,
       COALESCE(SUM(d.jumlah * d.harga_satuan), 0) AS total_belanja
FROM pelanggan pl
LEFT JOIN pesanan p ON p.pelanggan_id = pl.id AND p.status <> 'dibatalkan'
LEFT JOIN detail_pesanan d ON d.pesanan_id = p.no_pesanan
GROUP BY pl.id, pl.nama, pl.kota
ORDER BY total_belanja DESC, pl.nama;

queries/produk-terlaris.sql

Unduh berkas
sql
-- produk-terlaris.sql — Proyek Database Penjualan
-- Tujuan: memeringkat produk berdasarkan total unit terjual (pesanan aktif,
--   termasuk yang berstatus baru) memakai window function RANK().
--   Produk tanpa penjualan tetap tampil dengan angka nol.
-- Cara pakai: mysql -u root -p penjualan_db < queries/produk-terlaris.sql

SELECT p.sku, p.nama,
       COALESCE(SUM(dj.jumlah), 0) AS total_terjual,
       RANK() OVER (ORDER BY COALESCE(SUM(dj.jumlah), 0) DESC) AS peringkat
FROM produk p
LEFT JOIN (
  SELECT d.produk_id, d.jumlah
  FROM detail_pesanan d
  JOIN pesanan pe ON pe.no_pesanan = d.pesanan_id
  WHERE pe.status <> 'dibatalkan'
) dj ON dj.produk_id = p.id
GROUP BY p.id, p.sku, p.nama
ORDER BY total_terjual DESC, p.sku;

tests/pemeriksaan.sql

Unduh berkas
sql
-- pemeriksaan.sql — Proyek Database Penjualan
-- Tujuan: pemeriksaan akhir seluruh fitur proyek (pembuatan pesanan atomik,
--   penghitungan total, perubahan status, pembatalan yang memulihkan stok,
--   omzet per bulan, produk terlaris, dan distribusi status).
--   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 penjualan_db;
CREATE DATABASE penjualan_db;
USE penjualan_db;

CREATE TABLE pelanggan (
  id INT AUTO_INCREMENT PRIMARY KEY,
  nama VARCHAR(60) NOT NULL,
  email VARCHAR(80) NOT NULL UNIQUE,
  kota VARCHAR(30) NOT NULL
);
CREATE TABLE produk (
  id INT AUTO_INCREMENT PRIMARY KEY,
  sku VARCHAR(15) NOT NULL UNIQUE,
  nama VARCHAR(100) NOT NULL,
  kategori VARCHAR(30) NOT NULL,
  harga DECIMAL(10,2) NOT NULL DEFAULT 0.00,
  stok INT NOT NULL DEFAULT 0,
  CONSTRAINT chk_produk_stok CHECK (stok >= 0)
);
CREATE TABLE pesanan (
  no_pesanan INT AUTO_INCREMENT PRIMARY KEY,
  pelanggan_id INT NOT NULL,
  tanggal DATE NOT NULL,
  status ENUM('baru','dibayar','dikirim','selesai','dibatalkan') NOT NULL DEFAULT 'baru',
  CONSTRAINT fk_pesanan_pelanggan
    FOREIGN KEY (pelanggan_id) REFERENCES pelanggan(id)
);
CREATE TABLE detail_pesanan (
  pesanan_id INT NOT NULL,
  produk_id INT NOT NULL,
  jumlah INT NOT NULL,
  harga_satuan DECIMAL(10,2) NOT NULL,
  PRIMARY KEY (pesanan_id, produk_id),
  CONSTRAINT chk_detail_jumlah CHECK (jumlah > 0),
  CONSTRAINT fk_detail_pesanan
    FOREIGN KEY (pesanan_id) REFERENCES pesanan(no_pesanan),
  CONSTRAINT fk_detail_produk
    FOREIGN KEY (produk_id) REFERENCES produk(id)
);

INSERT INTO pelanggan (nama, email, kota) VALUES
  ('Budi Santoso', '[email protected]', 'Bandung'),
  ('Siti Rahmawati', '[email protected]', 'Jakarta'),
  ('Andi Wijaya', '[email protected]', 'Surabaya');
INSERT INTO produk (sku, nama, kategori, harga, stok) VALUES
  ('TS-001', 'Kaos Polos Katun Combed 30s', 'Pakaian', 45000.00, 20),
  ('TA-002', 'Totebag Kanvas Belajar', 'Tas', 35000.00, 15),
  ('AK-001', 'Topi Baseball Hitam', 'Aksesoris', 28000.00, 10);

-- Pemeriksaan 1: pesanan dibuat atomik - rincian masuk, stok berkurang.
START TRANSACTION;
INSERT INTO pesanan (pelanggan_id, tanggal, status)
  VALUES (2, '2026-01-10', 'baru');
INSERT INTO detail_pesanan (pesanan_id, produk_id, jumlah, harga_satuan) VALUES
  (LAST_INSERT_ID(), 1, 2, 45000.00),
  (LAST_INSERT_ID(), 3, 1, 28000.00);
UPDATE produk SET stok = stok - 2 WHERE id = 1;
UPDATE produk SET stok = stok - 1 WHERE id = 3;
COMMIT;
-- Ekspektasi: TS-001 stok 18, AK-001 stok 9
SELECT sku, stok FROM produk WHERE id IN (1, 3) ORDER BY id;

-- Pemeriksaan 2: total pesanan dihitung dari rincian (SUM jumlah x harga).
-- Ekspektasi: pesanan 1 total 118000.00
SELECT d.pesanan_id, SUM(d.jumlah * d.harga_satuan) AS total
FROM detail_pesanan d
GROUP BY d.pesanan_id
ORDER BY d.pesanan_id;

-- Pemeriksaan 3: status pesanan berubah dari baru menjadi dibayar.
UPDATE pesanan SET status = 'dibayar' WHERE no_pesanan = 1 AND status = 'baru';
-- Ekspektasi: pesanan 1 status dibayar
SELECT no_pesanan, status FROM pesanan WHERE no_pesanan = 1;

-- Pemeriksaan 4: pembatalan pesanan mengembalikan status dan stok.
START TRANSACTION;
INSERT INTO pesanan (pelanggan_id, tanggal, status)
  VALUES (3, '2026-01-15', 'baru');
INSERT INTO detail_pesanan (pesanan_id, produk_id, jumlah, harga_satuan)
  VALUES (2, 2, 1, 35000.00);
UPDATE produk SET stok = stok - 1 WHERE id = 2;
COMMIT;
START TRANSACTION;
UPDATE pesanan SET status = 'dibatalkan' WHERE no_pesanan = 2 AND status = 'baru';
UPDATE produk SET stok = stok + 1 WHERE id = 2;
COMMIT;
-- Ekspektasi: pesanan 2 dibatalkan, TA-002 stok pulih ke 15
SELECT no_pesanan, status FROM pesanan WHERE no_pesanan = 2;
SELECT sku, stok FROM produk WHERE id = 2;

-- Pemeriksaan 5: pesanan ketiga (Budi) selesai di bulan Februari.
START TRANSACTION;
INSERT INTO pesanan (pelanggan_id, tanggal, status)
  VALUES (1, '2026-02-05', 'baru');
INSERT INTO detail_pesanan (pesanan_id, produk_id, jumlah, harga_satuan)
  VALUES (3, 1, 1, 45000.00);
UPDATE produk SET stok = stok - 1 WHERE id = 1;
COMMIT;
UPDATE pesanan SET status = 'selesai' WHERE no_pesanan = 3 AND status = 'baru';
-- Ekspektasi: pesanan 3 selesai
SELECT no_pesanan, status FROM pesanan WHERE no_pesanan = 3;

-- Pemeriksaan 6: omzet per bulan hanya dari pesanan yang menghasilkan uang.
-- Ekspektasi: 2026-01 omzet 118000.00 (pesanan 2 dibatalkan dikecualikan),
--   2026-02 omzet 45000.00
SELECT DATE_FORMAT(p.tanggal, '%Y-%m') AS bulan,
       SUM(d.jumlah * d.harga_satuan) AS omzet
FROM pesanan p
JOIN detail_pesanan d ON d.pesanan_id = p.no_pesanan
WHERE p.status IN ('dibayar','dikirim','selesai')
GROUP BY DATE_FORMAT(p.tanggal, '%Y-%m')
ORDER BY bulan;

-- Pemeriksaan 7: produk terlaris dengan RANK(); pesanan dibatalkan dikecualikan.
-- Ekspektasi: TS-001 3 unit peringkat 1, AK-001 1 unit peringkat 2,
--   TA-002 0 unit peringkat 3
SELECT p.sku,
       COALESCE(SUM(dj.jumlah), 0) AS total_terjual,
       RANK() OVER (ORDER BY COALESCE(SUM(dj.jumlah), 0) DESC) AS peringkat
FROM produk p
LEFT JOIN (
  SELECT d.produk_id, d.jumlah
  FROM detail_pesanan d
  JOIN pesanan pe ON pe.no_pesanan = d.pesanan_id
  WHERE pe.status <> 'dibatalkan'
) dj ON dj.produk_id = p.id
GROUP BY p.id, p.sku
ORDER BY total_terjual DESC, p.sku;

-- Pemeriksaan 8: distribusi status seluruh pesanan.
-- Ekspektasi: dibatalkan 1, dibayar 1, selesai 1
SELECT status, COUNT(*) AS jumlah_pesanan
FROM pesanan
GROUP BY status
ORDER BY status;

DROP DATABASE penjualan_db;