INTENSIVE LEARNING PATH

Belajar SQL
dari Nol Sampai Mahir

Panduan lengkap 7 hari dengan praktek langsung — dari SELECT dasar hingga analisis bisnis profesional

📅
7 Hari Intensif
⏱️
4-6 Jam/Hari
🛠️
DBeaver + SQLite
🎯
Praktek Langsung
📋 Roadmap 7 Hari
1
Fondasi SQL
SELECT · WHERE · ORDER BY · LIMIT
2
Fungsi Agregat
COUNT · SUM · AVG · GROUP BY · HAVING
3
JOIN — Gabung Tabel
INNER · LEFT · SELF JOIN · Multi-table
4
Subquery & Fungsi
Subquery · CASE WHEN · String · Date
5
DML & Transaksi
INSERT · UPDATE · DELETE · TRANSACTION
6
Advanced SQL
VIEW · INDEX · CTE · Window Functions
7
Studi Kasus Nyata
Analisis Penjualan · Customer · Bisnis
⚙️ Persiapan Sebelum Mulai
Setup database latihan di DBeaver (cukup sekali di awal)
1
Buka DBeaver → klik Database → New Database Connection
2
Pilih SQLite (paling mudah, tidak perlu install server)
3
Klik Create → simpan file sebagai belajar_sql.db
4
Klik kanan database → SQL Editor → New SQL Script
5
Copy-paste & jalankan script di bawah ini (Ctrl + Enter)
📦 Script Buat Database Latihan — Jalankan Semua Ini
-- ============================================
-- DATABASE LATIHAN: SISTEM TOKO ONLINE
-- ============================================

-- Tabel Pelanggan
CREATE TABLE pelanggan (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    nama VARCHAR(100) NOT NULL,
    email VARCHAR(100) UNIQUE,
    kota VARCHAR(50),
    umur INTEGER,
    jenis_kelamin VARCHAR(10),
    tanggal_daftar DATE,
    status VARCHAR(20) DEFAULT 'aktif'
);

-- Tabel Kategori Produk
CREATE TABLE kategori (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    nama_kategori VARCHAR(50) NOT NULL,
    deskripsi TEXT
);

-- Tabel Produk
CREATE TABLE produk (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    nama_produk VARCHAR(100) NOT NULL,
    kategori_id INTEGER,
    harga DECIMAL(12,2) NOT NULL,
    stok INTEGER DEFAULT 0,
    berat_gram INTEGER,
    tanggal_masuk DATE,
    FOREIGN KEY (kategori_id) REFERENCES kategori(id)
);

-- Tabel Pesanan
CREATE TABLE pesanan (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    pelanggan_id INTEGER NOT NULL,
    tanggal_pesan DATE NOT NULL,
    total_harga DECIMAL(15,2),
    status_pesanan VARCHAR(20) DEFAULT 'pending',
    metode_bayar VARCHAR(30),
    alamat_kirim TEXT,
    FOREIGN KEY (pelanggan_id) REFERENCES pelanggan(id)
);

-- Tabel Detail Pesanan
CREATE TABLE detail_pesanan (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    pesanan_id INTEGER NOT NULL,
    produk_id INTEGER NOT NULL,
    jumlah INTEGER NOT NULL,
    harga_satuan DECIMAL(12,2) NOT NULL,
    subtotal DECIMAL(15,2),
    FOREIGN KEY (pesanan_id) REFERENCES pesanan(id),
    FOREIGN KEY (produk_id) REFERENCES produk(id)
);

-- Tabel Karyawan
CREATE TABLE karyawan (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    nama VARCHAR(100) NOT NULL,
    jabatan VARCHAR(50),
    departemen VARCHAR(50),
    gaji DECIMAL(12,2),
    tanggal_masuk DATE,
    manager_id INTEGER,
    FOREIGN KEY (manager_id) REFERENCES karyawan(id)
);

-- ============================================
-- ISI DATA SAMPLE
-- ============================================

INSERT INTO kategori (nama_kategori, deskripsi) VALUES
('Elektronik', 'Perangkat elektronik dan gadget'),
('Pakaian', 'Baju, celana, dan aksesoris fashion'),
('Makanan', 'Makanan ringan dan minuman'),
('Buku', 'Buku fisik dan digital'),
('Olahraga', 'Peralatan dan perlengkapan olahraga');

INSERT INTO pelanggan (nama, email, kota, umur, jenis_kelamin, tanggal_daftar, status) VALUES
('Andi Pratama','andi@email.com','Jakarta',28,'Pria','2024-01-15','aktif'),
('Budi Santoso','budi@email.com','Bandung',35,'Pria','2024-02-20','aktif'),
('Citra Dewi','citra@email.com','Surabaya',24,'Wanita','2024-03-10','aktif'),
('Dina Rahma','dina@email.com','Jakarta',30,'Wanita','2024-01-25','nonaktif'),
('Eko Wijaya','eko@email.com','Medan',42,'Pria','2024-04-05','aktif'),
('Fitri Yani','fitri@email.com','Bandung',27,'Wanita','2024-05-12','aktif'),
('Gilang Ramadhan','gilang@email.com','Jakarta',31,'Pria','2024-03-18','aktif'),
('Hana Pertiwi','hana@email.com','Yogyakarta',29,'Wanita','2024-06-01','aktif'),
('Irfan Hakim','irfan@email.com','Surabaya',38,'Pria','2024-02-14','nonaktif'),
('Jasmine Putri','jasmine@email.com','Bali',26,'Wanita','2024-07-20','aktif'),
('Kurnia Sandi','kurnia@email.com','Semarang',33,'Pria','2024-04-22','aktif'),
('Lina Marlina','lina@email.com','Jakarta',45,'Wanita','2024-01-08','aktif'),
('Mega Sari',NULL,'Bandung',22,'Wanita','2024-08-15','aktif'),
('Nanda Putra','nanda@email.com','Medan',36,'Pria','2024-05-30','aktif'),
('Oscar Tan','oscar@email.com','Jakarta',29,'Pria','2024-09-01','aktif');

