Daftar cacat dari Bagian 1 kini menemukan eksekusinya. Pada bagian ini kita menjalankan inti pipeline analitik: menyalin data dari penjualan_mentah ke penjualan_bersih sambil membuang tiga jenis penumpang gelap — duplikat persis, spasi buntut, dan qty nol — semuanya dalam satu transaksi yang bisa dibatalkan utuh. Setelah tabel bersih berdiri dan terverifikasi, kita mengeluarkan agregat pertama proyek: omzet per cabang, per bulan, dan per produk. Angka-angka inilah yang menjadi bahan baku window function pada Bagian 3.

Membangun Ulang Kondisi Awal #

Setiap bagian proyek ini mandiri, jadi blok pertama selalu membangun ulang analitik_db dari nol — skema dari berkas database/skema.sql diikuti 28 baris data mentah dari berkas database/data-awal.sql:

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

CREATE TABLE penjualan_mentah (
  id INT AUTO_INCREMENT PRIMARY KEY,
  tanggal DATE NOT NULL,
  cabang VARCHAR(20) NOT NULL,
  produk VARCHAR(30) NOT NULL,
  qty INT NOT NULL,
  harga DECIMAL(10, 2) NOT NULL
);

CREATE TABLE penjualan_bersih (
  id INT AUTO_INCREMENT PRIMARY KEY,
  tanggal DATE NOT NULL,
  cabang VARCHAR(20) NOT NULL,
  produk VARCHAR(30) NOT NULL,
  qty INT NOT NULL,
  harga DECIMAL(10, 2) NOT NULL,
  CONSTRAINT uq_bersih_transaksi
    UNIQUE (tanggal, cabang, produk, qty, harga)
);

