Bagian-bagian sebelumnya membangun mesin; bagian ini menyerahkan setirnya ke manajer. Tiga laporan penutup menjawab pertanyaan ruang rapat: apakah bisnis tumbuh dibanding tahun lalu, produk mana yang menopang omzet, dan bagaimana meringkas rak produk panjang menjadi lima baris yang terbaca sepintas. 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 — skema, data mentah, lalu staging dalam satu transaksi sehingga penjualan_bersih berisi 24 baris:

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

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, 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

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)

Perbandingan Tahun ke Tahun (YoY) #

Laporan pertama membandingkan omzet Januari-Juni 2026 terhadap periode sama di 2025, per cabang. Triknya: satu CTE agregat yang menyimpan kedua tahun sekaligus, lalu self-join — CTE itu dijoin dengan dirinya sendiri, versi 2025 sebagai sisi kiri dan versi 2026 sebagai sisi kanan:

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

Output yang Dihasilkan:

text
+----------+------------+------------+------------------+
| cabang   | omzet_2025 | omzet_2026 | perubahan_persen |
+----------+------------+------------+------------------+
| Bandung  |  734000.00 | 1109000.00 |             51.1 |
| Jakarta  |  634000.00 |  694000.00 |              9.5 |
| Surabaya |  474000.00 |  421000.00 |            -11.2 |
+----------+------------+------------+------------------+
3 rows in set (0.00 sec)

Bedah kerjanya:

  1. CTE omzet_cabang menghasilkan enam baris — tiga cabang × dua tahun. Kolom tahun hasil YEAR(tanggal) menjadi kunci pembanding.
  2. Alias a dan b keduanya menunjuk CTE yang sama; MySQL mengmaterialisasikannya sekali lalu membaca dua kali. Self-join menggantikan query UNION yang jauh lebih bertele-tele.
  3. ON b.cabang = a.cabang AND b.tahun = 2026 memasangkan tiap baris 2025 dengan satu-satunya baris 2026 milik cabang yang sama; WHERE a.tahun = 2025 memastikan sisi kiri murni tahun dasar.
  4. Persentase perubahan dihitung dari selisih dibagi omzet tahun dasar — bukan tahun baru — sesuai konvensi YoY.

Kisah laporannya: Bandung tumbuh 51.1% (didorong Grinder Manual yang terjual dua unit di 2026), Jakarta naik tipis 9.5%, sementara Surabaya terjun -11.2% dan pantas jadi perhatian rapat berikutnya.

Kontribusi Kumulatif Produk (Pareto) #

Laporan kedua menguji prinsip Pareto — apakah sedikit produk menopang sebagian besar omzet? Dua window function bekerja berjenjang: satu menghitung porsi tiap produk, satu menghitung akumulasi porsinya sejak produk terbesar:

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

Output yang Dihasilkan:

text
+----------------+------------+-------------------+------------------+
| produk         | omzet      | kontribusi_persen | kumulatif_persen |
+----------------+------------+-------------------+------------------+
| Grinder Manual | 1050000.00 |              25.8 |             25.8 |
| Kopi Gayo      |  825000.00 |              20.3 |             46.1 |
| Teh Melati     |  825000.00 |              20.3 |             66.4 |
| Kopi Toraja    |  806000.00 |              19.8 |             86.2 |
| Teh Hijau      |  560000.00 |              13.8 |            100.0 |
+----------------+------------+-------------------+------------------+
5 rows in set (0.00 sec)

Tiga jendela berbeda hidup dalam satu query:

  1. SUM(omzet) OVER () — jendela kosong tanpa ORDER BY — menjumlahkan seluruh baris, menjadi penyebut tetap (total 4066000) untuk setiap produk.
  2. SUM(omzet) OVER (ORDER BY omzet DESC ... ROWS UNBOUNDED PRECEDING) menjumlahkan berjalan dari produk terbesar ke bawah — sama seperti running total Bagian 3, tetapi partisinya daftar produk.
  3. Pembagian keduanya menghasilkan persentase kumulatif: setelah tiga produk teratas, angkanya sudah 66.4%. Aturan Pirani 80/20 tidak persis terpenuhi di dataset kecil ini, tetapi arahnya jelas — tiga dari lima produk menopang dua pertiga omzet, cukup untuk memrioritaskan stok dan promosi.

Tiga Produk Teratas dan “Lainnya” #

Laporan ketiga meringkas rak produk untuk tampilan singkat: tiga produk teratas tampil apa adanya, sisanya digumpalkan menjadi satu baris “Lainnya”. Polanya ROW_NUMBER untuk menomori, lalu UNION ALL menyatukan dua bagian — dan kolom posisi menjaga “Lainnya” tetap di baris terakhir:

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

Output yang Dihasilkan:

text
+----------------+------------+
| produk         | omzet      |
+----------------+------------+
| Grinder Manual | 1050000.00 |
| Kopi Gayo      |  825000.00 |
| Teh Melati     |  825000.00 |
| Lainnya        | 1366000.00 |
+----------------+------------+
4 rows in set (0.00 sec)