INSERT INTO produk (nama_produk, kategori_id, harga, stok, berat_gram, tanggal_masuk) VALUES
('Laptop Gaming ASUS',1,15000000,10,2500,'2024-01-01'),
('Smartphone Samsung S24',1,12000000,25,200,'2024-02-15'),
('Headphone Sony WH-1000',1,3500000,30,250,'2024-01-20'),
('Kaos Polos Premium',2,89000,200,200,'2024-03-01'),
('Celana Jeans Slim Fit',2,250000,150,500,'2024-02-10'),
('Jaket Hoodie Unisex',2,175000,100,400,'2024-04-05'),
('Coklat Batang Import',3,35000,500,100,'2024-01-15'),
('Kopi Arabica 250gr',3,85000,300,250,'2024-03-20'),
('Buku Pemrograman Python',4,120000,50,350,'2024-02-28'),
('Novel Best Seller 2024',4,95000,80,300,'2024-05-10'),
('Sepatu Lari Nike',5,1200000,40,600,'2024-01-25'),
('Matras Yoga Premium',5,350000,60,1500,'2024-04-15'),
('Tablet iPad Air',1,9500000,15,460,'2024-06-01'),
('Kemeja Batik Modern',2,195000,120,250,'2024-03-15'),
('Teh Hijau Organik',3,45000,400,150,'2024-02-05');

INSERT INTO pesanan (pelanggan_id, tanggal_pesan, total_harga, status_pesanan, metode_bayar, alamat_kirim) VALUES
(1,'2024-06-01',15089000,'selesai','Transfer Bank','Jl. Sudirman No.10, Jakarta'),
(2,'2024-06-02',12250000,'selesai','E-Wallet','Jl. Braga No.5, Bandung'),
(3,'2024-06-03',3585000,'dikirim','COD','Jl. Tunjungan No.8, Surabaya'),
(1,'2024-06-05',250000,'selesai','E-Wallet','Jl. Sudirman No.10, Jakarta'),
(5,'2024-06-07',1200000,'selesai','Transfer Bank','Jl. Sisingamangaraja No.15, Medan'),
(6,'2024-06-10',175000,'pending','E-Wallet','Jl. Dago No.20, Bandung'),
(7,'2024-06-12',9620000,'selesai','Kartu Kredit','Jl. Gatot Subroto No.30, Jakarta'),
(8,'2024-06-15',445000,'dikirim','Transfer Bank','Jl. Malioboro No.12, Yogyakarta'),
(3,'2024-06-18',12095000,'selesai','Kartu Kredit','Jl. Tunjungan No.8, Surabaya'),
(10,'2024-06-20',350000,'pending','COD','Jl. Sunset Road No.7, Bali'),
(11,'2024-06-22',15120000,'selesai','Transfer Bank','Jl. Pandanaran No.5, Semarang'),
(1,'2024-06-25',85000,'selesai','E-Wallet','Jl. Sudirman No.10, Jakarta'),
(12,'2024-07-01',24500000,'selesai','Kartu Kredit','Jl. Kemang No.18, Jakarta'),
(2,'2024-07-03',350000,'dikirim','E-Wallet','Jl. Braga No.5, Bandung'),
(14,'2024-07-05',1289000,'selesai','Transfer Bank','Jl. SM Raja No.22, Medan'),
(15,'2024-07-08',3500000,'pending','Kartu Kredit','Jl. Rasuna Said No.9, Jakarta'),
(7,'2024-07-10',250000,'selesai','COD','Jl. Gatot Subroto No.30, Jakarta'),
(6,'2024-07-12',12000000,'dikirim','Transfer Bank','Jl. Dago No.20, Bandung'),
(8,'2024-07-15',95000,'selesai','E-Wallet','Jl. Malioboro No.12, Yogyakarta'),
(3,'2024-07-18',1550000,'selesai','Kartu Kredit','Jl. Tunjungan No.8, Surabaya');

INSERT INTO detail_pesanan (pesanan_id, produk_id, jumlah, harga_satuan, subtotal) VALUES
(1,1,1,15000000,15000000),(1,4,1,89000,89000),(2,2,1,12000000,12000000),
(2,5,1,250000,250000),(3,3,1,3500000,3500000),(3,8,1,85000,85000),
(4,5,1,250000,250000),(5,11,1,1200000,1200000),(6,6,1,175000,175000),
(7,13,1,9500000,9500000),(7,9,1,120000,120000),(8,12,1,350000,350000),
(8,10,1,95000,95000),(9,2,1,12000000,12000000),(9,10,1,95000,95000),
(10,12,1,350000,350000),(11,1,1,15000000,15000000),(11,9,1,120000,120000),
(12,8,1,85000,85000),(13,1,1,15000000,15000000),(13,13,1,9500000,9500000),
(14,12,1,350000,350000),(15,11,1,1200000,1200000),(15,4,1,89000,89000),
(16,3,1,3500000,3500000),(17,5,1,250000,250000),(18,2,1,12000000,12000000),
(19,10,1,95000,95000),(20,11,1,1200000,1200000),(20,12,1,350000,350000);