INSERT INTO penjualan_mentah (id, tanggal, cabang, produk, qty, harga) VALUES
  (1,  '2025-01-10', 'Jakarta',  'Kopi Gayo',      4, 55000.00),
  (2,  '2025-01-22', 'Bandung',  'Teh Melati',     6, 25000.00),
  (3,  '2025-02-08', 'Surabaya', 'Kopi Toraja',    2, 62000.00),
  (4,  '2025-02-19', 'Jakarta',  'Teh Hijau',      5, 28000.00),
  (5,  '2025-03-05', 'Bandung',  'Kopi Gayo',      2, 55000.00),
  (6,  '2025-03-17', 'Surabaya', 'Teh Melati',     4, 25000.00),
  (7,  '2025-04-09', 'Jakarta',  'Kopi Toraja',    2, 62000.00),
  (8,  '2025-04-21', 'Bandung',  'Grinder Manual', 1, 350000.00),
  (9,  '2025-05-06', 'Surabaya', 'Kopi Gayo',      2, 55000.00),
  (10, '2025-05-18', 'Jakarta',  'Teh Melati',     6, 25000.00),
  (11, '2025-06-03', 'Bandung',  'Kopi Toraja',    2, 62000.00),
  (12, '2025-06-15', 'Surabaya', 'Teh Hijau',      5, 28000.00),
  (13, '2026-01-09', 'Jakarta',  'Kopi Gayo',      3, 55000.00),
  (14, '2026-01-21', 'Bandung',  'Teh Melati',     7, 25000.00),
  (15, '2026-02-05', 'Surabaya', 'Kopi Toraja',    2, 62000.00),
  (16, '2026-02-17', 'Jakarta',  'Teh Hijau',      6, 28000.00),
  (17, '2026-03-04', 'Bandung',  'Kopi Gayo',      2, 55000.00),
  (18, '2026-03-16', 'Surabaya', 'Teh Melati',     3, 25000.00),
  (19, '2026-04-08', 'Jakarta',  'Kopi Toraja',    3, 62000.00),
  (20, '2026-04-20', 'Bandung',  'Grinder Manual', 2, 350000.00),
  (21, '2026-05-05', 'Surabaya', 'Kopi Gayo',      2, 55000.00),
  (22, '2026-05-17', 'Jakarta',  'Teh Melati',     7, 25000.00),
  (23, '2026-06-02', 'Bandung ', 'Kopi Toraja',    2, 62000.00),
  (24, '2026-06-14', 'Surabaya', 'Teh Hijau',      4, 28000.00),
  (25, '2025-01-22', 'Bandung',  'Teh Melati',     6, 25000.00),
  (26, '2026-01-09', 'Jakarta',  'Kopi Gayo',      3, 55000.00),
  (27, '2025-02-08', 'Surabaya', 'Kopi Toraja',    2, 62000.00),
  (28, '2026-03-30', 'Jakarta',  'Teh Hijau',      0, 28000.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, 28 rows affected (0.00 sec)
Records: 28  Duplicates: 0  Warnings: 0

Menyalin Data Bersih dalam Satu Transaksi #

Pembersihan satu perintah ini adalah isi berkas queries/staging-bersih.sql. Tiga aturan dari Bagian 1 diterapkan serentak: DISTINCT melebur baris kembar, TRIM merapikan spasi buntut cabang, dan WHERE qty > 0 membuang transaksi hampa. Seluruh perintah dibungkus START TRANSACTION dan COMMIT agar penyalinan berlaku atomik:

sql
START TRANSACTION;

INSERT INTO penjualan_bersih (tanggal, cabang, produk, qty, harga)
SELECT DISTINCT
  tanggal,
  TRIM(cabang) AS cabang,
  produk,
  qty,
  harga
FROM penjualan_mentah
WHERE qty > 0;

COMMIT;

Output yang Dihasilkan:

text
Query OK, 0 rows affected (0.00 sec)

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

Query OK, 0 rows affected (0.00 sec)

Mari kita bedah aturan demi aturan:

  1. INSERT INTO ... SELECT menyalin hasil query langsung ke tabel tujuan — tanpa ekspor-impor manual. Baris tersalin dalam satu statement sehingga prosesnya cepat dan konsisten.
  2. DISTINCT bekerja pada kombinasi seluruh kolom di daftar SELECT: dua baris yang sama di kelima kolom hanya tercatat sekali. Baris 25, 26, dan 27 yang kembar dengan baris 2, 13, dan 3 otomatis terlebur.
  3. TRIM(cabang) memotong spasi di kedua ujung sebelum nilai masuk tabel bersih — baris 23 'Bandung ' menjadi 'Bandung', menyatu dengan cabang lainnya. TRIM berada di sisi SELECT sehingga pembersihan terjadi saat penyalinan, bukan setelahnya.
  4. WHERE qty > 0 menyaring sebelum penyalinan: transaksi qty nol tidak pernah menyentuh tabel bersih.
  5. START TRANSACTION ... COMMIT menjadikan penyalinan satu kesatuan: bila server berhenti di tengah proses, tidak ada setengah data tersimpan — seluruh insert dibatalkan atau seluruhnya sah.

Hasilnya 24 baris: 28 baris mentah dikurangi 3 duplikat dan 1 baris qty nol.

Memverifikasi Hasil Staging #

Staging yang tidak diverifikasi bukan staging, melainkan harapan. Pemeriksaan pertama membandingkan kedua tabel:

sql
SELECT
  (SELECT COUNT(*) FROM penjualan_mentah) AS baris_mentah,
  (SELECT COUNT(*) FROM penjualan_bersih) AS baris_bersih;

Output yang Dihasilkan:

text
+--------------+--------------+
| baris_mentah | baris_bersih |
+--------------+--------------+
|           28 |           24 |
+--------------+--------------+
1 row in set (0.00 sec)

Pemeriksaan kedua: tiga cabang muncul masing-masing tepat 8 baris — kelompok 'Bandung ' sudah tidak ada:

sql
SELECT cabang, COUNT(*) AS baris
FROM penjualan_bersih
GROUP BY cabang
ORDER BY cabang;

Output yang Dihasilkan:

text
+----------+-------+
| cabang   | baris |
+----------+-------+
| Bandung  |     8 |
| Jakarta  |     8 |
| Surabaya |     8 |
+----------+-------+
3 rows in set (0.00 sec)

Pemeriksaan ketiga mengulang deteksi duplikat dari Bagian 1 pada tabel bersih — hasilnya harus kosong:

sql
SELECT
  tanggal,
  cabang,
  produk,
  qty,
  harga,
  COUNT(*) AS kemunculan
FROM penjualan_bersih
GROUP BY tanggal, cabang, produk, qty, harga
HAVING COUNT(*) > 1;

Output yang Dihasilkan:

text
Empty set (0.00 sec)

Terakhir, kita uji jaring pengamannya. Constraint UNIQUE pada penjualan_bersih seharusnya menolak transaksi yang sudah ada — coba sisipkan baris yang persis sama dengan baris pertama:

sql
-- @error
INSERT INTO penjualan_bersih (tanggal, cabang, produk, qty, harga) VALUES ('2025-01-10', 'Jakarta', 'Kopi Gayo', 4, 55000.00);

Output yang Dihasilkan:

text
Query OK, 0 rows affected (0.00 sec)

ERROR 1062 (23000): Duplicate entry '2025-01-10-Jakarta-Kopi Gayo-4-55000.00' for key 'penjualan_bersih.uq_bersih_transaksi'
Bye

Pesan galat menyebut nama constraint uq_bersih_transaksi lengkap dengan nilai yang bentrok — jaring pengaman dari Bagian 1 terbukti hidup. Inilah alasan penamaan constraint secara eksplisit: pesan galat menjadi bisa dibaca tanpa membuka definisi tabel.

Menyelamatkan Data dengan ROLLBACK #

Bagaimana bila proses staging salah rumus? Katakanlah kita lupa memakai WHERE dan TRIM — 25 baris (termasuk qty nol) dan 'Bandung ' ikut tersalin. Karena percobaan itu dibungkus transaksi, ROLLBACK mengembalikan semuanya:

sql
START TRANSACTION;

DELETE FROM penjualan_bersih;

INSERT INTO penjualan_bersih (tanggal, cabang, produk, qty, harga)
SELECT DISTINCT tanggal, cabang, produk, qty, harga
FROM penjualan_mentah;

SELECT COUNT(*) AS baris_salah
FROM penjualan_bersih;

ROLLBACK;

SELECT COUNT(*) AS baris_kembali FROM penjualan_bersih;

Output yang Dihasilkan:

text
Query OK, 0 rows affected (0.00 sec)

Query OK, 24 rows affected (0.00 sec)

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

+-------------+
| baris_salah |
+-------------+
|          25 |
+-------------+
1 row in set (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

+---------------+
| baris_kembali |
+---------------+
|            24 |
+---------------+
1 row in set (0.00 sec)

Blok ini sengaja memuat dua kecerobohan sekaligus — penyaringan qty yang hilang dan spasi buntut yang ikut terbawa — untuk menunjukkan bahwa ROLLBACK membatalkan semua statement sejak START TRANSACTION: DELETE yang mengosongkan tabel pun ikut terurung, sehingga 24 baris bersih kembali utuh. Angka pada baris_salah (25) dan baris_kembali (24) menjadi bukti hitung yang tidak bisa dibantah.

Omzet per Cabang #

Tabel bersih siap dipakai. Laporan pertama menghitung omzet — hasil kali qty dan harga — untuk tiap cabang:

sql
SELECT
  cabang,
  COUNT(*) AS jumlah_transaksi,
  SUM(qty * harga) AS omzet
FROM penjualan_bersih
GROUP BY cabang
ORDER BY omzet DESC, cabang;

Output yang Dihasilkan:

text
+----------+------------------+------------+
| cabang   | jumlah_transaksi | omzet      |
+----------+------------------+------------+
| Bandung  |                8 | 1843000.00 |
| Jakarta  |                8 | 1328000.00 |
| Surabaya |                8 |  895000.00 |
+----------+------------------+------------+
3 rows in set (0.00 sec)

Ekspresi SUM(qty * harga) menghitung nilai tiap baris sebelum dijumlahkan per kelompok cabang. ORDER BY omzet DESC menempatkan cabang beromzet terbesar di atas — bentuk standar laporan manajerial — dengan cabang sebagai pengurai urutan bila dua cabang seri.

Omzet per Bulan #

Laporan kedua merangkum per bulan memakai DATE_FORMAT yang memotong tanggal ke bentuk YYYY-MM:

sql
SELECT
  DATE_FORMAT(tanggal, '%Y-%m') AS bulan,
  COUNT(*) AS jumlah_transaksi,
  SUM(qty * harga) AS omzet
FROM penjualan_bersih
GROUP BY bulan
ORDER BY bulan;

Output yang Dihasilkan:

text
+---------+------------------+-----------+
| bulan   | jumlah_transaksi | omzet     |
+---------+------------------+-----------+
| 2025-01 |                2 | 370000.00 |
| 2025-02 |                2 | 264000.00 |
| 2025-03 |                2 | 210000.00 |
| 2025-04 |                2 | 474000.00 |
| 2025-05 |                2 | 260000.00 |
| 2025-06 |                2 | 264000.00 |
| 2026-01 |                2 | 340000.00 |
| 2026-02 |                2 | 292000.00 |
| 2026-03 |                2 | 185000.00 |
| 2026-04 |                2 | 886000.00 |
| 2026-05 |                2 | 285000.00 |
| 2026-06 |                2 | 236000.00 |
+---------+------------------+-----------+
12 rows in set (0.00 sec)

Dua belas baris hadir utuh — Januari sampai Juni pada kedua tahun — karena dataset kita memang tidak bolong. Pada data nyata, bulan tanpa transaksi tidak muncul di hasil GROUP BY sama sekali (bukan bernilai nol), sehingga moving average pada Bagian 3 bisa tergeser tanpa terdeteksi. Analis kerap menambalnya dengan tabel kalender — tabel kecil berisi semua tanggal periode — lalu LEFT JOIN dari kalender ke data penjualan; bila nanti data nyata kita berlubang, pola itulah senjata pertama.

Omzet per Produk #

Laporan ketiga memilah per produk, ditambah total unit terjual:

sql
SELECT
  produk,
  SUM(qty) AS total_unit,
  SUM(qty * harga) AS omzet
FROM penjualan_bersih
GROUP BY produk
ORDER BY omzet DESC, produk;

Output yang Dihasilkan:

text
+----------------+------------+------------+
| produk         | total_unit | omzet      |
+----------------+------------+------------+
| Grinder Manual |          3 | 1050000.00 |
| Kopi Gayo      |         15 |  825000.00 |
| Teh Melati     |         33 |  825000.00 |
| Kopi Toraja    |         13 |  806000.00 |
| Teh Hijau      |         20 |  560000.00 |
+----------------+------------+------------+
5 rows in set (0.00 sec)

SUM(qty) menjumlahkan unit — metrik volume — sedangkan SUM(qty * harga) menjumlahkan nilai — metrik omzet. Keduanya tidak selalu sejalan: Grinder Manual memimpin omzet dengan hanya 3 unit, sementara Teh Melati menjual 33 unit tapi beromzet lebih kecil. Perbedaan volume lawan nilai inilah yang membuat peringkat di Bagian 3 menarik.

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

bash
mysql -u root -p analitik_db < queries/omzet-per-cabang.sql
mysql -u root -p analitik_db < queries/omzet-bulanan.sql
mysql -u root -p analitik_db < queries/omzet-per-produk.sql

Membersihkan Database Latihan #

Bagian ini selesai dan analitik_db boleh kita bongkar:

sql
DROP DATABASE IF EXISTS analitik_db;

Output yang Dihasilkan:

text
Query OK, 2 rows affected (0.00 sec)

Referensi #

Data bersih sudah berdiri dengan 24 baris terverifikasi, dan tiga agregat dasar sudah tercetak. Bagian 3 naik kelas: tabel ringkasan tadi dibaca ulang oleh window function untuk menghasilkan peringkat produk per cabang, running total omzet, dan moving average tiga bulan.

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 Analisis Penjualan (Course MySQL)
-- Tujuan      : Memuat 28 baris data penjualan mentah Januari-Juni
--               dua tahun (2025 dan 2026) — sengaja berisi cacat:
--               3 baris duplikat persis (id 25-27), 1 baris qty nol
--               (id 28), dan 1 cabang dengan spasi buntut 'Bandung '
--               (id 23). Perlakukan seolah ekspor dari sistem luar.
-- Cara pakai  : mysql -u root -p analitik_db < database/data-awal.sql
--               (jalankan setelah database/skema.sql)
-- =============================================================

USE analitik_db;

INSERT INTO penjualan_mentah (id, tanggal, cabang, produk, qty, harga) VALUES
  (1,  '2025-01-10', 'Jakarta',  'Kopi Gayo',      4, 55000.00),
  (2,  '2025-01-22', 'Bandung',  'Teh Melati',     6, 25000.00),
  (3,  '2025-02-08', 'Surabaya', 'Kopi Toraja',    2, 62000.00),
  (4,  '2025-02-19', 'Jakarta',  'Teh Hijau',      5, 28000.00),
  (5,  '2025-03-05', 'Bandung',  'Kopi Gayo',      2, 55000.00),
  (6,  '2025-03-17', 'Surabaya', 'Teh Melati',     4, 25000.00),
  (7,  '2025-04-09', 'Jakarta',  'Kopi Toraja',    2, 62000.00),
  (8,  '2025-04-21', 'Bandung',  'Grinder Manual', 1, 350000.00),
  (9,  '2025-05-06', 'Surabaya', 'Kopi Gayo',      2, 55000.00),
  (10, '2025-05-18', 'Jakarta',  'Teh Melati',     6, 25000.00),
  (11, '2025-06-03', 'Bandung',  'Kopi Toraja',    2, 62000.00),
  (12, '2025-06-15', 'Surabaya', 'Teh Hijau',      5, 28000.00),
  (13, '2026-01-09', 'Jakarta',  'Kopi Gayo',      3, 55000.00),
  (14, '2026-01-21', 'Bandung',  'Teh Melati',     7, 25000.00),
  (15, '2026-02-05', 'Surabaya', 'Kopi Toraja',    2, 62000.00),
  (16, '2026-02-17', 'Jakarta',  'Teh Hijau',      6, 28000.00),
  (17, '2026-03-04', 'Bandung',  'Kopi Gayo',      2, 55000.00),
  (18, '2026-03-16', 'Surabaya', 'Teh Melati',     3, 25000.00),
  (19, '2026-04-08', 'Jakarta',  'Kopi Toraja',    3, 62000.00),
  (20, '2026-04-20', 'Bandung',  'Grinder Manual', 2, 350000.00),
  (21, '2026-05-05', 'Surabaya', 'Kopi Gayo',      2, 55000.00),
  (22, '2026-05-17', 'Jakarta',  'Teh Melati',     7, 25000.00),
  (23, '2026-06-02', 'Bandung ', 'Kopi Toraja',    2, 62000.00),
  (24, '2026-06-14', 'Surabaya', 'Teh Hijau',      4, 28000.00),
  (25, '2025-01-22', 'Bandung',  'Teh Melati',     6, 25000.00),
  (26, '2026-01-09', 'Jakarta',  'Kopi Gayo',      3, 55000.00),
  (27, '2025-02-08', 'Surabaya', 'Kopi Toraja',    2, 62000.00),
  (28, '2026-03-30', 'Jakarta',  'Teh Hijau',      0, 28000.00);

database/skema.sql

Unduh berkas
sql
-- =============================================================
-- Nama berkas : skema.sql
-- Proyek      : Database Analisis Penjualan (Course MySQL)
-- Tujuan      : Membangun ulang database analitik_db dengan dua
--               tabel — penjualan_mentah (staging data kotor,
--               tanpa constraint kualitas) dan penjualan_bersih
--               (hasil pembersihan, dijaga UNIQUE komposit lima
--               kolom agar baris duplikat tidak bisa masuk).
-- Cara pakai  : mysql -u root -p < database/skema.sql
--               (lanjutkan dengan database/data-awal.sql dan
--               queries/staging-bersih.sql)
-- =============================================================

DROP DATABASE IF EXISTS analitik_db;
CREATE DATABASE analitik_db;
USE analitik_db;

CREATE TABLE penjualan_mentah (
  id INT AUTO_INCREMENT PRIMARY KEY,
  tanggal DATE NOT NULL,
  cabang VARCHAR(20) NOT NULL,
  produk VARCHAR(30) NOT NULL,
  qty INT NOT NULL,
  harga DECIMAL(10, 2) NOT NULL
);

CREATE TABLE penjualan_bersih (
  id INT AUTO_INCREMENT PRIMARY KEY,
  tanggal DATE NOT NULL,
  cabang VARCHAR(20) NOT NULL,
  produk VARCHAR(30) NOT NULL,
  qty INT NOT NULL,
  harga DECIMAL(10, 2) NOT NULL,
  CONSTRAINT uq_bersih_transaksi
    UNIQUE (tanggal, cabang, produk, qty, harga)
);

queries/kontribusi-kumulatif.sql

Unduh berkas
sql
-- =============================================================
-- Nama berkas : kontribusi-kumulatif.sql
-- Proyek      : Database Analisis Penjualan (Course MySQL)
-- Tujuan      : Menghitung kontribusi omzet tiap produk terhadap
--               total beserta akumulatifnya (pola Pareto) memakai
--               dua window function berjenjang.
-- Cara pakai  : mysql -u root -p analitik_db < queries/kontribusi-kumulatif.sql
-- =============================================================

USE analitik_db;

WITH per_produk AS (
  SELECT
    produk,
    SUM(qty * harga) AS omzet
  FROM penjualan_bersih
  GROUP BY produk
)
SELECT
  produk,
  omzet,
  ROUND(omzet / SUM(omzet) OVER () * 100, 1) AS kontribusi_persen,
  ROUND(
    SUM(omzet) OVER (
      ORDER BY omzet DESC, produk
      ROWS UNBOUNDED PRECEDING
    ) / SUM(omzet) OVER () * 100, 1
  ) AS kumulatif_persen
FROM per_produk
ORDER BY omzet DESC, produk;

queries/laporan-yoy.sql

Unduh berkas
sql
-- =============================================================
-- Nama berkas : laporan-yoy.sql
-- Proyek      : Database Analisis Penjualan (Course MySQL)
-- Tujuan      : Membandingkan omzet tiap cabang Januari-Juni 2026
--               terhadap periode sama tahun 2025 (YoY) lewat CTE
--               yang di-self-join.
-- Cara pakai  : mysql -u root -p analitik_db < queries/laporan-yoy.sql
-- =============================================================

USE analitik_db;

WITH omzet_cabang AS (
  SELECT
    YEAR(tanggal) AS tahun,
    cabang,
    SUM(qty * harga) AS omzet
  FROM penjualan_bersih
  GROUP BY tahun, cabang
)
SELECT
  a.cabang,
  a.omzet AS omzet_2025,
  b.omzet AS omzet_2026,
  ROUND((b.omzet - a.omzet) / a.omzet * 100, 1) AS perubahan_persen
FROM omzet_cabang a
JOIN omzet_cabang b
  ON b.cabang = a.cabang AND b.tahun = 2026
WHERE a.tahun = 2025
ORDER BY perubahan_persen DESC, a.cabang;

queries/moving-average.sql

Unduh berkas
sql
-- =============================================================
-- Nama berkas : moving-average.sql
-- Proyek      : Database Analisis Penjualan (Course MySQL)
-- Tujuan      : Menghitung rata-rata bergerak (moving average) omzet
--               3 bulan dengan AVG() OVER dan frame ROWS.
-- Cara pakai  : mysql -u root -p analitik_db < queries/moving-average.sql
-- =============================================================

USE analitik_db;

WITH omzet_bulanan AS (
  SELECT
    DATE_FORMAT(tanggal, '%Y-%m') AS bulan,
    SUM(qty * harga) AS omzet
  FROM penjualan_bersih
  GROUP BY bulan
)
SELECT
  bulan,
  omzet,
  ROUND(
    AVG(omzet) OVER (
      ORDER BY bulan
      ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
    ), 2
  ) AS rata3_bulan
FROM omzet_bulanan
ORDER BY bulan;

queries/omzet-bulanan.sql

Unduh berkas
sql
-- =============================================================
-- Nama berkas : omzet-bulanan.sql
-- Proyek      : Database Analisis Penjualan (Course MySQL)
-- Tujuan      : Merangkum omzet per bulan (YYYY-MM) dari data
--               bersih, terurut naik.
-- Cara pakai  : mysql -u root -p analitik_db < queries/omzet-bulanan.sql
-- =============================================================

USE analitik_db;

SELECT
  DATE_FORMAT(tanggal, '%Y-%m') AS bulan,
  COUNT(*) AS jumlah_transaksi,
  SUM(qty * harga) AS omzet
FROM penjualan_bersih
GROUP BY bulan
ORDER BY bulan;

queries/omzet-per-cabang.sql

Unduh berkas
sql
-- =============================================================
-- Nama berkas : omzet-per-cabang.sql
-- Proyek      : Database Analisis Penjualan (Course MySQL)
-- Tujuan      : Menghitung total omzet (jumlah x harga) tiap cabang
--               dari data bersih, lengkap jumlah transaksinya.
-- Cara pakai  : mysql -u root -p analitik_db < queries/omzet-per-cabang.sql
-- =============================================================

USE analitik_db;

SELECT
  cabang,
  COUNT(*) AS jumlah_transaksi,
  SUM(qty * harga) AS omzet
FROM penjualan_bersih
GROUP BY cabang
ORDER BY omzet DESC, cabang;

queries/omzet-per-produk.sql

Unduh berkas
sql
-- =============================================================
-- Nama berkas : omzet-per-produk.sql
-- Proyek      : Database Analisis Penjualan (Course MySQL)
-- Tujuan      : Menghitung omzet dan total unit tiap produk dari
--               data bersih, terurut dari omzet terbesar.
-- Cara pakai  : mysql -u root -p analitik_db < queries/omzet-per-produk.sql
-- =============================================================

USE analitik_db;

SELECT
  produk,
  SUM(qty) AS total_unit,
  SUM(qty * harga) AS omzet
FROM penjualan_bersih
GROUP BY produk
ORDER BY omzet DESC, produk;

queries/peringkat-produk.sql

Unduh berkas
sql
-- =============================================================
-- Nama berkas : peringkat-produk.sql
-- Proyek      : Database Analisis Penjualan (Course MySQL)
-- Tujuan      : Memberi peringkat produk berdasarkan omzet di dalam
--               masing-masing cabang dengan CTE agregat + RANK()
--               OVER (PARTITION BY cabang ORDER BY omzet DESC).
-- Cara pakai  : mysql -u root -p analitik_db < queries/peringkat-produk.sql
--               (jalankan setelah staging; tambahkan filter
--               peringkat <= 2 untuk dua produk teratas per cabang)
-- =============================================================

USE analitik_db;

WITH omzet_cabang_produk AS (
  SELECT
    cabang,
    produk,
    SUM(qty * harga) AS omzet
  FROM penjualan_bersih
  GROUP BY cabang, produk
)
SELECT
  cabang,
  produk,
  omzet,
  RANK() OVER (
    PARTITION BY cabang
    ORDER BY omzet DESC
  ) AS peringkat
FROM omzet_cabang_produk
ORDER BY cabang, peringkat;

queries/running-total.sql

Unduh berkas
sql
-- =============================================================
-- Nama berkas : running-total.sql
-- Proyek      : Database Analisis Penjualan (Course MySQL)
-- Tujuan      : Menghitung omzet berjalan (running total) per bulan
--               dengan CTE agregat + SUM() OVER ber-frame ROWS
--               UNBOUNDED PRECEDING.
-- Cara pakai  : mysql -u root -p analitik_db < queries/running-total.sql
--               (jalankan setelah staging)
-- =============================================================

USE analitik_db;

WITH omzet_bulanan AS (
  SELECT
    DATE_FORMAT(tanggal, '%Y-%m') AS bulan,
    SUM(qty * harga) AS omzet
  FROM penjualan_bersih
  GROUP BY bulan
)
SELECT
  bulan,
  omzet,
  SUM(omzet) OVER (
    ORDER BY bulan
    ROWS UNBOUNDED PRECEDING
  ) AS omzet_berjalan
FROM omzet_bulanan
ORDER BY bulan;

queries/staging-bersih.sql

Unduh berkas
sql
-- =============================================================
-- Nama berkas : staging-bersih.sql
-- Proyek      : Database Analisis Penjualan (Course MySQL)
-- Tujuan      : Proses staging — menyalin data dari penjualan_mentah
--               ke penjualan_bersih sekaligus membersihkannya dalam
--               satu transaksi: buang qty nol (WHERE qty > 0),
--               buang duplikat persis (SELECT DISTINCT), dan rapikan
--               spasi buntut cabang lewat TRIM.
-- Cara pakai  : mysql -u root -p analitik_db < queries/staging-bersih.sql
--               (jalankan setelah database/skema.sql dan
--               database/data-awal.sql)
-- =============================================================

USE analitik_db;

START TRANSACTION;

INSERT INTO penjualan_bersih (tanggal, cabang, produk, qty, harga)
SELECT DISTINCT
  tanggal,
  TRIM(cabang) AS cabang,
  produk,
  qty,
  harga
FROM penjualan_mentah
WHERE qty > 0;

COMMIT;

queries/top-n-lainnya.sql

Unduh berkas
sql
-- =============================================================
-- Nama berkas : top-n-lainnya.sql
-- Proyek      : Database Analisis Penjualan (Course MySQL)
-- Tujuan      : Menampilkan 3 produk beromzet terbesar apa adanya
--               lalu menggumpalkan sisanya menjadi baris Lainnya
--               lewat ROW_NUMBER + UNION ALL. Kolom posisi tetap
--               (nomor_urut 4) menjaga baris Lainnya di urutan
--               terakhir laporan.
-- Cara pakai  : mysql -u root -p analitik_db < queries/top-n-lainnya.sql
--               (jalankan setelah staging)
-- =============================================================

USE analitik_db;

WITH per_produk AS (
  SELECT
    produk,
    SUM(qty * harga) AS omzet
  FROM penjualan_bersih
  GROUP BY produk
),
berperingkat AS (
  SELECT
    produk,
    omzet,
    ROW_NUMBER() OVER (ORDER BY omzet DESC, produk) AS nomor_urut
  FROM per_produk
)
SELECT produk, omzet
FROM (
  SELECT nomor_urut, produk, omzet
  FROM berperingkat
  WHERE nomor_urut <= 3
  UNION ALL
  SELECT 4, 'Lainnya', SUM(omzet)
  FROM berperingkat
  WHERE nomor_urut > 3
) AS ringkas
ORDER BY nomor_urut;

tests/pemeriksaan.sql

Unduh berkas
sql
-- =============================================================
-- Nama berkas : pemeriksaan.sql
-- Proyek      : Database Analisis Penjualan (Course MySQL)
-- Tujuan      : Uji akhir otomatis — memeriksa hasil staging
--               (baris mentah vs bersih, cabang rapi), omzet per
--               tahun, produk teratas, dan kelengkapan bulan.
--               Ekspektasi tiap query dituliskan sebagai komentar.
-- Cara pakai  : mysql -h 127.0.0.1 -P 3306 -u root < tests/pemeriksaan.sql
--               (jalankan setelah database/skema.sql, data-awal.sql,
--               dan queries/staging-bersih.sql)
-- =============================================================

USE analitik_db;

-- Pemeriksaan 1: data mentah termuat lengkap.
-- Ekspektasi: baris_mentah = 28.
SELECT COUNT(*) AS baris_mentah FROM penjualan_mentah;

-- Pemeriksaan 2: staging membuang 3 duplikat dan 1 baris qty nol.
-- Ekspektasi: baris_bersih = 24.
SELECT COUNT(*) AS baris_bersih FROM penjualan_bersih;

-- Pemeriksaan 3: spasi buntut cabang hilang — hanya 3 cabang tersisa.
-- Ekspektasi: 3 baris — Bandung 8, Jakarta 8, Surabaya 8.
SELECT cabang, COUNT(*) AS baris
FROM penjualan_bersih
GROUP BY cabang
ORDER BY cabang;

-- Pemeriksaan 4: omzet per tahun (Januari-Juni 2025 dan 2026).
-- Ekspektasi: 2 baris — 2025 sebesar 1842000.00, 2026 sebesar 2224000.00.
SELECT
  YEAR(tanggal) AS tahun,
  SUM(qty * harga) AS omzet
FROM penjualan_bersih
GROUP BY tahun
ORDER BY tahun;

-- Pemeriksaan 5: produk beromzet terbesar.
-- Ekspektasi: 1 baris — Grinder Manual dengan omzet 1050000.00.
SELECT
  produk,
  SUM(qty * harga) AS omzet
FROM penjualan_bersih
GROUP BY produk
ORDER BY omzet DESC, produk
LIMIT 1;

-- Pemeriksaan 6: dua produk seri di peringkat kedua.
-- Ekspektasi: 2 baris — Kopi Gayo dan Teh Melati, keduanya 825000.00.
SELECT
  produk,
  SUM(qty * harga) AS omzet
FROM penjualan_bersih
GROUP BY produk
HAVING SUM(qty * harga) = 825000.00
ORDER BY produk;

-- Pemeriksaan 7: setiap bulan Januari-Juni pada kedua tahun berisi data.
-- Ekspektasi: jumlah_bulan = 12.
SELECT COUNT(*) AS jumlah_bulan
FROM (
  SELECT DATE_FORMAT(tanggal, '%Y-%m') AS bulan
  FROM penjualan_bersih
  GROUP BY bulan
) AS per_bulan;