Bedah susunannya:

  1. CTE kedua berperingkat memberi nomor urut global dengan tiebreaker produk; tiga besar tetap Grinder Manual, Kopi Gayo, Teh Melati.
  2. UNION ALL menumpuk dua hasil: tiga baris pertama apa adanya, dan satu baris agregat gumpalan. ALL dipilih bukan tanpa alasan — UNION (tanpa ALL) menjalankan penghapusan duplikat ekstra yang membuang waktu dan, pada nilai yang sama, bisa menghapus baris sah.
  3. Angka literal 4 pada cabang kedua menjadi posisi tetap baris Lainnya. Tanpa kolom posisi, ORDER BY omzet DESC akan menaikkan Lainnya (omzet 1366000) ke baris pertama — benar secara hitungan, salah secara tata letak laporan.
  4. Subquery ringkas membungkus UNION ALL agar ORDER BY final berlaku pada hasil gabungan, bukan pada masing-masing cabang.

Baris Lainnya yang beromzet 1366000 juga menyimpan pesan: ekor produk menyumbang sepertiga omzet. Grafik top-3 yang memesona bisa menyesatkan bila pembaca lupa baris terakhirnya.

Uji Akhir dengan Berkas Pemeriksaan #

Setiap laporan yang kita bangun sepanjang proyek kini diuji dari kondisi benar-benar bersih. Empat perintah berikut membangun ulang database dari berkas database/skema.sql, memuat data mentah, menjalankan staging queries/staging-bersih.sql, lalu menjalankan tests/pemeriksaan.sql yang memeriksa staging, omzet per tahun, produk teratas, hingga kelengkapan bulan — lengkap dengan komentar ekspektasi di tiap langkah:

bash
-- @test-bash
mysql -h 127.0.0.1 -P 3306 -u root < content/mysql/proyek/database-analisis-penjualan/database/skema.sql
mysql -h 127.0.0.1 -P 3306 -u root < content/mysql/proyek/database-analisis-penjualan/database/data-awal.sql
mysql -h 127.0.0.1 -P 3306 -u root < content/mysql/proyek/database-analisis-penjualan/queries/staging-bersih.sql
mysql -h 127.0.0.1 -P 3306 -u root < content/mysql/proyek/database-analisis-penjualan/tests/pemeriksaan.sql

Output yang Dihasilkan:

text
baris_mentah
28
baris_bersih
24
cabang	baris
Bandung	8
Jakarta	8
Surabaya	8
tahun	omzet
2025	1842000.00
2026	2224000.00
produk	omzet
Grinder Manual	1050000.00
produk	omzet
Kopi Gayo	825000.00
Teh Melati	825000.00
jumlah_bulan
12

Tujuh pemeriksaan lolos semua: 28 baris mentah termuat, staging menghasilkan tepat 24 baris, tiga cabang masing-masing 8 baris, omzet 2025 sebesar 1842000.00 dan 2026 sebesar 2224000.00, produk teratas Grinder Manual dengan 1050000.00, dua produk seri di peringkat kedua, dan 12 bulan periode lengkap. Mode batch mysql menampilkan hasil dengan pemisah tab sehingga angka-angka tersebut identik dengan output interaktif — hanya formatnya lebih ramping.

Membersihkan Database Latihan #

Proyek tuntas dan analitik_db hasil uji akhir tadi bisa kita lepaskan:

sql
DROP DATABASE IF EXISTS analitik_db;

Output yang Dihasilkan:

text
Query OK, 2 rows affected (0.00 sec)

Ringkasan Proyek #

Empat bagian proyek Database Analisis Penjualan menghasilkan satu pipeline analitik utuh. Capaian kita:

  • dua tabel berperan tegas — staging bebas constraint untuk data kotor, tabel bersih dijaga UNIQUE komposit lima kolom;
  • audit kualitas data dengan COUNT DISTINCT, GROUP BY ... HAVING, dan ROW_NUMBER penanda kembar;
  • pembersihan atomik lewat INSERT ... SELECT DISTINCT bergulir TRIM dan penyaring qty, dibungkus satu transaksi dengan ROLLBACK penyelamat;
  • agregasi omzet per cabang, per bulan, dan per produk sebagai fondasi laporan;
  • window function dan CTE beruntun: peringkat produk per cabang, top-2 per cabang, running total, dan moving average tiga bulan;
  • tiga laporan analitis — YoY self-join, kontribusi kumulatif Pareto, dan top-3 plus Lainnya lewat UNION ALL;
  • berkas tests/pemeriksaan.sql yang memverifikasi seluruh hasil dari kondisi bersih.

Referensi #

Proyek analisis penjualan menutup satu pola kerja: data mentah dibersihkan lalu dibaca diam-diam oleh query analitik. Proyek berikutnya, Database Manajemen Tugas, menghidupkan kembali sisi transaksional — data berubah setiap hari oleh banyak pengguna, integritasnya dijaga foreign key dan ENUM, dan setiap perubahan status tercatat dalam riwayat yang atomik.

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;