INSERT INTO karyawan (nama, jabatan, departemen, gaji, tanggal_masuk, manager_id) VALUES
('Rudi Hartono','CEO','Direksi',50000000,'2020-01-01',NULL),
('Sari Indah','CTO','Teknologi',40000000,'2020-03-15',1),
('Tono Sugiarto','CFO','Keuangan',40000000,'2020-02-01',1),
('Umar Faruq','Lead Developer','Teknologi',25000000,'2021-06-01',2),
('Vina Amelia','Senior Developer','Teknologi',20000000,'2021-09-15',4),
('Wahyu Prabowo','Junior Developer','Teknologi',12000000,'2023-01-10',4),
('Xena Putri','Finance Manager','Keuangan',22000000,'2021-04-01',3),
('Yogi Saputra','Accountant','Keuangan',15000000,'2022-07-20',7),
('Zahra Nabila','Marketing Manager','Marketing',22000000,'2021-05-10',1),
('Arif Budiman','Marketing Staff','Marketing',10000000,'2023-03-01',9),
('Bella Safitri','HR Manager','HRD',22000000,'2021-08-15',1),
('Chandra Wijaya','HR Staff','HRD',10000000,'2023-06-01',11);
💡
Tips DBeaver: Untuk menjalankan query, highlight (blok) bagian yang ingin dijalankan → tekan Ctrl + Enter (Mac: Cmd + Enter)
1

Fondasi SQL

SELECT · WHERE · ORDER BY · LIMIT

📖 Apa itu SQL?
SQL (Structured Query Language) = bahasa untuk berkomunikasi dengan database. Bayangkan database seperti lemari arsip raksasa, dan SQL adalah perintah untuk mengelola isinya.
DQL (Query)
SELECT
Membaca data
DML (Manipulasi)
INSERT, UPDATE, DELETE
Mengubah data
DDL (Definisi)
CREATE, ALTER, DROP
Ubah struktur tabel
DCL (Kontrol)
GRANT, REVOKE
Hak akses
1.1 SELECT — Mengambil Data
Perintah paling dasar dan paling sering dipakai. Fungsinya: mengambil data dari tabel.
Dasar SELECT
-- Ambil SEMUA kolom
SELECT * FROM pelanggan;

-- Ambil kolom tertentu saja
SELECT nama, email, kota FROM pelanggan;

-- Beri nama alias pada kolom
SELECT 
    nama AS nama_pelanggan, 
    email AS alamat_email
FROM pelanggan;
💡
* artinya "semua kolom". AS untuk memberi alias. SQL tidak case-sensitive.

DISTINCT — Hapus Duplikat

-- Lihat kota apa saja yang ada (tanpa duplikat)
SELECT DISTINCT kota FROM pelanggan;

-- Kombinasi unik kota dan jenis kelamin
SELECT DISTINCT kota, jenis_kelamin FROM pelanggan;
1.2 WHERE — Menyaring Data
WHERE adalah filter. Tentukan syarat data mana yang ingin ditampilkan.

Operator Perbandingan

Operator Arti Contoh
= Sama dengan kota = 'Jakarta'
!= atau <> Tidak sama kota != 'Jakarta'
> < Lebih besar / kecil umur > 30
BETWEEN Antara X dan Y harga BETWEEN 100000 AND 500000
Contoh WHERE
-- Pelanggan di Jakarta
SELECT * FROM pelanggan WHERE kota = 'Jakarta';

-- Umur lebih dari 30
SELECT * FROM pelanggan WHERE umur > 30;

-- Harga antara 100rb - 500rb
SELECT nama_produk, harga FROM produk 
WHERE harga BETWEEN 100000 AND 500000;

AND, OR, NOT — Logika Kombinasi

-- Wanita di Jakarta
SELECT * FROM pelanggan 
WHERE kota = 'Jakarta' AND jenis_kelamin = 'Wanita';

-- Dari Jakarta ATAU Bandung
SELECT * FROM pelanggan 
WHERE kota = 'Jakarta' OR kota = 'Bandung';

-- Kombinasi (SELALU pakai tanda kurung!)
SELECT * FROM pelanggan 
WHERE (kota = 'Jakarta' OR kota = 'Bandung') AND umur > 25;
⚠️
Selalu pakai tanda kurung () saat gabungkan AND dan OR agar hasilnya benar!

IN — Cek dari Daftar

-- Lebih ringkas daripada banyak OR
SELECT * FROM pelanggan 
WHERE kota IN ('Jakarta', 'Bandung', 'Surabaya');

LIKE — Pencarian Pola Teks

Pattern Arti Contoh Match
'A%' Diawali A Andi, Ahmad
'%Putr%' Mengandung "Putr" Jasmine Putri, Nanda Putra
'Di__' Di + tepat 2 karakter Dina, Didi
SELECT * FROM pelanggan WHERE nama LIKE 'A%';      -- Diawali A
SELECT * FROM pelanggan WHERE nama LIKE '%Putr%';  -- Mengandung Putr

IS NULL — Cek Data Kosong

SELECT * FROM pelanggan WHERE email IS NULL;      -- Tanpa email
SELECT * FROM pelanggan WHERE email IS NOT NULL;  -- Punya email
🚫
Jangan gunakan = NULL. Selalu pakai IS NULL / IS NOT NULL
1.3 ORDER BY & LIMIT — Urut & Batasi
Mengurutkan & Membatasi
-- Urutkan nama A-Z (ASC = naik, default)
SELECT * FROM pelanggan ORDER BY nama ASC;

-- Produk termahal dulu (DESC = turun)
SELECT nama_produk, harga FROM produk ORDER BY harga DESC;

-- Ambil 5 data pertama
SELECT * FROM pelanggan LIMIT 5;

-- 3 produk termahal
SELECT nama_produk, harga FROM produk 
ORDER BY harga DESC LIMIT 3;

