Panduan lengkap 30 hari β dari tidak tahu apa-apa tentang SQL hingga mampu menulis query analisis bisnis tingkat lanjut
belajar_sql.db
-- βββββββββββββββββββββββββββββββββββββββββββββ -- β DATABASE LATIHAN: SISTEM TOKO ONLINE β -- βββββββββββββββββββββββββββββββββββββββββββββ 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' ); CREATE TABLE kategori ( id INTEGER PRIMARY KEY AUTOINCREMENT, nama_kategori VARCHAR(50) NOT NULL, deskripsi TEXT ); 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) ); 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) ); 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) ); 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) ); -- DATA SAMPLE INSERT INTO kategori VALUES(1,'Elektronik','Gadget & elektronik'),(2,'Pakaian','Fashion'),(3,'Makanan','Snack & minuman'),(4,'Buku','Buku fisik & digital'),(5,'Olahraga','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 10, Jakarta'),(2,'2024-06-02',12250000,'selesai','E-Wallet','Jl. Braga 5, Bandung'), (3,'2024-06-03',3585000,'dikirim','COD','Jl. Tunjungan 8, Surabaya'),(1,'2024-06-05',250000,'selesai','E-Wallet','Jl. Sudirman 10, Jakarta'), (5,'2024-06-07',1200000,'selesai','Transfer Bank','Jl. SM Raja 15, Medan'),(6,'2024-06-10',175000,'pending','E-Wallet','Jl. Dago 20, Bandung'), (7,'2024-06-12',9620000,'selesai','Kartu Kredit','Jl. Gatot Subroto 30, Jakarta'),(8,'2024-06-15',445000,'dikirim','Transfer Bank','Jl. Malioboro 12, Yogyakarta'), (3,'2024-06-18',12095000,'selesai','Kartu Kredit','Jl. Tunjungan 8, Surabaya'),(10,'2024-06-20',350000,'pending','COD','Jl. Sunset Road 7, Bali'), (11,'2024-06-22',15120000,'selesai','Transfer Bank','Jl. Pandanaran 5, Semarang'),(1,'2024-06-25',85000,'selesai','E-Wallet','Jl. Sudirman 10, Jakarta'), (12,'2024-07-01',24500000,'selesai','Kartu Kredit','Jl. Kemang 18, Jakarta'),(2,'2024-07-03',350000,'dikirim','E-Wallet','Jl. Braga 5, Bandung'), (14,'2024-07-05',1289000,'selesai','Transfer Bank','Jl. SM Raja 22, Medan'),(15,'2024-07-08',3500000,'pending','Kartu Kredit','Jl. Rasuna Said 9, Jakarta'), (7,'2024-07-10',250000,'selesai','COD','Jl. Gatot Subroto 30, Jakarta'),(6,'2024-07-12',12000000,'dikirim','Transfer Bank','Jl. Dago 20, Bandung'), (8,'2024-07-15',95000,'selesai','E-Wallet','Jl. Malioboro 12, Yogyakarta'),(3,'2024-07-18',1550000,'selesai','Kartu Kredit','Jl. Tunjungan 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 Dev','Teknologi',25000000,'2021-06-01',2), ('Vina Amelia','Senior Dev','Teknologi',20000000,'2021-09-15',4),('Wahyu Prabowo','Junior Dev','Teknologi',12000000,'2023-01-10',4), ('Xena Putri','Finance Mgr','Keuangan',22000000,'2021-04-01',3),('Yogi Saputra','Accountant','Keuangan',15000000,'2022-07-20',7), ('Zahra Nabila','Marketing Mgr','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);
Ctrl+Enter (Mac: Cmd+Enter) untuk
menjalankan. Jalankan script di atas sekaligus.
Mengenal database, membaca data, filter, dan sorting
Konsep dasar Β· Tabel Β· Kolom Β· Baris Β· Relasi
| Istilah | Analogi | Contoh di Excel |
|---|---|---|
| Database | Lemari arsip | File workbook |
| Tabel | Laci/folder | Sheet |
| Kolom | Jenis info (Nama, Alamat) | Header kolom |
| Baris (Row) | Satu kartu data | Satu baris data |
| Primary Key (PK) | Nomor KTP (unik) | ID unik tiap baris |
| Foreign Key (FK) | Referensi ke tabel lain | VLOOKUP ke sheet lain |
pelanggan βββββββΊ pesanan βββββββΊ detail_pesanan β kategori βββββββΊ produk ββββββββββββββββ karyawan (self-reference: manager_id β karyawan.id)
SELECT * FROM pelanggan; dan tekan Ctrl+Enter.
Jika muncul 15 baris data, kamu siap!
Mengambil data Β· Kolom tertentu Β· AS alias Β· DISTINCT
SELECT = ambil/baca data dari tabel. Ini perintah yang
kamu pakai 90% dari waktu.
-- Ambil SEMUA kolom (* = semua) SELECT * FROM pelanggan; -- Ambil kolom tertentu saja (lebih efisien) SELECT nama, email, kota FROM pelanggan; -- Beri nama alias dengan AS SELECT nama AS nama_pelanggan, email AS alamat_email FROM pelanggan; -- Hitung kolom baru SELECT nama_produk, harga, stok, harga * stok AS nilai_total FROM produk; -- Gabung teks (concatenation) SELECT nama || ' - ' || kota AS info FROM pelanggan;
SELECT DISTINCT kota FROM pelanggan; -- Kota unik saja SELECT DISTINCT kota, jenis_kelamin FROM pelanggan; -- Kombinasi unik
1. Tampilkan semua data dari tabel produk 2. Tampilkan hanya nama dan harga produk 3. Tampilkan kota-kota unik pelanggan 4. Tampilkan nama produk dan harga setelah diskon 10% 5. Tampilkan info pelanggan format: "Nama (Kota)"
-- 1 SELECT * FROM produk; -- 2 SELECT nama_produk, harga FROM produk; -- 3 SELECT DISTINCT kota FROM pelanggan; -- 4 SELECT nama_produk, harga, harga*0.9 AS harga_diskon FROM produk; -- 5 SELECT nama||' ('||kota||')' AS info FROM pelanggan;
Operator = != > < BETWEEN
| Operator | Arti | Contoh |
|---|---|---|
= |
Sama dengan | kota = 'Jakarta' |
!= atau <> |
Tidak sama | kota != 'Jakarta' |
> < >=
<=
|
Perbandingan | umur > 30 |
BETWEEN x AND y |
Dalam rentang | harga BETWEEN 50000 AND 500000 |
SELECT * FROM pelanggan WHERE kota = 'Jakarta'; SELECT nama, umur FROM pelanggan WHERE umur > 30; SELECT nama_produk, harga FROM produk WHERE harga BETWEEN 100000 AND 500000; SELECT * FROM pesanan WHERE status_pesanan = 'selesai';
'Jakarta'. Angka tidak
perlu: 30
1. Pelanggan wanita 2. Produk harga di atas 1 juta 3. Pelanggan umur 25-35 4. Pesanan bukan status 'pending' 5. Produk stok kurang dari 30
-- 1 SELECT * FROM pelanggan WHERE jenis_kelamin='Wanita'; -- 2 SELECT * FROM produk WHERE harga>1000000; -- 3 SELECT * FROM pelanggan WHERE umur BETWEEN 25 AND 35; -- 4 SELECT * FROM pesanan WHERE status_pesanan!='pending'; -- 5 SELECT nama_produk,stok FROM produk WHERE stok<30;
AND Β· OR Β· NOT Β· IN Β· NOT IN
-- AND: kedua syarat harus benar SELECT * FROM pelanggan WHERE kota='Jakarta' AND jenis_kelamin='Wanita'; -- OR: salah satu benar cukup SELECT * FROM pelanggan WHERE kota='Jakarta' OR kota='Bandung'; -- β οΈ PENTING: pakai () saat gabung AND + OR SELECT * FROM pelanggan WHERE (kota='Jakarta' OR kota='Bandung') AND umur>25; -- IN: lebih ringkas dari banyak OR SELECT * FROM pelanggan WHERE kota IN('Jakarta','Bandung','Surabaya'); -- NOT IN: yang TIDAK di daftar SELECT * FROM pelanggan WHERE kota NOT IN('Jakarta','Bandung');
A OR B AND C =
A OR (B AND C). AND diproses duluan!
LIKE Β· Wildcard % _ Β· IS NULL Β· IS NOT NULL
| Pattern | Arti | Contoh Cocok |
|---|---|---|
'A%' |
Diawali A | Andi, Ahmad |
'%Putr%' |
Mengandung "Putr" | Jasmine Putri, Nanda Putra |
'%a' |
Diakhiri a | Citra Dewiβtidak, Megaβtidak |
'_i%' |
Huruf ke-2 = i | Dina, Lina |
SELECT * FROM pelanggan WHERE nama LIKE 'A%'; -- Diawali A SELECT * FROM pelanggan WHERE nama LIKE '%Putr%'; -- Mengandung Putr SELECT * FROM produk WHERE nama_produk LIKE '%Premium%';
SELECT * FROM pelanggan WHERE email IS NULL; -- Tanpa email SELECT * FROM pelanggan WHERE email IS NOT NULL; -- Punya email
= NULL! NULL bukan nilai, tapi
"ketiadaan data". Harus pakai IS NULL.
ORDER BY Β· ASC Β· DESC Β· LIMIT Β· OFFSET
-- A-Z (ASC = ascending, default) SELECT * FROM pelanggan ORDER BY nama ASC; -- Termahal dulu SELECT nama_produk, harga FROM produk ORDER BY harga DESC; -- Multi-sort: kota A-Z, lalu umur besar dulu SELECT * FROM pelanggan ORDER BY kota ASC, umur DESC; -- Top 3 produk termahal SELECT nama_produk, harga FROM produk ORDER BY harga DESC LIMIT 3; -- Halaman 2 (skip 5, ambil 5) SELECT * FROM pelanggan LIMIT 5 OFFSET 5;
Gabungkan semua yang dipelajari
SELECT kolom FROM tabel β ambil dataWHERE kondisi β filter barisAND / OR / NOT / IN β gabung kondisiLIKE '%pola%' β cari pola teksIS NULL / IS NOT NULL β cek data kosongORDER BY kolom DESC β urutkanLIMIT n OFFSET m β batasi & paging1. 5 pelanggan wanita termuda dari Jakarta atau Bandung 2. Produk yang namanya mengandung 'a', harga di bawah 500rb 3. Pesanan 'selesai' via 'Kartu Kredit', urutkan terbesar 4. 3 produk termurah dari kategori pakaian (kategori_id=2) 5. Pelanggan dari luar Jakarta, umur 25-35 6. Pesanan total > 5 juta, tampilkan 5 terbesar 7. Pelanggan yang tidak punya email
-- 1 SELECT * FROM pelanggan WHERE jenis_kelamin='Wanita' AND kota IN('Jakarta','Bandung') ORDER BY umur LIMIT 5; -- 2 SELECT * FROM produk WHERE nama_produk LIKE '%a%' AND harga<500000; -- 3 SELECT * FROM pesanan WHERE status_pesanan='selesai' AND metode_bayar='Kartu Kredit' ORDER BY total_harga DESC; -- 4 SELECT * FROM produk WHERE kategori_id=2 ORDER BY harga LIMIT 3; -- 5 SELECT * FROM pelanggan WHERE kota!='Jakarta' AND umur BETWEEN 25 AND 35; -- 6 SELECT * FROM pesanan WHERE total_harga>5000000 ORDER BY total_harga DESC LIMIT 5; -- 7 SELECT * FROM pelanggan WHERE email IS NULL;
Agregasi, pengelompokan, dan menggabungkan tabel
COUNT Β· SUM Β· AVG Β· MIN Β· MAX
SELECT COUNT(*) AS total_pelanggan FROM pelanggan; SELECT COUNT(email) AS punya_email FROM pelanggan; -- NULL tak dihitung! SELECT SUM(total_harga) AS pendapatan FROM pesanan; SELECT AVG(umur) AS rata_umur FROM pelanggan; SELECT MIN(harga) AS termurah, MAX(harga) AS termahal FROM produk; -- Gabung semua sekaligus SELECT COUNT(*) AS total, SUM(stok) AS stok, AVG(harga) AS rata, MIN(harga) AS min, MAX(harga) AS max FROM produk;
Mengelompokkan data Β· Hitung per grup
GROUP BY kota = pisahkan per kota, hitung tiap
tumpukan.
-- Jumlah pelanggan per kota SELECT kota, COUNT(*) AS jumlah FROM pelanggan GROUP BY kota; -- Total penjualan per metode bayar SELECT metode_bayar, COUNT(*) AS trx, SUM(total_harga) AS total FROM pesanan GROUP BY metode_bayar; -- Produk per kategori SELECT kategori_id, COUNT(*) AS jml, AVG(harga) AS rata FROM produk GROUP BY kategori_id; -- Total belanja per pelanggan, terbesar dulu SELECT pelanggan_id, COUNT(*) AS orders, SUM(total_harga) AS total FROM pesanan GROUP BY pelanggan_id ORDER BY total DESC;
WHERE vs HAVING Β· Urutan eksekusi SQL
WHERE filter sebelum grouping.
HAVING filter setelah grouping.
-- Kota dengan > 2 pelanggan SELECT kota, COUNT(*) AS jml FROM pelanggan GROUP BY kota HAVING COUNT(*)>2; -- WHERE + HAVING bersama 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 -- lalu filter GRUP ORDER BY total DESC;
Gabung 2 tabel Β· Data yang cocok Β· Alias
-- Pesanan + nama pelanggannya SELECT pes.id, pel.nama, pes.tanggal_pesan, pes.total_harga FROM pesanan pes INNER JOIN pelanggan pel ON pes.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;
JOIN tanpa awalan = INNER JOIN.
Semua data kiri Β· Temukan yang tidak punya relasi
| Jenis | Hasil | Kapan Pakai |
|---|---|---|
INNER JOIN |
Hanya yang cocok | Hanya butuh data yang punya relasi |
LEFT JOIN |
Semua kiri + cocok kanan (NULL jika tak ada) | Mau lihat semua, termasuk yang tanpa relasi |
-- Semua pelanggan + pesanannya (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; -- π₯ Trik populer: cari 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;
Join 3+ tabel Β· Hierarki Β· JOIN + Agregat
SELECT pel.nama, pr.nama_produk, k.nama_kategori, 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;
SELECT k.nama AS karyawan, k.jabatan, m.nama AS manager FROM karyawan k LEFT JOIN karyawan m ON k.manager_id = m.id;
-- Total belanja per pelanggan (dengan nama) SELECT pel.nama, COUNT(pes.id) AS orders, COALESCE(SUM(pes.total_harga),0) AS total FROM pelanggan pel LEFT JOIN pesanan pes ON pel.id=pes.pelanggan_id GROUP BY pel.id, pel.nama ORDER BY total DESC; -- Penjualan per kategori SELECT k.nama_kategori, SUM(dp.subtotal) AS 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 pendapatan DESC;
Agregat + JOIN = Laporan multi-tabel
1. Jumlah pelanggan per kota, hanya > 1 pelanggan 2. Top 5 pelanggan by total belanja (nama!) 3. Kategori paling banyak terjual 4. Produk yang belum pernah dipesan 5. Karyawan + manager + gaji 6. Total penjualan per bulan 7. Metode bayar paling populer
-- 1 SELECT kota,COUNT(*) AS j FROM pelanggan GROUP BY kota HAVING COUNT(*)>1; -- 2 SELECT pel.nama,SUM(pes.total_harga) AS t FROM pelanggan pel JOIN pesanan pes ON pel.id=pes.pelanggan_id GROUP BY pel.id ORDER BY t DESC LIMIT 5; -- 3 SELECT k.nama_kategori,SUM(dp.jumlah) AS terjual 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 terjual DESC; -- 4 SELECT pr.nama_produk FROM produk pr LEFT JOIN detail_pesanan dp ON pr.id=dp.produk_id WHERE dp.id IS NULL; -- 5 SELECT k.nama,k.jabatan,COALESCE(m.nama,'-') AS mgr,k.gaji FROM karyawan k LEFT JOIN karyawan m ON k.manager_id=m.id; -- 6 SELECT SUBSTR(tanggal_pesan,1,7) AS bln,SUM(total_harga) AS t FROM pesanan GROUP BY bln ORDER BY bln; -- 7 SELECT metode_bayar,COUNT(*) AS j FROM pesanan GROUP BY metode_bayar ORDER BY j DESC LIMIT 1;
Subquery, manipulasi data, fungsi lanjutan
Query bersarang Β· IN + subquery
-- Produk di atas harga rata-rata SELECT nama_produk, harga FROM produk WHERE harga > (SELECT AVG(harga) FROM produk); -- Pelanggan yang pernah beli 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 );
Derived table Β· EXISTS / NOT EXISTS
-- Rata-rata jumlah pesanan per pelanggan SELECT AVG(jml) AS rata_pesanan FROM (SELECT pelanggan_id, COUNT(*) AS jml FROM pesanan GROUP BY pelanggan_id) AS r;
-- 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);
Logika kondisional Β· IF-ELSE Β· Handle NULL
-- Kategorisasi harga SELECT nama_produk, harga, CASE WHEN harga < 100000 THEN 'Murah' WHEN harga <= 1000000 THEN 'Menengah' WHEN harga <= 10000000 THEN 'Mahal' ELSE 'Premium' END AS level_harga FROM produk ORDER BY harga; -- Pivot: hitung per status dalam 1 baris 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;
SELECT nama, COALESCE(email, 'Tidak ada email') AS email FROM pelanggan;
UPPER Β· LOWER Β· SUBSTR Β· STRFTIME Β· ROUND
SELECT UPPER(nama), LOWER(email), LENGTH(nama) FROM pelanggan; SELECT SUBSTR(nama,1,5) AS potong FROM pelanggan; SELECT REPLACE(email,'@email.com','@gmail.com') FROM pelanggan; SELECT nama||' ('||kota||')' AS info FROM pelanggan;
SELECT STRFTIME('%Y',tanggal_pesan) AS tahun, STRFTIME('%m',tanggal_pesan) AS bulan FROM pesanan; SELECT nama_produk, harga, ROUND(harga*1.11,0) AS ppn FROM produk;
Tambah Β· Ubah Β· Hapus data (DML)
INSERT INTO pelanggan (nama,email,kota,umur,jenis_kelamin,tanggal_daftar) VALUES ('Putri H','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 A','rizky@email.com','Palembang',32,'Pria','2024-10-05'), ('Sinta D','sinta@email.com','Bali',28,'Wanita','2024-10-10');
UPDATE pelanggan SET kota='Tangerang' WHERE id=1; UPDATE produk SET harga=harga*1.1 WHERE kategori_id=3; -- Makanan +10%
SELECT * FROM pelanggan WHERE id=16; -- Cek dulu! DELETE FROM pelanggan WHERE id=16; -- Baru hapus
CREATE TABLE Β· ALTER Β· DROP Β· TRANSACTION
CREATE TABLE review_produk ( id INTEGER PRIMARY KEY AUTOINCREMENT, produk_id INTEGER NOT NULL, pelanggan_id INTEGER NOT NULL, rating INTEGER CHECK(rating BETWEEN 1 AND 5), komentar TEXT, tanggal_review DATE DEFAULT(DATE('now')) ); ALTER TABLE pelanggan ADD COLUMN no_telepon VARCHAR(15); DROP TABLE IF EXISTS tabel_temp;
BEGIN TRANSACTION; INSERT INTO pesanan (...) VALUES (...); INSERT INTO detail_pesanan (...) VALUES (...); UPDATE produk SET stok=stok-1 WHERE id=1; COMMIT; -- Simpan semua (atau ROLLBACK untuk batalkan)
Subquery + CASE + DML
1. Produk di atas harga rata-rata + kategorinya 2. Kolom segmentasi umur (Gen Z <25, Millennial 25-34, Gen X 35+) 3. Pelanggan yang pernah beli pakaian (via subquery) 4. Buat kolom baru harga+ppn 11% dan level_harga 5. Insert 3 review, tampilkan rata-rata rating per produk
-- 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,umur,CASE WHEN umur<25 THEN 'Gen Z' WHEN umur<=34 THEN 'Millennial' ELSE 'Gen X' END AS gen FROM pelanggan; -- 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 nama_produk,harga,ROUND(harga*1.11,0) AS ppn,CASE WHEN harga<100000 THEN 'Murah' WHEN harga<=1000000 THEN 'Menengah' ELSE 'Mahal' END AS lvl FROM produk; -- 5 INSERT INTO review_produk(produk_id,pelanggan_id,rating,komentar) VALUES(1,1,5,'Bagus'),(1,3,4,'Oke'),(2,2,5,'Mantap'); SELECT produk_id,AVG(rating) AS avg_rate FROM review_produk GROUP BY produk_id;
View, CTE, Window Functions, dan analisis bisnis nyata
Simpan query Β· Reusable Β· Abstraksi
CREATE VIEW v_ringkasan_pelanggan AS SELECT pel.id, 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 seperti tabel biasa! SELECT * FROM v_ringkasan_pelanggan WHERE total_belanja>10000000; SELECT * FROM v_ringkasan_pelanggan ORDER BY total_belanja DESC LIMIT 5;
Percepat query Β· EXPLAIN Β· Kapan pakai
CREATE INDEX idx_pel_kota ON pelanggan(kota); CREATE INDEX idx_pes_pel ON pesanan(pelanggan_id); CREATE INDEX idx_pes_tgl ON pesanan(tanggal_pesan); -- Lihat apakah index terpakai EXPLAIN QUERY PLAN SELECT * FROM pelanggan WHERE kota='Jakarta';
Common Table Expression Β· Pecah query kompleks
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;
WITH ringkasan AS ( SELECT produk_id, SUM(jumlah) AS terjual, SUM(subtotal) AS rev FROM detail_pesanan GROUP BY produk_id ), rata AS (SELECT AVG(rev) AS avg_rev FROM ringkasan) SELECT pr.nama_produk, r.terjual, r.rev, CASE WHEN r.rev>ra.avg_rev THEN 'Di Atas RataΒ²' ELSE 'Di Bawah' END AS performa FROM ringkasan r JOIN produk pr ON r.produk_id=pr.id CROSS JOIN rata ra ORDER BY r.rev DESC;
ROW_NUMBER Β· RANK Β· DENSE_RANK Β· PARTITION BY
-- Ranking global by harga SELECT nama_produk, harga, ROW_NUMBER() OVER(ORDER BY harga DESC) AS ranking FROM produk; -- 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 rank_di_kat FROM produk pr JOIN kategori k ON pr.kategori_id=k.id;
| Fungsi | Kegunaan | Jika nilai sama |
|---|---|---|
ROW_NUMBER() |
Nomor urut unik | 1, 2, 3, 4 |
RANK() |
Ranking | 1, 2, 2, 4 (skip) |
DENSE_RANK() |
Ranking padat | 1, 2, 2, 3 (no skip) |
LAG Β· LEAD Β· Running Total Β· NTILE
-- Running total (akumulasi penjualan) 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 sebelum, total_harga - LAG(total_harga) OVER(ORDER BY tanggal_pesan) AS selisih FROM pesanan WHERE status_pesanan='selesai'; -- NTILE: bagi ke 4 grup (quartile) SELECT nama, umur, NTILE(4) OVER(ORDER BY umur) AS quartile FROM pelanggan;
| Fungsi | Kegunaan |
|---|---|
LAG(col, n) |
Nilai n baris sebelumnya |
LEAD(col, n) |
Nilai n baris sesudahnya |
SUM() OVER() |
Running total / akumulasi |
NTILE(n) |
Bagi ke n grup sama rata |
Dashboard Β· MoM Growth Β· Kontribusi %
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_pct FROM bulanan;
WITH gt AS (SELECT SUM(total_harga) AS v FROM pesanan WHERE status_pesanan='selesai') SELECT metode_bayar, COUNT(*) AS trx, SUM(total_harga) AS rev, ROUND(SUM(total_harga)*100.0/(SELECT v FROM gt),2) AS persen FROM pesanan WHERE status_pesanan='selesai' GROUP BY metode_bayar ORDER BY rev DESC;
RFM Segmentasi Β· Repeat vs One-time
WITH rfm AS ( SELECT pel.id,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;
Dashboard Β· Cross-sell Β· All-in-one query
WITH sales AS (SELECT COUNT(*) AS orders,SUM(total_harga) AS rev,ROUND(AVG(total_harga),0) AS aov FROM pesanan WHERE status_pesanan='selesai'), top_p AS (SELECT pr.nama_produk,SUM(dp.subtotal) AS r FROM detail_pesanan dp JOIN produk pr ON dp.produk_id=pr.id GROUP BY pr.id ORDER BY r DESC LIMIT 1), top_c AS (SELECT pel.nama,SUM(pes.total_harga) AS b FROM pesanan pes JOIN pelanggan pel ON pes.pelanggan_id=pel.id GROUP BY pel.id ORDER BY b DESC LIMIT 1) SELECT s.*,tp.nama_produk AS top_produk,tc.nama AS top_pelanggan,tc.b AS belanja FROM sales s CROSS JOIN top_p tp CROSS JOIN top_c tc;
SELECT p1.nama_produk AS produk_1, p2.nama_produk AS produk_2, COUNT(*) AS bareng FROM detail_pesanan d1 JOIN detail_pesanan d2 ON d1.pesanan_id=d2.pesanan_id AND d1.produk_id<d2.produk_id JOIN produk p1 ON d1.produk_id=p1.id JOIN produk p2 ON d2.produk_id=p2.id GROUP BY p1.nama_produk,p2.nama_produk ORDER BY bareng DESC;
Proyek akhir Β· Referensi lengkap
Buat laporan bisnis lengkap yang menjawab semua ini: 1. Dashboard: total pesanan, revenue, avg order value 2. Top 5 pelanggan VIP (nama, kota, total belanja, jumlah order) 3. Top 5 produk terlaris (nama, kategori, terjual, revenue) 4. Pertumbuhan penjualan bulan ke bulan (MoM growth %) 5. Segmentasi pelanggan: VIP / Premium / Regular / Inactive 6. Kategori mana kontribusi revenue paling besar (+ persentase)? 7. Produk yang perlu restock (stok rendah, penjualan tinggi) 8. Produk sering dibeli bersamaan (cross-sell opportunity) 9. Rata-rata berapa hari dari daftar sampai pembelian pertama? 10. Executive summary: semua metrik kunci dalam 1 query
SELECT kolom FROM tabel JOIN gabung tabel WHERE filter baris GROUP BY kelompokkan HAVING filter grup ORDER BY urutkan LIMIT batasi jumlah
FROM β JOIN β WHERE β GROUP BY β HAVING β SELECT β DISTINCT β ORDER BY β LIMIT Tips: WHERE tak bisa pakai alias dari SELECT
= != > < >= <= AND OR NOT IN BETWEEN x AND y IS NULL / IS NOT NULL LIKE 'A%' LIKE '%x%' % = karakter apapun _ = tepat 1 karakter
COUNT(*) Hitung baris COUNT(c) Skip NULL SUM(c) Jumlahkan AVG(c) Rata-rata MIN(c) Terkecil MAX(c) Terbesar
INNER JOIN Cocok di kedua LEFT JOIN Semua kiri SELF JOIN Tabelβdirinya CROSS JOIN Semua kombinasi
ROW_NUMBER() Nomor urut RANK() Ranking DENSE_RANK() Ranking padat LAG()/LEAD() Sebelum/sesudah SUM() OVER() Running total NTILE(n) Bagi n grup
INSERT INTO t(c) VALUES(v) UPDATE t SET c=v WHERE ... DELETE FROM t WHERE ... β οΈ SELALU pakai WHERE!
CREATE TABLE t (...) ALTER TABLE ADD COLUMN DROP TABLE IF EXISTS CREATE VIEW v AS SELECT CREATE INDEX i ON t(c) WITH cte AS (...) SELECT