-- Paging: skip 5, ambil 5
SELECT * FROM pelanggan LIMIT 5 OFFSET 5;
🏋️ Latihan Hari 1
-- 1. Tampilkan nama & email semua pelanggan wanita
-- 2. Produk harga 50rb-500rb, urutkan termurah
-- 3. 5 pelanggan termuda
-- 4. Nama mengandung 'i' dan tinggal di Jakarta
-- 5. Semua kota unik, urutkan A-Z
-- 6. Stok < 30 atau harga > 5 juta
-- 7. Pelanggan yang tidak punya email
-- 8. Pesanan 'selesai' dengan metode 'E-Wallet'
✅ Jawaban
-- 1.
SELECT nama, email FROM pelanggan WHERE jenis_kelamin = 'Wanita';
-- 2.
SELECT nama_produk, harga FROM produk 
WHERE harga BETWEEN 50000 AND 500000 ORDER BY harga ASC;
-- 3.
SELECT nama, umur FROM pelanggan ORDER BY umur ASC LIMIT 5;
-- 4.
SELECT * FROM pelanggan WHERE nama LIKE '%i%' AND kota = 'Jakarta';
-- 5.
SELECT DISTINCT kota FROM pelanggan ORDER BY kota ASC;
-- 6.
SELECT nama_produk, stok, harga FROM produk 
WHERE stok < 30 OR harga > 5000000;
-- 7.
SELECT * FROM pelanggan WHERE email IS NULL;
-- 8.
SELECT * FROM pesanan 
WHERE status_pesanan = 'selesai' AND metode_bayar = 'E-Wallet';
2

Fungsi Agregat & GROUP BY

COUNT · SUM · AVG · MIN · MAX · GROUP BY · HAVING

2.1 Fungsi Agregat — Menghitung Data
Fungsi agregat bekerja pada sekumpulan baris dan menghasilkan satu nilai.
COUNT
Hitung baris
SUM
Jumlahkan
AVG
Rata-rata
MIN
Terkecil
MAX
Terbesar
Fungsi Agregat
SELECT COUNT(*) AS total_pelanggan FROM pelanggan;
SELECT COUNT(email) AS punya_email FROM pelanggan;  -- NULL tak dihitung
SELECT SUM(harga * stok) AS nilai_inventaris FROM produk;
SELECT AVG(umur) AS rata_rata_umur FROM pelanggan;
SELECT MIN(harga) AS termurah, MAX(harga) AS termahal FROM produk;
2.2 GROUP BY — Mengelompokkan Data
Bayangkan setumpuk kartu nama. GROUP BY kota = pisahkan per kota, lalu hitung tiap tumpukan.
Group By
-- Jumlah pelanggan per kota
SELECT kota, COUNT(*) AS jumlah 
FROM pelanggan GROUP BY kota;

-- Total penjualan per metode bayar
SELECT metode_bayar, COUNT(*) AS transaksi, SUM(total_harga) AS total
FROM pesanan GROUP BY metode_bayar;

-- Pelanggan dan total belanjanya
SELECT pelanggan_id, COUNT(*) AS jumlah_pesanan, SUM(total_harga) AS total
FROM pesanan GROUP BY pelanggan_id ORDER BY total DESC;
⚠️
Kolom di SELECT yang bukan fungsi agregat HARUS ada di GROUP BY!
2.3 HAVING — Filter Setelah Grouping
WHERE = filter baris sebelum group. HAVING = filter setelah group.
HAVING
-- Kota dengan > 2 pelanggan
SELECT kota, COUNT(*) AS jumlah
FROM pelanggan GROUP BY kota HAVING COUNT(*) > 2;

-- Kombinasi WHERE + HAVING
SELECT pelanggan_id, SUM(total_harga) AS total
FROM pesanan 
WHERE status_pesanan = 'selesai'      -- filter BARIS dulu
GROUP BY pelanggan_id 
HAVING SUM(total_harga) > 5000000   -- filter GRUP
ORDER BY total DESC;
⚡ Urutan Eksekusi SQL (Penting!)
FROM WHERE GROUP BY HAVING SELECT ORDER BY LIMIT
🏋️ Latihan Hari 2
-- 1. Total pesanan dan total pendapatan keseluruhan
-- 2. Jumlah produk per kategori
-- 3. Total penjualan per bulan (hint: SUBSTR pada tanggal)
-- 4. Metode bayar dengan total > 15 juta
-- 5. Kota dengan rata-rata umur > 30
-- 6. Top 3 pelanggan by total belanja
✅ Jawaban
-- 1.
SELECT COUNT(*) AS total_pesanan, SUM(total_harga) AS pendapatan FROM pesanan;
-- 2.
SELECT kategori_id, COUNT(*) AS jumlah FROM produk GROUP BY kategori_id;
-- 3.
SELECT SUBSTR(tanggal_pesan,1,7) AS bulan, SUM(total_harga) AS total
FROM pesanan GROUP BY SUBSTR(tanggal_pesan,1,7) ORDER BY bulan;
-- 4.
SELECT metode_bayar, SUM(total_harga) AS total
FROM pesanan GROUP BY metode_bayar HAVING SUM(total_harga) > 15000000;
-- 5.
SELECT kota, AVG(umur) AS rata_umur
FROM pelanggan GROUP BY kota HAVING AVG(umur) > 30;
-- 6.
SELECT pelanggan_id, SUM(total_harga) AS total
FROM pesanan GROUP BY pelanggan_id ORDER BY total DESC LIMIT 3;
3

JOIN — Menggabungkan Tabel

INNER JOIN · LEFT JOIN · SELF JOIN · Multi-table

Konsep JOIN — Visualisasi
JOIN mencocokkan data dari 2+ tabel berdasarkan kolom yang sama (biasanya ID/Foreign Key).
Jenis Hasil Kapan Pakai?
INNER JOIN Hanya data yang cocok di kedua tabel Saat hanya butuh data yang ada relasi
LEFT JOIN Semua data kiri + cocok kanan (NULL jika tidak ada) Saat mau lihat semua data kiri, termasuk yang tidak punya relasi
SELF JOIN Gabungkan tabel dengan dirinya sendiri Data hierarki (karyawan → manager)
CROSS JOIN Semua kombinasi (Cartesian product) Jarang, untuk referensi silang
3.1 INNER JOIN — Data yang Cocok
INNER JOIN
-- Pesanan + nama pelanggannya
SELECT p.id, pel.nama, p.tanggal_pesan, p.total_harga
FROM pesanan p
INNER JOIN pelanggan pel ON p.pelanggan_id = pel.id;

-- Produk + nama kategorinya
SELECT pr.nama_produk, k.nama_kategori, pr.harga
FROM produk pr
INNER JOIN kategori k ON pr.kategori_id = k.id;

-- Detail pesanan lengkap: pesanan + pelanggan + produk
SELECT pes.id AS no_pesanan, pel.nama, pro.nama_produk, dp.jumlah, dp.subtotal
FROM detail_pesanan dp
INNER JOIN pesanan pes ON dp.pesanan_id = pes.id
INNER JOIN pelanggan pel ON pes.pelanggan_id = pel.id
INNER JOIN produk pro ON dp.produk_id = pro.id;
💡
Gunakan alias singkat (p, pel, pr, k) agar query lebih mudah dibaca.
3.2 LEFT JOIN & SELF JOIN
LEFT JOIN — Semua data kiri
-- Semua pelanggan + pesanan (termasuk yg belum pesan)
SELECT pel.nama, pes.id, pes.total_harga
FROM pelanggan pel
LEFT JOIN pesanan pes ON pel.id = pes.pelanggan_id;

-- Cari pelanggan yang BELUM PERNAH pesan
SELECT pel.nama, pel.email
FROM pelanggan pel
LEFT JOIN pesanan pes ON pel.id = pes.pelanggan_id
WHERE pes.id IS NULL;
SELF JOIN — Hierarki dalam 1 tabel
-- Karyawan dan nama managernya
SELECT k.nama AS karyawan, k.jabatan, m.nama AS manager
FROM karyawan k
LEFT JOIN karyawan m ON k.manager_id = m.id;
3.3 JOIN + Agregat — Kombinasi Powerful
JOIN + GROUP BY
-- Total belanja setiap pelanggan (dengan nama)
SELECT pel.nama, COUNT(pes.id) AS jml_pesanan,
    COALESCE(SUM(pes.total_harga), 0) AS total_belanja
FROM pelanggan pel
LEFT JOIN pesanan pes ON pel.id = pes.pelanggan_id
GROUP BY pel.id, pel.nama ORDER BY total_belanja DESC;

-- Penjualan per kategori
SELECT k.nama_kategori, SUM(dp.subtotal) AS total_pendapatan
FROM kategori k
JOIN produk pr ON k.id = pr.kategori_id
JOIN detail_pesanan dp ON pr.id = dp.produk_id
GROUP BY k.nama_kategori ORDER BY total_pendapatan DESC;
🏋️ Latihan Hari 3
-- 1. Semua pesanan + nama pelanggan + kota
-- 2. Pelanggan yang belum pernah pesan
-- 3. Detail pesanan lengkap (pelanggan, produk, kategori, subtotal)
-- 4. Total pendapatan per kategori produk
-- 5. Karyawan + nama manager-nya
-- 6. Produk yang belum pernah dipesan
✅ Jawaban
-- 1.
SELECT pes.id, pel.nama, pel.kota, pes.tanggal_pesan, pes.total_harga
FROM pesanan pes JOIN pelanggan pel ON pes.pelanggan_id = pel.id;
-- 2.
SELECT pel.nama FROM pelanggan pel
LEFT JOIN pesanan pes ON pel.id = pes.pelanggan_id
WHERE pes.id IS NULL;
-- 3.
SELECT pel.nama, k.nama_kategori, pr.nama_produk, dp.jumlah, dp.subtotal
FROM detail_pesanan dp
JOIN pesanan pes ON dp.pesanan_id = pes.id
JOIN pelanggan pel ON pes.pelanggan_id = pel.id
JOIN produk pr ON dp.produk_id = pr.id
JOIN kategori k ON pr.kategori_id = k.id;
-- 4.
SELECT k.nama_kategori, SUM(dp.subtotal) AS total
FROM kategori k JOIN produk pr ON k.id = pr.kategori_id
JOIN detail_pesanan dp ON pr.id = dp.produk_id
GROUP BY k.nama_kategori ORDER BY total DESC;
-- 5.
SELECT k.nama AS karyawan, k.jabatan, COALESCE(m.nama,'-') AS manager
FROM karyawan k LEFT JOIN karyawan m ON k.manager_id = m.id;
-- 6.
SELECT pr.nama_produk FROM produk pr
LEFT JOIN detail_pesanan dp ON pr.id = dp.produk_id
WHERE dp.id IS NULL;
4

Subquery & Fungsi Bawaan

Subquery · CASE WHEN · String · Date · COALESCE

4.1 Subquery — Query dalam Query
Subquery = pertanyaan bersarang. Contoh: "Siapa pelanggan yang belanja lebih dari rata-rata?"
Subquery di WHERE
-- Produk di atas harga rata-rata
SELECT nama_produk, harga FROM produk
WHERE harga > (SELECT AVG(harga) FROM produk);

-- Pelanggan yang pernah pesan produk elektronik
SELECT DISTINCT pel.nama FROM pelanggan pel
WHERE pel.id IN (
    SELECT pes.pelanggan_id FROM pesanan pes
    JOIN detail_pesanan dp ON pes.id = dp.pesanan_id
    JOIN produk pr ON dp.produk_id = pr.id
    WHERE pr.kategori_id = 1
);
Subquery di FROM (Derived Table)
-- Rata-rata jumlah pesanan per pelanggan
SELECT AVG(jumlah_pesanan) AS rata_rata
FROM (
    SELECT pelanggan_id, COUNT(*) AS jumlah_pesanan
    FROM pesanan GROUP BY pelanggan_id
) AS ringkasan;
EXISTS — Cek keberadaan
-- Pelanggan yang PERNAH pesan
SELECT nama FROM pelanggan pel
WHERE EXISTS (SELECT 1 FROM pesanan pes WHERE pes.pelanggan_id = pel.id);

-- Pelanggan yang BELUM PERNAH pesan
SELECT nama FROM pelanggan pel
WHERE NOT EXISTS (SELECT 1 FROM pesanan pes WHERE pes.pelanggan_id = pel.id);
4.2 CASE WHEN — Logika Kondisional
Seperti IF-ELSE di pemrograman. Buat kolom kategori baru secara dinamis.
CASE WHEN
-- Kategorisasi harga produk
SELECT nama_produk, harga,
    CASE 
        WHEN harga < 100000 THEN 'Murah'
        WHEN harga BETWEEN 100000 AND 1000000 THEN 'Menengah'
        WHEN harga BETWEEN 1000001 AND 10000000 THEN 'Mahal'
        ELSE 'Premium'
    END AS level_harga
FROM produk ORDER BY harga;

-- Pivot manual: hitung per status
SELECT 
    COUNT(CASE WHEN status_pesanan = 'selesai' THEN 1 END) AS selesai,
    COUNT(CASE WHEN status_pesanan = 'dikirim' THEN 1 END) AS dikirim,
    COUNT(CASE WHEN status_pesanan = 'pending' THEN 1 END) AS pending
FROM pesanan;
4.3 Fungsi String, Angka, Tanggal
Fungsi String
SELECT UPPER(nama), LOWER(email), LENGTH(nama) FROM pelanggan;
SELECT SUBSTR(nama,1,5) AS lima_huruf FROM pelanggan;  -- Potong teks
SELECT REPLACE(email,'@email.com','@gmail.com') FROM pelanggan;
SELECT nama || ' (' || kota || ')' AS info FROM pelanggan;  -- Gabung teks
Fungsi Angka & Tanggal
-- PPN 11%
SELECT nama_produk, harga, ROUND(harga * 1.11, 0) AS setelah_ppn FROM produk;

-- Ambil bagian tanggal
SELECT STRFTIME('%Y', tanggal_pesan) AS tahun,
       STRFTIME('%m', tanggal_pesan) AS bulan
FROM pesanan;

-- COALESCE: ganti NULL dengan default
SELECT nama, COALESCE(email, 'Tidak ada') AS email FROM pelanggan;
🏋️ Latihan Hari 4
-- 1. Produk di atas harga rata-rata + kategorinya
-- 2. Kolom "level_harga" (Murah/Sedang/Mahal) tiap produk
-- 3. Pelanggan yang pernah beli produk kategori "Pakaian"
-- 4. Hitung jumlah pelanggan per generasi (Gen Z/Millennial/Gen X)
-- 5. Nama pelanggan, email (NULL jadi 'N/A'), panjang nama
✅ Jawaban
-- 1.
SELECT pr.nama_produk, k.nama_kategori, pr.harga FROM produk pr
JOIN kategori k ON pr.kategori_id = k.id
WHERE pr.harga > (SELECT AVG(harga) FROM produk);
-- 2.
SELECT nama_produk, harga, CASE WHEN harga < 100000 THEN 'Murah'
    WHEN harga <= 1000000 THEN 'Sedang' ELSE 'Mahal' END AS level
FROM produk ORDER BY harga;
-- 3.
SELECT DISTINCT pel.nama FROM pelanggan pel
WHERE pel.id IN (SELECT pes.pelanggan_id FROM pesanan pes
    JOIN detail_pesanan dp ON pes.id = dp.pesanan_id
    JOIN produk pr ON dp.produk_id = pr.id WHERE pr.kategori_id = 2);
-- 4.
SELECT gen, COUNT(*) FROM (SELECT CASE
    WHEN umur < 25 THEN 'Gen Z' WHEN umur <= 34 THEN 'Millennial'
    ELSE 'Gen X' END AS gen FROM pelanggan) GROUP BY gen;
-- 5.
SELECT nama, COALESCE(email,'N/A') AS email, LENGTH(nama) FROM pelanggan;
5

DML & Transaksi

INSERT · UPDATE · DELETE · BEGIN · COMMIT · ROLLBACK

5.1 INSERT — Tambah Data
INSERT
-- Insert satu baris
INSERT INTO pelanggan (nama, email, kota, umur, jenis_kelamin, tanggal_daftar) 
VALUES ('Putri Handayani', 'putri@email.com', 'Makassar', 25, 'Wanita', '2024-10-01');

-- Insert banyak sekaligus
INSERT INTO pelanggan (nama, email, kota, umur, jenis_kelamin, tanggal_daftar) VALUES
('Rizky Aditya', 'rizky@email.com', 'Palembang', 32, 'Pria', '2024-10-05'),
('Sinta Dewi', 'sinta@email.com', 'Bali', 28, 'Wanita', '2024-10-10');
5.2 UPDATE — Ubah Data
🚫
SELALU pakai WHERE saat UPDATE! Tanpa WHERE, SEMUA baris akan berubah.
UPDATE
-- Update satu kolom
UPDATE pelanggan SET kota = 'Tangerang' WHERE id = 1;

-- Update beberapa kolom
UPDATE pelanggan SET email = 'andi.new@email.com', status = 'nonaktif' WHERE id = 1;

-- Update berdasarkan kondisi
UPDATE produk SET harga = harga * 1.1 WHERE kategori_id = 3;  -- Harga makanan +10%
Best practice: SELECT dulu → UPDATE → SELECT lagi untuk verifikasi
5.3 DELETE — Hapus Data
🚫
SELALU pakai WHERE saat DELETE! Tanpa WHERE, SEMUA data terhapus.
DELETE
DELETE FROM pelanggan WHERE id = 16;
-- Tips aman: cek dulu!
SELECT COUNT(*) FROM pelanggan WHERE status = 'nonaktif';  -- cek jumlah
SELECT * FROM pelanggan WHERE status = 'nonaktif';          -- lihat data
DELETE FROM pelanggan WHERE status = 'nonaktif';              -- baru hapus
5.4 TRANSACTION — Operasi Aman
Memastikan serangkaian operasi semuanya berhasil atau semuanya dibatalkan.
TRANSACTION
BEGIN TRANSACTION;
    INSERT INTO pesanan (...) VALUES (...);    -- Buat pesanan
    INSERT INTO detail_pesanan (...) VALUES (...); -- Tambah detail
    UPDATE produk SET stok = stok - 1 WHERE id = 1;  -- Kurangi stok
COMMIT;   -- Simpan semua perubahan
-- Jika error → ROLLBACK;  (batalkan semua)
6

Advanced SQL

VIEW · INDEX · CTE · Window Functions

6.1 VIEW — Tabel Virtual
View = query yang disimpan sebagai tabel virtual. Bisa dipakai ulang kapan saja.
VIEW
-- Buat view: ringkasan pelanggan
CREATE VIEW v_ringkasan_pelanggan AS
SELECT pel.nama, pel.kota, COUNT(pes.id) AS total_pesanan,
    COALESCE(SUM(pes.total_harga), 0) AS total_belanja
FROM pelanggan pel
LEFT JOIN pesanan pes ON pel.id = pes.pelanggan_id
GROUP BY pel.id, pel.nama, pel.kota;

-- Pakai view seperti tabel biasa!
SELECT * FROM v_ringkasan_pelanggan WHERE total_belanja > 10000000;
6.2 INDEX — Percepat Query
Seperti indeks di belakang buku — bantu database temukan data lebih cepat.
INDEX
CREATE INDEX idx_pelanggan_kota ON pelanggan(kota);
CREATE INDEX idx_pesanan_pelanggan ON pesanan(pelanggan_id);
CREATE INDEX idx_pesanan_tanggal ON pesanan(tanggal_pesan);
💡
Pakai index di: kolom WHERE, kolom JOIN (FK), kolom ORDER BY. Jangan di: tabel kecil, kolom yang sering UPDATE.
6.3 CTE — Common Table Expression
CTE = "tabel sementara" untuk 1 query. Pecah query kompleks jadi langkah-langkah yang mudah dibaca.
CTE (WITH Clause)
-- Pelanggan VIP (belanja > 10 juta)
WITH pelanggan_vip AS (
    SELECT pelanggan_id, SUM(total_harga) AS total
    FROM pesanan GROUP BY pelanggan_id
    HAVING SUM(total_harga) > 10000000
)
SELECT pel.nama, pel.kota, vip.total
FROM pelanggan_vip vip
JOIN pelanggan pel ON vip.pelanggan_id = pel.id;
6.4 Window Functions — Analisis Lanjut
Kalkulasi pada sekumpulan baris tanpa menggabungkannya (beda dengan GROUP BY).
Window Functions
-- ROW_NUMBER: nomor urut
SELECT nama_produk, harga,
    ROW_NUMBER() OVER (ORDER BY harga DESC) AS ranking
FROM produk;

-- PARTITION BY: ranking PER KATEGORI
SELECT k.nama_kategori, pr.nama_produk, pr.harga,
    ROW_NUMBER() OVER (PARTITION BY pr.kategori_id ORDER BY pr.harga DESC) AS ranking
FROM produk pr JOIN kategori k ON pr.kategori_id = k.id;

-- Running total (akumulasi)
SELECT tanggal_pesan, total_harga,
    SUM(total_harga) OVER (ORDER BY tanggal_pesan) AS akumulasi
FROM pesanan WHERE status_pesanan = 'selesai';

-- LAG: bandingkan dengan baris sebelumnya
SELECT tanggal_pesan, total_harga,
    LAG(total_harga) OVER (ORDER BY tanggal_pesan) AS sebelumnya,
    total_harga - LAG(total_harga) OVER (ORDER BY tanggal_pesan) AS selisih
FROM pesanan WHERE status_pesanan = 'selesai';
Fungsi Kegunaan
ROW_NUMBER() Nomor urut unik
RANK() Ranking (bisa skip angka jika sama)
DENSE_RANK() Ranking tanpa skip
LAG() / LEAD() Nilai baris sebelum/sesudah
SUM() OVER() Running total
NTILE(n) Bagi ke n grup sama rata
7

Studi Kasus Nyata

Analisis Penjualan · Customer Analytics · Laporan Bisnis

7.1 Dashboard Penjualan
Penjualan Harian
SELECT tanggal_pesan, COUNT(*) AS pesanan,
    SUM(total_harga) AS total, AVG(total_harga) AS rata_rata
FROM pesanan WHERE status_pesanan = 'selesai'
GROUP BY tanggal_pesan ORDER BY tanggal_pesan;
Perbandingan Bulanan (Month-over-Month)
WITH bulanan AS (
    SELECT SUBSTR(tanggal_pesan,1,7) AS bulan, SUM(total_harga) AS total
    FROM pesanan GROUP BY SUBSTR(tanggal_pesan,1,7)
)
SELECT bulan, total,
    LAG(total) OVER (ORDER BY bulan) AS bulan_lalu,
    ROUND((total - LAG(total) OVER (ORDER BY bulan)) * 100.0 / 
        LAG(total) OVER (ORDER BY bulan), 2) AS growth_persen
FROM bulanan;
7.2 Customer Analytics — Segmentasi RFM
Segmentasi Pelanggan
WITH rfm AS (
    SELECT pel.nama, pel.kota,
        COUNT(pes.id) AS frequency,
        SUM(pes.total_harga) AS monetary,
        MAX(pes.tanggal_pesan) AS last_order
    FROM pelanggan pel
    LEFT JOIN pesanan pes ON pel.id = pes.pelanggan_id
    GROUP BY pel.id, pel.nama, pel.kota
)
SELECT *, CASE 
    WHEN monetary > 15000000 AND frequency >= 3 THEN 'VIP'
    WHEN monetary > 5000000 THEN 'Premium'
    WHEN monetary > 0 THEN 'Regular'
    ELSE 'Inactive'
END AS segmen FROM rfm ORDER BY monetary DESC;
7.3 Analisis Produk & Cross-Sell
Produk yang Sering Dibeli Bersamaan
SELECT p1.nama_produk AS produk_1, p2.nama_produk AS produk_2,
    COUNT(*) AS dibeli_bersamaan
FROM detail_pesanan dp1
JOIN detail_pesanan dp2 ON dp1.pesanan_id = dp2.pesanan_id 
    AND dp1.produk_id < dp2.produk_id
JOIN produk p1 ON dp1.produk_id = p1.id
JOIN produk p2 ON dp2.produk_id = p2.id
GROUP BY p1.nama_produk, p2.nama_produk
ORDER BY dibeli_bersamaan DESC;
7.4 Laporan Keuangan
Kontribusi Per Metode Bayar (%)
WITH total AS (
    SELECT SUM(total_harga) AS grand_total 
    FROM pesanan WHERE status_pesanan = 'selesai'
)
SELECT metode_bayar, COUNT(*) AS transaksi,
    SUM(total_harga) AS pendapatan,
    ROUND(SUM(total_harga)*100.0/(SELECT grand_total FROM total),2) AS persen
FROM pesanan WHERE status_pesanan = 'selesai'
GROUP BY metode_bayar ORDER BY pendapatan DESC;
7.5 Executive Dashboard — Semua dalam Satu Query
Executive Summary
WITH 
penjualan AS (
    SELECT COUNT(*) AS total_pesanan, SUM(total_harga) AS revenue,
        AVG(total_harga) AS avg_order, COUNT(DISTINCT pelanggan_id) AS pelanggan_aktif
    FROM pesanan WHERE status_pesanan = 'selesai'
),
top_produk AS (
    SELECT pr.nama_produk, SUM(dp.subtotal) AS rev
    FROM detail_pesanan dp JOIN produk pr ON dp.produk_id = pr.id
    GROUP BY pr.id ORDER BY rev DESC LIMIT 1
),
top_pelanggan AS (
    SELECT pel.nama, SUM(pes.total_harga) AS belanja
    FROM pesanan pes JOIN pelanggan pel ON pes.pelanggan_id = pel.id
    GROUP BY pel.id ORDER BY belanja DESC LIMIT 1
)
SELECT p.total_pesanan, p.revenue, ROUND(p.avg_order,0) AS avg_order,
    p.pelanggan_aktif, tp.nama_produk AS produk_terlaris,
    tpl.nama AS pelanggan_terbaik, tpl.belanja
FROM penjualan p CROSS JOIN top_produk tp CROSS JOIN top_pelanggan tpl;
📋 SQL Cheat Sheet
Referensi cepat — simpan & gunakan kapan saja
Urutan Penulisan SQL
SELECT    kolom yang ditampilkan
FROM      dari tabel mana
JOIN      gabungkan tabel lain
WHERE     filter baris
GROUP BY  kelompokkan
HAVING    filter kelompok
ORDER BY  urutkan
LIMIT     batasi jumlah
Urutan Eksekusi
FROMJOINWHEREGROUP BYHAVINGSELECTDISTINCTORDER BYLIMIT

Tips: Itulah kenapa WHERE
tidak bisa pakai alias dari SELECT
Operator & Pattern
=  !=  >  <  >=  <=  Perbandingan
AND  OR  NOT         Logika
IN (...)             Cek dari daftar
BETWEEN x AND y     Rentang nilai
IS NULL             Cek data kosong
LIKE 'A%'           Pola teks
Fungsi Agregat
COUNT(*)     Hitung semua baris
COUNT(col)   Hitung (skip NULL)
SUM(col)     Jumlahkan
AVG(col)     Rata-rata
MIN(col)     Terkecil
MAX(col)     Terbesar
JOIN Types
INNER JOIN   Cocok di kedua tabel
LEFT JOIN    Semua kiri + cocok kanan
SELF JOIN    Tabel dengan dirinya
CROSS JOIN   Semua kombinasi
Window Functions
ROW_NUMBER()   Nomor urut unik
RANK()         Ranking (skip)
DENSE_RANK()   Ranking (no skip)
LAG()/LEAD()   Baris sebelum/sesudah
SUM() OVER()   Running total
🎯 Tips Sukses Belajar SQL
1
Ketik sendiri — jangan copy-paste. Muscle memory sangat membantu.
2
Eksperimen — modifikasi query, lihat apa yang berubah.
3
Baca error — DBeaver memberi tahu letak kesalahan.
4
Pakai EXPLAIN — pahami cara kerja query dari dalam.
5
Konsisten — 4 jam fokus tiap hari lebih baik dari 12 jam sekali.
6
Buat project nyata — database untuk kasus yang kamu hadapi sehari-hari.
SQL Mastery — 7 Hari dari Nol Sampai Mahir
Dibuat oleh Miftah, 2026.