SQL adalah bahasa universal data

Hampir semua perusahaan menyimpan data di database relasional. SQL adalah cara Abbas berbicara langsung ke database tersebut, dari startup kecil hingga perusahaan Fortune 500. Data cleaning adalah fondasi wajib: data kotor menghasilkan analisis yang menyesatkan.

  • Menulis query SELECT, FROM, WHERE, ORDER BY untuk mengambil data spesifik dari tabel database
  • Menggunakan fungsi agregasi COUNT, SUM, AVG, MIN, MAX bersama GROUP BY dan HAVING
  • Menggabungkan dua atau lebih tabel dengan INNER JOIN dan LEFT JOIN berdasarkan kunci relasi
  • Menulis CASE WHEN untuk membuat kolom kategori baru secara kondisional dalam satu query
  • Menggunakan subquery dan CTE (WITH) untuk memecah masalah kompleks menjadi langkah bertahap
  • Menangani nilai NULL dengan COALESCE, NULLIF, IS NULL agar data hilang tidak luput dari perhatian
  • Membersihkan string dengan TRIM, UPPER, LOWER, REPLACE, SUBSTR secara efisien
  • Menghapus duplikat secara tepat menggunakan window function ROW_NUMBER() OVER (PARTITION BY)
Konteks Fase 2: Bulan 4 dan 5 membangun jembatan antara konsep statistik (Bulan 2-3) dan analisis lanjutan (Bulan 6-12). SQL dan pandas adalah dua senjata utama setiap data analyst profesional.
Estimasi waktu untuk Abbas: 4 minggu, sekitar 5-6 jam per minggu. Satu sesi belajar cukup untuk satu blok hari. Tidak harus berurutan sesuai hari kalender.

Minggu 1: SQL Fundamentals

Abbas mulai dari nol: memahami konsep relational database, lalu langsung menulis query pertama yang benar-benar mengambil data dari tabel nyata.

1
SQL Fundamentals
SELECT, FROM, WHERE, operator logika, ORDER BY, LIMIT, DISTINCT
โ–ผ
Hari 1
SQL vs NoSQL, Konsep Relational Database
Database relasional menyimpan data dalam tabel dengan baris (record) dan kolom (field). Setiap tabel punya primary key sebagai identifier unik per baris. Tabel-tabel saling terhubung lewat foreign key sehingga data tidak perlu diulang di setiap tabel.
Analogi sederhana: Bayangkan Google Sheets dengan beberapa tab yang bisa saling merujuk satu sama lain secara otomatis dan efisien, tanpa harus copy-paste data berulang kali.
-- Contoh struktur: tabel siswa
-- id        INTEGER PRIMARY KEY  (unik per siswa)
-- nama      TEXT
-- kelas     TEXT
-- tgl_lahir DATE

-- Tabel nilai merujuk ke tabel siswa via foreign key:
-- id             INTEGER PRIMARY KEY
-- siswa_id       INTEGER REFERENCES siswa(id)
-- mata_pelajaran TEXT
-- nilai          INTEGER
-- semester       INTEGER
Hari 2
Query Pertama: SELECT, FROM, WHERE
SELECT menentukan kolom yang diambil. FROM menentukan tabel sumbernya. WHERE menyaring baris berdasarkan kondisi. Tiga kata kunci ini mencakup 80% pekerjaan SQL harian Abbas sebagai data analyst.
-- Ambil semua kolom dari tabel siswa
SELECT * FROM siswa;

-- Ambil kolom tertentu saja
SELECT nama, kelas, nilai
FROM siswa;

-- Filter: hanya siswa dengan nilai di atas 75
SELECT nama, kelas, nilai
FROM siswa
WHERE nilai > 75;

-- Filter ganda dengan AND
SELECT nama, nilai
FROM siswa
WHERE kelas = '11-IPA' AND nilai >= 80;
Kebiasaan baik: Tulis SQL keyword dengan huruf besar (SELECT, FROM, WHERE) agar mudah dibedakan dari nama tabel dan kolom. Ini bukan aturan teknis, tapi membuat kode lebih mudah dibaca orang lain.
Hari 3
Operator Logika: AND, OR, NOT, BETWEEN, IN, LIKE
Perluas kemampuan WHERE dengan operator yang lebih ekspresif. BETWEEN untuk rentang nilai, IN untuk daftar pilihan, LIKE untuk mencocokkan pola teks dengan wildcard persen (%).
-- BETWEEN: nilai dalam rentang 70 sampai 90
SELECT nama, nilai FROM siswa
WHERE nilai BETWEEN 70 AND 90;

-- IN: pilih beberapa kelas sekaligus
SELECT nama, kelas FROM siswa
WHERE kelas IN ('11-IPA', '11-IPS', '11-Bahasa');

-- LIKE: nama diawali huruf A
SELECT nama FROM siswa
WHERE nama LIKE 'A%';

-- LIKE: nama mengandung kata 'putra' (case-insensitive)
SELECT nama FROM siswa
WHERE LOWER(nama) LIKE '%putra%';

-- NOT: siswa yang bukan dari kelas 12
SELECT nama, kelas FROM siswa
WHERE kelas NOT LIKE '12%';
Hari 4
Urutkan dan Batasi: ORDER BY, LIMIT, DISTINCT
ORDER BY mengurutkan hasil query berdasarkan satu atau lebih kolom. LIMIT membatasi jumlah baris yang dikembalikan. DISTINCT menghilangkan duplikat dari hasil, berguna untuk melihat daftar nilai unik suatu kolom.
-- Top 10 nilai tertinggi
SELECT nama, nilai FROM siswa
ORDER BY nilai DESC
LIMIT 10;

-- Urutkan dua kolom sekaligus: kelas A-Z, nilai Z-A
SELECT nama, kelas, nilai FROM siswa
ORDER BY kelas ASC, nilai DESC;

-- Daftar kelas unik yang ada (tanpa duplikat)
SELECT DISTINCT kelas FROM siswa
ORDER BY kelas ASC;
Tantangan Hari 4: Tulis satu query untuk mengambil 5 siswa dengan nilai paling rendah dari kelas 10 saja, tanpa melihat contoh di atas terlebih dahulu. Verifikasi hasilnya masuk akal.
Hari 5
Latihan: 10 Query dari Dataset Nilai Sekolah
Abbas mengerjakan 10 query nyata menggunakan SQLite dan DB Browser for SQLite. Dataset: tabel siswa 200 baris, tabel nilai 1.000 baris mencakup 5 mata pelajaran per siswa. Setiap query mengetes kombinasi konsep dari Hari 1-4.
10 Soal Latihan: (1) Semua siswa dengan nilai Matematika di atas 80. (2) 5 nilai Bahasa Indonesia tertinggi dari kelas 11. (3) Daftar mata pelajaran unik yang ada. (4) Siswa yang namanya mengandung kata "Putra" atau "Putri". (5) Nilai antara 65 dan 79 diurutkan dari tertinggi. (6) Siswa kelas 10 ATAU yang punya nilai di atas 95. (7) Semua siswa yang BUKAN dari kelas 12. (8) 3 nilai terendah dari seluruh dataset. (9) Data siswa tanpa menampilkan kolom tgl_lahir. (10) Siswa dengan nilai tepat 100 di mata pelajaran apa saja.

Minggu 2: Agregasi dan Subquery

Satu baris data tidak cukup untuk insight. Abbas perlu merangkum ribuan baris menjadi angka bermakna lewat fungsi agregasi dan logika kondisional CASE WHEN.

2
Agregasi dan Subquery
COUNT, SUM, AVG, GROUP BY, HAVING, CASE WHEN, CTE
โ–ผ
Hari 1
Fungsi Agregasi: COUNT, SUM, AVG, MIN, MAX
Fungsi agregasi merangkum banyak baris menjadi satu angka ringkasan. Inilah cara Abbas menjawab pertanyaan bisnis seperti "berapa rata-rata nilai?" atau "berapa total pendapatan bulan ini?"
-- Jumlah total siswa dalam tabel
SELECT COUNT(*) AS total_siswa FROM siswa;

-- Rangkuman statistik nilai sekaligus dalam satu query
SELECT
  COUNT(*)              AS jumlah_data,
  ROUND(AVG(nilai), 2)  AS rata_rata,
  MAX(nilai)            AS nilai_maks,
  MIN(nilai)            AS nilai_min,
  SUM(nilai)            AS total_poin
FROM nilai;

-- Hitung hanya siswa yang lulus (nilai >= 75)
SELECT COUNT(*) AS siswa_lulus
FROM nilai
WHERE nilai >= 75;
Hari 2
GROUP BY dan HAVING
GROUP BY mengelompokkan baris berdasarkan satu atau lebih kolom sebelum agregasi dilakukan. HAVING memfilter hasil GROUP BY, berbeda dari WHERE yang memfilter baris individual sebelum pengelompokan.
-- Rata-rata nilai per kelas, diurutkan dari tertinggi
SELECT kelas, ROUND(AVG(nilai), 1) AS rata_rata
FROM nilai
JOIN siswa ON nilai.siswa_id = siswa.id
GROUP BY kelas
ORDER BY rata_rata DESC;

-- Hanya tampilkan kelas dengan rata-rata di atas 78
SELECT kelas, ROUND(AVG(nilai), 1) AS rata, COUNT(*) AS jml_siswa
FROM nilai
JOIN siswa ON nilai.siswa_id = siswa.id
GROUP BY kelas
HAVING AVG(nilai) > 78;
WHERE vs HAVING: WHERE menyaring baris SEBELUM GROUP BY. HAVING menyaring kelompok SETELAH GROUP BY. HAVING bisa menggunakan fungsi agregasi, WHERE tidak bisa.
Hari 3
CASE WHEN untuk Kolom Kategori Baru
CASE WHEN bekerja seperti if-else dalam SQL. Abbas bisa membuat kolom kategori baru langsung dalam query tanpa mengubah data asli di tabel, sangat berguna untuk segmentasi dan pelaporan.
-- Tambahkan kolom grade berdasarkan nilai numerik
SELECT nama, nilai,
  CASE
    WHEN nilai >= 90 THEN 'A'
    WHEN nilai >= 80 THEN 'B'
    WHEN nilai >= 70 THEN 'C'
    WHEN nilai >= 60 THEN 'D'
    ELSE 'E'
  END AS grade
FROM nilai
JOIN siswa ON nilai.siswa_id = siswa.id;

-- Hitung distribusi status kelulusan per kelas
SELECT kelas,
  SUM(CASE WHEN nilai >= 75 THEN 1 ELSE 0 END) AS lulus,
  SUM(CASE WHEN nilai < 75  THEN 1 ELSE 0 END) AS tidak_lulus
FROM nilai
JOIN siswa ON nilai.siswa_id = siswa.id
GROUP BY kelas;
Hari 4
Subquery dan CTE (WITH)
Subquery adalah query di dalam query. CTE (Common Table Expression) dengan keyword WITH membuat subquery lebih mudah dibaca dan bisa dirujuk berulang dalam satu query panjang.
-- Siswa dengan nilai di atas rata-rata keseluruhan (subquery)
SELECT nama, nilai
FROM nilai JOIN siswa ON nilai.siswa_id = siswa.id
WHERE nilai > (SELECT AVG(nilai) FROM nilai);

-- CTE: buat ringkasan per kelas, lalu filter dari ringkasan itu
WITH avg_per_kelas AS (
  SELECT s.kelas, ROUND(AVG(n.nilai), 1) AS avg_nilai
  FROM nilai n JOIN siswa s ON n.siswa_id = s.id
  GROUP BY s.kelas
)
SELECT kelas, avg_nilai
FROM avg_per_kelas
WHERE avg_nilai > 80
ORDER BY avg_nilai DESC;
CTE vs subquery: Hasilnya identik, tapi CTE jauh lebih mudah dibaca dan bisa dirujuk lebih dari satu kali dalam query yang sama.
Hari 5
Latihan: Analisis Penjualan per Kategori per Bulan
Abbas menganalisis dataset penjualan toko (tabel orders, products, categories). Target akhir: laporan penjualan per kategori per bulan dengan kolom total transaksi, total pendapatan, dan rank berdasarkan pendapatan.
Kriteria sukses: Query menghasilkan kolom: nama_kategori, bulan (format YYYY-MM), total_transaksi, total_pendapatan, rank_bulan. Wajib menggunakan GROUP BY, SUM, COUNT, CASE WHEN, dan ORDER BY dalam satu query yang rapi.

Minggu 3: JOIN, Gabungkan Data dari Banyak Tabel

Data nyata jarang tersimpan dalam satu tabel saja. JOIN adalah cara Abbas menggabungkan informasi dari beberapa tabel berdasarkan kolom kunci yang cocok di antara keduanya.

3
JOIN: Relasi Antar Tabel
INNER JOIN, LEFT JOIN, memilih jenis JOIN yang tepat, gabung 3 tabel
โ–ผ
Hari 1
INNER JOIN: Hanya Baris yang Cocok di Kedua Tabel
INNER JOIN mengembalikan baris yang memiliki nilai cocok di kedua tabel. Baris yang tidak punya pasangan di tabel lain tidak akan muncul dalam hasil. Ini jenis JOIN yang paling sering dipakai di pekerjaan nyata.
-- Gabungkan data siswa dengan nilai mereka
SELECT s.nama, s.kelas, n.mata_pelajaran, n.nilai
FROM siswa AS s
INNER JOIN nilai AS n ON s.id = n.siswa_id;

-- Tambahkan filter setelah JOIN
SELECT s.nama, n.mata_pelajaran, n.nilai
FROM siswa AS s
INNER JOIN nilai AS n ON s.id = n.siswa_id
WHERE n.nilai >= 85
ORDER BY n.nilai DESC;
Alias tabel: AS s dan AS n adalah alias singkatan tabel. s.nama berarti "kolom nama dari tabel yang diberi alias s". Wajib dipakai saat dua tabel memiliki nama kolom yang sama.
Hari 2
LEFT JOIN: Pertahankan Semua Baris dari Tabel Kiri
LEFT JOIN mengembalikan semua baris dari tabel kiri, ditambah data dari tabel kanan jika ada pasangannya. Jika tidak ada pasangan, kolom dari tabel kanan diisi NULL. Berguna untuk menemukan data yang "belum lengkap".
-- Semua siswa, termasuk yang belum punya nilai sama sekali
SELECT s.nama, s.kelas, n.mata_pelajaran, n.nilai
FROM siswa AS s
LEFT JOIN nilai AS n ON s.id = n.siswa_id;

-- Hanya siswa yang BELUM ada nilai: filter WHERE kolom kanan IS NULL
SELECT s.nama, s.kelas
FROM siswa AS s
LEFT JOIN nilai AS n ON s.id = n.siswa_id
WHERE n.siswa_id IS NULL;
Trik deteksi data hilang: LEFT JOIN lalu filter WHERE kolom_kanan IS NULL adalah pola standar untuk menemukan baris di tabel kiri yang tidak punya pasangan di tabel kanan.
Hari 3
Kapan Pakai JOIN Mana
Ada empat jenis JOIN utama. Pilihan yang tepat menentukan data apa yang ikut dalam hasil, terutama ketika ada baris yang tidak memiliki pasangan di salah satu tabel.
Panduan cepat Abbas: INNER JOIN untuk hanya irisan dua tabel. LEFT JOIN untuk semua baris dari tabel kiri meski tidak punya pasangan. RIGHT JOIN jarang dipakai, lebih mudah balik urutan tabel lalu LEFT JOIN. FULL OUTER JOIN untuk semua baris dari kedua tabel (tidak didukung SQLite).
-- Simulasi FULL OUTER JOIN di SQLite menggunakan UNION
SELECT s.nama, n.nilai
FROM siswa s LEFT JOIN nilai n ON s.id = n.siswa_id
UNION
SELECT s.nama, n.nilai
FROM nilai n LEFT JOIN siswa s ON n.siswa_id = s.id
WHERE s.id IS NULL;
Hari 4
JOIN 3 Tabel Sekaligus
JOIN bisa dirantai lebih dari dua tabel. Setiap JOIN baru ditambahkan setelah JOIN sebelumnya dengan kondisi ON yang menghubungkan kunci yang tepat antar tabel yang relevan.
-- Gabungkan siswa, nilai, DAN guru pengampu mata pelajaran
SELECT
  s.nama           AS nama_siswa,
  s.kelas,
  n.mata_pelajaran,
  n.nilai,
  g.nama           AS nama_guru
FROM siswa  AS s
INNER JOIN nilai AS n ON s.id      = n.siswa_id
INNER JOIN guru  AS g ON n.guru_id = g.id
WHERE n.nilai >= 75
ORDER BY s.kelas, n.mata_pelajaran, n.nilai DESC;
Latihan eksplorasi: Tambahkan tabel ruangan (id, kode_ruang, kapasitas) dan JOIN ke tabel kelas. Tampilkan nama siswa, kelas, kode ruang, dan kapasitas ruang masing-masing.
Hari 5
Latihan: JOIN Tabel Siswa, Nilai, dan Guru
Abbas membuat laporan lengkap berisi daftar semua siswa beserta nilai per mata pelajaran dan nama guru pengampu, termasuk siswa yang belum memiliki nilai (pakai LEFT JOIN). Laporan diurutkan per kelas lalu per mata pelajaran.
Bonus query: Gunakan subquery atau CTE untuk menampilkan hanya siswa yang nilainya di atas rata-rata mata pelajaran tersebut. Contoh: jika rata-rata Matematika 78, tampilkan hanya siswa Matematika dengan nilai lebih dari 78.

Minggu 4: Data Cleaning dengan SQL

Data nyata hampir selalu kotor: ada NULL, spasi tersembunyi, format tidak konsisten, dan duplikat. Minggu ini Abbas belajar membersihkannya langsung di database tanpa harus download data terlebih dahulu.

4
Data Cleaning SQL
NULL, string cleaning, type casting, ROW_NUMBER deduplikasi
โ–ผ
Hari 1
NULL Handling: IS NULL, COALESCE, NULLIF
NULL bukan nol dan bukan string kosong. NULL artinya "nilai tidak diketahui". COALESCE mengganti NULL dengan nilai fallback, NULLIF mengubah nilai tertentu menjadi NULL agar tidak merusak hasil agregasi.
-- Cari baris yang punya NULL di kolom nilai
SELECT * FROM nilai WHERE nilai IS NULL;
SELECT * FROM nilai WHERE nilai IS NOT NULL;

-- Ganti NULL dengan 0 sebagai nilai default
SELECT nama, COALESCE(nilai, 0) AS nilai_bersih
FROM nilai JOIN siswa ON nilai.siswa_id = siswa.id;

-- Imputasi NULL dengan rata-rata keseluruhan
SELECT nama,
  COALESCE(nilai, (SELECT ROUND(AVG(nilai), 0) FROM nilai)) AS nilai_imputed
FROM nilai JOIN siswa ON nilai.siswa_id = siswa.id;

-- NULLIF: ubah nilai 0 menjadi NULL agar tidak ikut dihitung AVG
SELECT ROUND(AVG(NULLIF(nilai, 0)), 2) AS avg_tanpa_nol FROM nilai;
Hari 2
String Cleaning: TRIM, UPPER, LOWER, REPLACE, SUBSTR
Data teks sering punya masalah tersembunyi: spasi ekstra di awal atau akhir, huruf besar-kecil tidak konsisten, atau karakter yang salah. Fungsi string SQL menanganinya tanpa perlu download data ke Excel.
-- Hapus spasi di awal dan akhir nama
SELECT TRIM(nama) AS nama_bersih FROM siswa;

-- Standarisasi ke huruf kapital semua
SELECT UPPER(TRIM(nama)) AS nama_standar FROM siswa;

-- Ganti singkatan kelas lama ke format baru
SELECT REPLACE(kelas, 'XI', '11') AS kelas_baru FROM siswa;

-- Gabungkan beberapa fungsi dalam satu ekspresi
SELECT UPPER(TRIM(REPLACE(nama, '  ', ' '))) AS nama_final
FROM siswa;

-- Ambil 2 karakter pertama untuk menentukan level kelas
SELECT nama, SUBSTR(kelas, 1, 2) AS level_kelas FROM siswa;
Hari 3
Type Casting: Konversi Tipe Data
Data kadang tersimpan dengan tipe yang salah karena kesalahan input: angka tersimpan sebagai TEXT, tanggal sebagai string biasa. CAST mengubah tipe data langsung dalam query tanpa perlu mengubah tabel asli.
-- Ubah teks menjadi integer untuk kalkulasi
SELECT CAST(nilai_teks AS INTEGER) AS nilai_angka
FROM siswa_raw;

-- Konversi integer ke teks untuk digabung dengan string
SELECT nama || ' (Nilai: ' || CAST(nilai AS TEXT) || ')' AS info
FROM nilai JOIN siswa ON nilai.siswa_id = siswa.id;

-- Parsing komponen tanggal di SQLite dengan STRFTIME
SELECT
  tanggal_ujian,
  STRFTIME('%Y', tanggal_ujian) AS tahun,
  STRFTIME('%m', tanggal_ujian) AS bulan,
  STRFTIME('%d', tanggal_ujian) AS hari
FROM ujian;
Catatan lintas database: Di PostgreSQL gunakan :: untuk casting (nilai::INTEGER). Di MySQL gunakan CONVERT(nilai, SIGNED). Konsep sama, sintaks sedikit berbeda.
Hari 4
Deduplikasi dengan ROW_NUMBER() OVER (PARTITION BY)
Window function ROW_NUMBER() memberi nomor urut untuk setiap baris dalam kelompok yang ditentukan PARTITION BY. Strategi deduplikasi klasik: beri nomor urut, lalu ambil hanya baris bernomor 1 per kelompok.
-- Lihat duplikat: nama dan kelas sama, id berbeda
SELECT nama, kelas, COUNT(*) AS kemunculan
FROM siswa
GROUP BY nama, kelas
HAVING COUNT(*) > 1;

-- Beri nomor urut per kelompok duplikat (simpan yang paling lama: id terkecil)
WITH ranked AS (
  SELECT *,
    ROW_NUMBER() OVER (
      PARTITION BY nama, kelas
      ORDER BY id ASC
    ) AS rn
  FROM siswa
)
-- Pilih hanya baris pertama per kelompok (baris asli, hapus duplikat)
SELECT id, nama, kelas
FROM ranked
WHERE rn = 1;
Tentang CTE di sini: WITH ranked AS (...) membuat tabel sementara bernama "ranked" yang bisa diquery di bawahnya. Cara ini lebih bersih daripada nested subquery panjang.
Hari 5
Mini Project: Bersihkan Dataset Kotor 500 Baris
Abbas mendapat dataset nilai ujian 500 siswa dengan masalah nyata: kolom NULL tersebar, nama tidak konsisten (huruf campur dan spasi berlebih), duplikat baris, kolom nilai tersimpan sebagai TEXT. Target: hasilkan tabel bersih yang siap dianalisis dalam satu sesi kerja.
Checklist cleaning Abbas: (1) Audit semua kolom dengan NULL menggunakan COUNT dan CASE WHEN. (2) Standarisasi nama dengan UPPER, TRIM, REPLACE. (3) Cast kolom nilai dari TEXT ke INTEGER. (4) Hapus duplikat dengan ROW_NUMBER OVER PARTITION BY. (5) Validasi jumlah baris sebelum dan sesudah cleaning. (6) Simpan hasil: CREATE TABLE siswa_bersih AS SELECT ... dengan semua transformasi dalam satu query utuh.

Referensi Buku SQL untuk Abbas

Empat buku terbaik yang membangun fondasi SQL dari level nol hingga siap kerja profesional. Tidak perlu dibeli sekaligus, ikuti urutan prioritas ini.

๐Ÿ“˜
Learning SQL
Alan Beaulieu, O'Reilly Media
Fondasi SQL paling lengkap dan jelas. Cocok untuk Abbas di Bulan 4. Prioritaskan bab 1-9 untuk query dasar, JOIN, dan subquery.
๐Ÿ“—
Practical SQL
Anthony DeBarros, No Starch Press
SQL untuk analisis data dengan PostgreSQL. Banyak studi kasus dari data publik dunia nyata. Sangat relevan untuk Proyek P3 nanti.
๐Ÿ“™
SQL Cookbook
Anthony Molinaro, O'Reilly Media
Format problem-solution: lebih dari 200 resep SQL siap pakai. Bagus sebagai referensi saat Abbas menemukan masalah spesifik yang belum tahu cara SQL-nya.
๐Ÿ“•
SQL for Data Analysis
Cathy Tanimura, O'Reilly Media
Teknik SQL lanjutan untuk analisis bisnis: cohort analysis, time series, funnel. Cocok dibaca Abbas setelah Bulan 4 selesai dan mulai Bulan 6.

Tools SQL untuk Abbas

Mulai dari SQLite untuk latihan lokal di MacBook Air M5, hingga BigQuery untuk merasakan SQL di data skala produksi nyata.

๐Ÿ—ƒ๏ธ
SQLite
Database berbasis satu file, tanpa server. Sudah tersedia di macOS. Ideal untuk latihan harian Abbas tanpa setup rumit.
๐Ÿ–ฅ๏ธ
DB Browser for SQLite
GUI gratis dan ringan untuk SQLite. Abbas bisa lihat struktur tabel, jalankan query, dan import CSV tanpa perlu command line sama sekali.
๐Ÿ˜
PostgreSQL
Database open-source paling populer di industri. Mendukung window functions, CTE, dan semua fitur SQL standar yang dibutuhkan Abbas.
๐Ÿ”ต
BigQuery
SQL di cloud dari Google untuk data skala besar. Free tier 1 TB query per bulan. Merasakan SQL yang dipakai perusahaan nyata.
๐Ÿฆซ
DBeaver
IDE database universal gratis. Mendukung SQLite, PostgreSQL, MySQL, BigQuery dalam satu aplikasi. Berguna saat proyek makin besar.
๐Ÿ“Š
Mode Analytics
SQL editor berbasis web dengan visualisasi bawaan. Abbas bisa langsung buat chart dari hasil query tanpa pindah ke tool lain.
Setup yang direkomendasikan untuk Abbas: Minggu 1-2, pakai DB Browser for SQLite saja. Minggu 3, mulai eksplorasi DBeaver. Akhir bulan, buka BigQuery sekali untuk merasakan scale yang berbeda. PostgreSQL dan Mode Analytics bisa diinstall di Bulan 5 saat Abbas sudah nyaman dengan SQL dasar.

Tips Praktis dan Jebakan Pemula SQL

Delapan kebiasaan yang memisahkan analyst pemula dari yang sudah berpengalaman. Abbas tidak perlu menunggu sampai membuat kesalahan besar untuk belajar hal-hal ini.

Tips 1 Selalu SELECT dulu, baru UPDATE atau DELETE
Prinsip Utama
Verifikasi sebelum memodifikasi data
Kesalahan UPDATE tanpa WHERE yang tepat bisa merusak ribuan baris sekaligus dan tidak ada tombol "undo" di SQL. Kebiasaan wajib: tulis SELECT dengan kondisi yang sama terlebih dahulu, pastikan baris yang muncul adalah yang memang ingin diubah, baru ganti SELECT menjadi UPDATE.
-- LANGKAH 1: Verifikasi dulu dengan SELECT
SELECT id, nama, email
FROM siswa
WHERE email LIKE '%@gmail%' AND kelas = '10-A';
-- Pastikan hasilnya sesuai ekspektasi (misal: 5 baris)

-- LANGKAH 2: Setelah yakin, baru jalankan UPDATE
UPDATE siswa
SET email = LOWER(email)
WHERE email LIKE '%@gmail%' AND kelas = '10-A';
-- Gunakan WHERE yang identik dengan SELECT di atas

-- Cek hasil setelah update
SELECT id, nama, email FROM siswa WHERE kelas = '10-A';
Peringatan keras: UPDATE siswa SET nilai = 0; tanpa WHERE akan mengosongkan nilai SEMUA siswa di tabel. Ini tidak bisa dibatalkan. Selalu cek: apakah query Abbas punya klausa WHERE?
Tips 2 NULL bukan nol dan bukan string kosong
Konsep Kritis
NULL adalah "tidak ada data", bukan angka nol
Kesalahan paling sering untuk pemula: mencoba membandingkan NULL dengan operator biasa (= atau !=). NULL tidak sama dengan apapun, termasuk NULL itu sendiri. Gunakan IS NULL dan IS NOT NULL.
-- SALAH: ini tidak akan menemukan baris apapun
SELECT * FROM nilai WHERE skor = NULL;
SELECT * FROM nilai WHERE skor != NULL;

-- BENAR: gunakan IS NULL / IS NOT NULL
SELECT * FROM nilai WHERE skor IS NULL;
SELECT * FROM nilai WHERE skor IS NOT NULL;

-- NULL vs string kosong: keduanya BERBEDA
INSERT INTO siswa (nama, email) VALUES ('Budi', '');   -- email = string kosong
INSERT INTO siswa (nama, email) VALUES ('Ani', NULL);  -- email = tidak ada sama sekali

-- Cek keduanya sekaligus
SELECT nama,
  CASE
    WHEN email IS NULL THEN 'tidak ada email'
    WHEN email = '' THEN 'email kosong (belum diisi)'
    ELSE email
  END AS status_email
FROM siswa;

-- COALESCE: ganti NULL dengan nilai default
SELECT nama, COALESCE(skor, 0) AS skor_final FROM nilai;
Kebiasaan Abbas: Setiap kali melihat kolom yang mungkin kosong, tanyakan dua pertanyaan: apakah bisa NULL? apakah bisa string kosong? Keduanya perlu ditangani dengan cara berbeda.
Tips 3 LIMIT selalu ada saat eksplorasi awal
Efisiensi Query
Preview data dengan aman sebelum eksplorasi penuh
Saat pertama kali menjelajahi tabel yang belum dikenal, selalu tambahkan LIMIT. Di SQLite ini mungkin tidak masalah karena datanya kecil, tapi kebiasaan ini sangat penting saat nanti bekerja di BigQuery atau PostgreSQL dengan jutaan baris.
-- Pola eksplorasi data yang aman
-- Langkah 1: lihat struktur dan sampel awal
SELECT * FROM nilai LIMIT 5;

-- Langkah 2: cek distribusi data
SELECT kelas, COUNT(*) AS total_siswa
FROM siswa
GROUP BY kelas
ORDER BY total_siswa DESC;

-- Langkah 3: cek range dan anomali nilai
SELECT
  MIN(skor) AS nilai_min,
  MAX(skor) AS nilai_max,
  AVG(skor) AS nilai_rata,
  COUNT(*) AS total_baris,
  COUNT(skor) AS baris_berisi_nilai,
  COUNT(*) - COUNT(skor) AS baris_null
FROM nilai;

-- Langkah 4: baru eksplorasi detail
SELECT s.nama, n.mata_pelajaran, n.skor
FROM siswa s
JOIN nilai n ON s.id = n.siswa_id
WHERE n.skor < 50
LIMIT 20;
Urutan eksplorasi standar: (1) SELECT * LIMIT 5 untuk lihat bentuk data. (2) COUNT dan distribusi untuk pahami skala. (3) MIN/MAX/AVG untuk deteksi anomali. (4) Filter dan JOIN spesifik setelah Abbas tahu data seperti apa.
Tips 4 Baca pesan error dengan teliti sebelum panik
Debug Mindset
Empat error SQL paling umum dan artinya
Setiap error SQL memberi petunjuk lokasi masalah. Abbas perlu belajar membaca error message dengan sabar sebelum mencoba solusi random.
-- Error 1: "no such column: nama_siswa"
-- Artinya: nama kolom salah. Cek dengan:
PRAGMA table_info(siswa);  -- lihat semua kolom di tabel siswa

-- Error 2: "ambiguous column name: id"
-- Artinya: dua tabel punya kolom bernama "id" dan SQL bingung yang mana
-- Solusi: tambahkan prefix nama tabel
SELECT siswa.id, siswa.nama, nilai.id AS nilai_id
FROM siswa JOIN nilai ON siswa.id = nilai.siswa_id;

-- Error 3: "syntax error near 'FROM'"
-- Artinya: ada koma lebih di akhir SELECT list
SELECT nama, kelas  -- hapus koma di sini
FROM siswa;         -- bukan: SELECT nama, kelas, FROM siswa

-- Error 4: "misuse of aggregate function COUNT()"
-- Artinya: mencampur kolom biasa dengan agregasi tanpa GROUP BY
-- SALAH:
SELECT nama, COUNT(*) FROM siswa;
-- BENAR:
SELECT kelas, COUNT(*) AS total FROM siswa GROUP BY kelas;
Strategi debug Abbas: Baca baris pertama error message. Biasanya ada petunjuk baris dan kolom yang bermasalah. Isolasi dengan menjalankan bagian kecil query terlebih dahulu untuk menemukan bagian yang rusak.
Tips 5 Gunakan alias yang deskriptif, bukan singkatan misterius
Readability
Alias yang baik membuat query bisa dibaca 6 bulan kemudian
Alias AS bukan hanya untuk mempersingkat penulisan. Alias yang baik mendokumentasikan intent query: apa yang sedang dihitung, apa arti kolom tersebut dalam konteks bisnis.
-- Kurang baik: alias tidak menjelaskan apapun
SELECT s.nama, AVG(n.skor) AS a, COUNT(n.id) AS c
FROM siswa s JOIN nilai n ON s.id = n.siswa_id
GROUP BY s.nama;

-- Lebih baik: alias menjelaskan konteks
SELECT
  s.nama AS nama_siswa,
  AVG(n.skor) AS rata_rata_nilai,
  COUNT(n.id) AS jumlah_mata_pelajaran,
  CASE
    WHEN AVG(n.skor) >= 90 THEN 'Istimewa'
    WHEN AVG(n.skor) >= 80 THEN 'Baik'
    WHEN AVG(n.skor) >= 70 THEN 'Cukup'
    ELSE 'Perlu Perhatian'
  END AS kategori_prestasi
FROM siswa s
JOIN nilai n ON s.id = n.siswa_id
GROUP BY s.id, s.nama
ORDER BY rata_rata_nilai DESC;
Aturan alias Abbas: Jika Abbas sendiri harus berpikir sebentar untuk mengingat arti alias yang ia tulis, itu tanda alias tersebut perlu diperbaiki. Orang lain yang membaca query tidak akan tahu apa arti "a" atau "c".
Tips 6 WHERE vs HAVING: jangan tertukar
Kesalahan Klasik
WHERE menyaring baris, HAVING menyaring kelompok
WHERE dieksekusi SEBELUM GROUP BY: menyaring baris individual dari tabel. HAVING dieksekusi SETELAH GROUP BY: menyaring hasil agregasi. Mencampur keduanya adalah kesalahan yang sering terjadi.
-- SALAH: tidak bisa pakai fungsi agregat di WHERE
SELECT kelas, AVG(skor) AS rata
FROM nilai
WHERE AVG(skor) > 75  -- ini akan error!
GROUP BY kelas;

-- BENAR: fungsi agregat masuk ke HAVING
SELECT kelas, AVG(skor) AS rata_rata
FROM nilai
GROUP BY kelas
HAVING AVG(skor) > 75
ORDER BY rata_rata DESC;

-- Kombinasi WHERE dan HAVING (keduanya boleh ada bersamaan)
-- WHERE dulu (saring baris): hanya nilai semester genap
-- GROUP BY kemudian: kelompokkan per kelas
-- HAVING terakhir (saring kelompok): rata > 75
SELECT kelas, AVG(skor) AS rata_semester_genap
FROM nilai
WHERE semester = 'Genap'
GROUP BY kelas
HAVING AVG(skor) > 75
ORDER BY rata_semester_genap DESC;
Mnemonic Abbas: WHERE = sebelum perhitungan (filter mentah). HAVING = setelah perhitungan (filter hasil). Kalau kolom yang disaring punya fungsi COUNT/SUM/AVG/MIN/MAX, masuk ke HAVING. Kalau tidak, masuk ke WHERE.
Tips 7 Urutan eksekusi SQL berbeda dari urutan penulisan
Mental Model Penting
SQL tidak dibaca dari atas ke bawah saat dieksekusi
Memahami urutan eksekusi SQL menjelaskan mengapa alias di SELECT tidak bisa dipakai di WHERE (WHERE dieksekusi lebih dulu), dan mengapa HAVING bisa menggunakan agregasi.
-- Urutan PENULISAN (yang Abbas tulis):
SELECT kelas, COUNT(*) AS total     -- 5. pilih kolom output
FROM siswa                           -- 1. ambil dari tabel ini
JOIN nilai ON siswa.id = nilai.id   -- 2. gabungkan tabel
WHERE skor IS NOT NULL              -- 3. saring baris
GROUP BY kelas                      -- 4. kelompokkan
HAVING COUNT(*) > 5                -- 6. saring kelompok
ORDER BY total DESC                 -- 7. urutkan
LIMIT 10;                           -- 8. batasi output

-- Urutan EKSEKUSI (yang database lakukan):
-- 1. FROM: tentukan tabel sumber
-- 2. JOIN: gabungkan tabel
-- 3. WHERE: saring baris (sebelum agregasi)
-- 4. GROUP BY: kelompokkan baris
-- 5. HAVING: saring kelompok (setelah agregasi)
-- 6. SELECT: hitung ekspresi dan pilih kolom
-- 7. ORDER BY: urutkan hasil
-- 8. LIMIT: potong output

-- Itulah mengapa ini error (alias di WHERE):
SELECT skor * 1.1 AS skor_baru FROM nilai WHERE skor_baru > 80;
-- skor_baru belum ada saat WHERE dieksekusi

-- Solusinya: ulangi ekspresi di WHERE
SELECT skor * 1.1 AS skor_baru FROM nilai WHERE skor * 1.1 > 80;
Insight: Setiap kali Abbas bingung kenapa sebuah query error, tanyakan: pada urutan eksekusi ke berapa baris ini diproses? Apakah data yang dibutuhkan sudah tersedia pada tahap itu?
Tips 8 Dokumentasikan query penting dengan komentar
Kebiasaan Profesional
Komentar SQL adalah dokumentasi mini yang menyelamatkan
Query yang Abbas tulis hari ini, seminggu lagi mungkin terasa asing. Komentar singkat di bagian penting membuat query bisa dimengerti kembali dalam hitungan detik.
-- ============================================================
-- Laporan: Siswa Berpotensi Tidak Naik Kelas
-- Dibuat: 2025-01-15
-- Logika: rata-rata < 65 ATAU ada mata pelajaran dengan skor < 40
-- ============================================================

WITH rata_per_siswa AS (
  -- Hitung rata-rata nilai setiap siswa
  SELECT
    siswa_id,
    AVG(skor) AS rata_rata,
    MIN(skor) AS nilai_terendah
  FROM nilai
  WHERE semester = 'Ganjil'
  GROUP BY siswa_id
),
siswa_berisiko AS (
  -- Filter siswa yang memenuhi kriteria berisiko
  SELECT siswa_id
  FROM rata_per_siswa
  WHERE rata_rata < 65     -- kriteria 1: rata-rata di bawah KKM
     OR nilai_terendah < 40  -- kriteria 2: ada nilai sangat rendah
)
-- Output final: nama siswa dan detail nilai mereka
SELECT
  s.nama,
  s.kelas,
  r.rata_rata,
  r.nilai_terendah
FROM siswa s
JOIN rata_per_siswa r ON s.id = r.siswa_id
WHERE s.id IN (SELECT siswa_id FROM siswa_berisiko)
ORDER BY r.rata_rata ASC;
Template komentar Abbas: Di awal query kompleks, tulis tiga hal: tujuan query, tanggal dibuat, dan logika utama. Ini kebiasaan yang membedakan analyst amatir dan profesional.

Latihan Tambahan: Tantangan Lebih Dalam

Lima latihan ini dirancang untuk dikerjakan setelah Abbas menyelesaikan semua minggu di bulan 4. Tingkat kesulitan naik bertahap. Tidak ada kunci jawaban di sini, tetapi ada petunjuk arah yang cukup untuk Abbas menemukan solusinya sendiri.

Latihan A Audit Kualitas Data: Temukan Semua Anomali
Level: Menengah
Buat laporan data quality yang komprehensif
Sebelum menganalisis data, analyst wajib memahami kondisi data tersebut. Latihan ini mensimulasikan pekerjaan audit data quality pertama yang akan Abbas hadapi di dunia nyata.
-- Setup: buat tabel dengan data kotor untuk diaudit
CREATE TABLE IF NOT EXISTS siswa_raw (
  id INTEGER PRIMARY KEY,
  nama TEXT,
  kelas TEXT,
  email TEXT,
  tgl_lahir TEXT,
  nilai_uts TEXT,  -- sengaja TEXT, bukan INTEGER
  nilai_uas TEXT
);

INSERT INTO siswa_raw VALUES
(1, 'Ahmad Fauzi', '10-A', 'ahmad@email.com', '2008-03-15', '85', '90'),
(2, 'Budi Santoso', '10-A', 'budi@email.com', '2008-07-22', '72', NULL),
(3, '  Citra Dewi  ', '10-B', 'CITRA@EMAIL.COM', '2009/01/10', '88', '91'),
(4, 'Ahmad Fauzi', '10-A', 'ahmad@email.com', '2008-03-15', '85', '90'),
(5, 'Dani Pratama', 'kelas 10C', 'dani.email', '10-Nov-2008', 'delapan puluh', '78'),
(6, 'Eka Rahayu', '10-B', '', '2008-12-05', '95', '97'),
(7, 'Fajar Nugroho', '10-C', NULL, '2008-06-18', '65', '0'),
(8, NULL, '10-A', 'noname@email.com', '2008-09-30', '77', '82');

-- TUGAS Abbas: tulis satu query untuk setiap masalah berikut:

-- A1: Temukan semua baris yang punya NULL di kolom manapun
-- (petunjuk: pakai beberapa kondisi IS NULL dengan OR)

-- A2: Temukan duplikat berdasarkan nama + kelas + email
-- (petunjuk: GROUP BY + HAVING COUNT(*) > 1)

-- A3: Temukan email yang formatnya tidak valid
-- (petunjuk: email valid harus mengandung '@' dan '.')
-- Gunakan: NOT LIKE '%@%.%'

-- A4: Temukan nilai_uts yang tidak bisa dikonversi ke angka
-- (petunjuk: CAST(nilai_uts AS REAL) = 0 tapi nilai_uts != '0')

-- A5: Buat summary audit dalam satu query besar
SELECT
  'Total baris' AS metrik, COUNT(*) AS jumlah FROM siswa_raw
UNION ALL
SELECT 'Nama NULL', COUNT(*) FROM siswa_raw WHERE nama IS NULL
UNION ALL
SELECT 'Email NULL atau kosong', COUNT(*) FROM siswa_raw WHERE email IS NULL OR email = ''
UNION ALL
SELECT 'Nilai UTS tidak valid', COUNT(*) FROM siswa_raw WHERE CAST(nilai_uts AS REAL) = 0 AND nilai_uts != '0'
UNION ALL
SELECT 'Duplikat (berdasar nama+kelas)', COUNT(*) - COUNT(DISTINCT nama || kelas) FROM siswa_raw;
Tantangan bonus: Setelah audit, buat tabel siswa_clean yang sudah dibersihkan dari semua anomali di atas. Gunakan INSERT INTO siswa_clean SELECT ... dengan transformasi yang tepat: TRIM nama, LOWER email, CAST nilai, hapus duplikat dengan ROW_NUMBER().
Latihan B Laporan Rekap Nilai Semester dengan Ranking
Level: Menengah-Tinggi
Gabungkan JOIN, agregasi, CASE WHEN, dan ranking dalam satu query
Ini adalah tipe query yang paling sering diminta wali kelas atau kepala sekolah: laporan komprehensif yang bisa langsung dicetak. Abbas harus menggabungkan semua teknik yang sudah dipelajari.
-- Dataset yang dibutuhkan (jalankan setup ini dulu)
CREATE TABLE IF NOT EXISTS guru (
  id INTEGER PRIMARY KEY,
  nama TEXT NOT NULL,
  mata_pelajaran TEXT NOT NULL
);

CREATE TABLE IF NOT EXISTS siswa (
  id INTEGER PRIMARY KEY,
  nama TEXT NOT NULL,
  kelas TEXT NOT NULL,
  nisn TEXT UNIQUE
);

CREATE TABLE IF NOT EXISTS nilai (
  id INTEGER PRIMARY KEY,
  siswa_id INTEGER REFERENCES siswa(id),
  guru_id INTEGER REFERENCES guru(id),
  mata_pelajaran TEXT NOT NULL,
  skor INTEGER CHECK(skor BETWEEN 0 AND 100),
  semester TEXT CHECK(semester IN ('Ganjil', 'Genap')),
  tahun_ajaran TEXT
);

-- Insert data sample
INSERT OR IGNORE INTO guru VALUES
(1,'Bu Ani','Matematika'),(2,'Pak Budi','Bahasa Indonesia'),
(3,'Bu Cici','IPA'),(4,'Pak Dedi','IPS'),(5,'Bu Eka','Bahasa Inggris');

INSERT OR IGNORE INTO siswa VALUES
(1,'Ahmad Fauzi','10-A','3301010001'),(2,'Budi Santoso','10-A','3301010002'),
(3,'Citra Dewi','10-B','3301010003'),(4,'Dani Pratama','10-B','3301010004'),
(5,'Eka Rahayu','10-A','3301010005');

INSERT OR IGNORE INTO nilai VALUES
(1,1,1,'Matematika',85,'Ganjil','2024/2025'),
(2,1,2,'Bahasa Indonesia',78,'Ganjil','2024/2025'),
(3,1,3,'IPA',90,'Ganjil','2024/2025'),
(4,2,1,'Matematika',72,'Ganjil','2024/2025'),
(5,2,2,'Bahasa Indonesia',88,'Ganjil','2024/2025'),
(6,3,1,'Matematika',95,'Ganjil','2024/2025'),
(7,3,3,'IPA',91,'Ganjil','2024/2025'),
(8,4,2,'Bahasa Indonesia',65,'Ganjil','2024/2025'),
(9,4,4,'IPS',70,'Ganjil','2024/2025'),
(10,5,1,'Matematika',88,'Ganjil','2024/2025'),
(11,5,5,'Bahasa Inggris',92,'Ganjil','2024/2025');

-- QUERY UTAMA: Laporan rekap dengan ranking per kelas
WITH rekap_siswa AS (
  SELECT
    s.id,
    s.nama,
    s.kelas,
    s.nisn,
    COUNT(n.id) AS jumlah_mapel,
    ROUND(AVG(n.skor), 1) AS rata_rata,
    MIN(n.skor) AS nilai_terendah,
    MAX(n.skor) AS nilai_tertinggi
  FROM siswa s
  LEFT JOIN nilai n ON s.id = n.siswa_id AND n.semester = 'Ganjil'
  GROUP BY s.id, s.nama, s.kelas, s.nisn
)
SELECT
  r.kelas,
  r.nama,
  r.nisn,
  r.jumlah_mapel,
  r.rata_rata,
  r.nilai_terendah,
  r.nilai_tertinggi,
  CASE
    WHEN r.rata_rata >= 90 THEN 'A'
    WHEN r.rata_rata >= 80 THEN 'B'
    WHEN r.rata_rata >= 70 THEN 'C'
    WHEN r.rata_rata >= 60 THEN 'D'
    ELSE 'E'
  END AS grade,
  -- Ranking dalam kelas (SQLite tidak punya RANK() tapi bisa pakai subquery)
  (SELECT COUNT(*) + 1 FROM rekap_siswa r2
   WHERE r2.kelas = r.kelas AND r2.rata_rata > r.rata_rata) AS rank_kelas
FROM rekap_siswa r
ORDER BY r.kelas ASC, rank_kelas ASC;
Catatan teknis: Subquery untuk ranking di dalam SELECT (correlated subquery) bekerja tapi lambat untuk data besar. Di PostgreSQL, Abbas bisa menggantinya dengan window function RANK() OVER (PARTITION BY kelas ORDER BY rata_rata DESC) yang jauh lebih efisien.
Latihan C Analisis Guru Paling Banyak Mengajar
Level: Tinggi
Multi-level aggregation dengan CTE berantai
Latihan ini melatih kemampuan membangun query bertahap menggunakan beberapa CTE yang saling bergantung. Teknik ini adalah fondasi untuk query analisis yang kompleks di dunia nyata.
-- Gunakan tabel guru, siswa, nilai dari Latihan B

-- QUERY: Analisis beban mengajar dan efektivitas guru
WITH beban_guru AS (
  -- CTE 1: hitung beban mengajar setiap guru
  SELECT
    g.id AS guru_id,
    g.nama AS nama_guru,
    g.mata_pelajaran,
    COUNT(DISTINCT n.siswa_id) AS jumlah_siswa_diajar,
    COUNT(n.id) AS total_nilai_diinput,
    ROUND(AVG(n.skor), 1) AS rata_nilai_kelas
  FROM guru g
  LEFT JOIN nilai n ON g.id = n.guru_id
  GROUP BY g.id, g.nama, g.mata_pelajaran
),
statistik_mapel AS (
  -- CTE 2: rata-rata keseluruhan per mata pelajaran
  SELECT
    mata_pelajaran,
    ROUND(AVG(skor), 1) AS rata_global_mapel
  FROM nilai
  GROUP BY mata_pelajaran
),
perbandingan AS (
  -- CTE 3: bandingkan rata kelas guru dengan rata global mapel
  SELECT
    b.nama_guru,
    b.mata_pelajaran,
    b.jumlah_siswa_diajar,
    b.rata_nilai_kelas,
    s.rata_global_mapel,
    ROUND(b.rata_nilai_kelas - s.rata_global_mapel, 1) AS selisih_dari_rata_global
  FROM beban_guru b
  JOIN statistik_mapel s ON b.mata_pelajaran = s.mata_pelajaran
)
-- OUTPUT: tampilkan dengan interpretasi
SELECT
  nama_guru,
  mata_pelajaran,
  jumlah_siswa_diajar,
  rata_nilai_kelas,
  rata_global_mapel,
  selisih_dari_rata_global,
  CASE
    WHEN selisih_dari_rata_global > 5 THEN 'Di atas rata-rata'
    WHEN selisih_dari_rata_global < -5 THEN 'Di bawah rata-rata'
    ELSE 'Sesuai rata-rata'
  END AS performa_relatif
FROM perbandingan
ORDER BY selisih_dari_rata_global DESC;
Perpanjangan latihan: Tambahkan CTE keempat bernama "flagging" yang menandai guru dengan jumlah_siswa_diajar di bawah 3 sebagai 'Data tidak cukup untuk evaluasi'. Filter hasil akhir agar hanya menampilkan guru dengan data yang cukup.
Latihan D Pipeline Cleaning: Dari Data Kotor ke Laporan Bersih
Level: Tinggi
Simulasi kerja nyata: cleaning pipeline lengkap dalam SQL
Latihan ini mensimulasikan alur kerja lengkap: terima data mentah kotor, audit, bersihkan, validasi, lalu hasilkan laporan. Ini adalah pola yang akan Abbas ulang ratusan kali sebagai data analyst.
-- FASE 1: Terima data mentah
CREATE TABLE IF NOT EXISTS import_nilai_raw (
  row_num INTEGER PRIMARY KEY,
  nama_input TEXT,
  kelas_input TEXT,
  mapel_input TEXT,
  skor_input TEXT,  -- sengaja TEXT
  catatan TEXT
);

INSERT OR IGNORE INTO import_nilai_raw VALUES
(1,'Ahmad Fauzi','10A','MTK','87',NULL),
(2,'budi santoso','10-A','Matematika','72',NULL),
(3,'CITRA DEWI','10B','mtk','','nilai belum masuk'),
(4,'Dani Pratama','10-B','Matematika','101','nilai di atas 100?'),
(5,'eka rahayu','10-A','MTK','88',NULL),
(6,'Ahmad Fauzi','10A','MTK','87',NULL);  -- duplikat baris 1

-- FASE 2: Audit (jalankan ini dan catat hasilnya)
SELECT
  'Total baris import' AS cek, COUNT(*) AS hasil FROM import_nilai_raw
UNION ALL
SELECT 'Skor kosong atau NULL', COUNT(*) FROM import_nilai_raw
  WHERE skor_input IS NULL OR TRIM(skor_input) = ''
UNION ALL
SELECT 'Skor di luar 0-100', COUNT(*) FROM import_nilai_raw
  WHERE CAST(skor_input AS REAL) NOT BETWEEN 0 AND 100
  AND TRIM(skor_input) != ''
UNION ALL
SELECT 'Kemungkinan duplikat', COUNT(*) - COUNT(DISTINCT UPPER(TRIM(nama_input)) || kelas_input || mapel_input)
  FROM import_nilai_raw;

-- FASE 3: Cleaning dan normalisasi
CREATE TABLE IF NOT EXISTS nilai_cleaned AS
WITH deduplicated AS (
  SELECT *,
    ROW_NUMBER() OVER (
      PARTITION BY UPPER(TRIM(nama_input)), kelas_input, mapel_input
      ORDER BY row_num ASC
    ) AS rn
  FROM import_nilai_raw
  WHERE TRIM(skor_input) != ''
    AND skor_input IS NOT NULL
    AND CAST(skor_input AS REAL) BETWEEN 0 AND 100
)
SELECT
  -- Normalisasi nama: title case sederhana (huruf pertama kapital)
  UPPER(SUBSTR(TRIM(nama_input), 1, 1)) ||
    LOWER(SUBSTR(TRIM(nama_input), 2)) AS nama_bersih,
  -- Normalisasi kelas: hapus format tidak standar
  REPLACE(REPLACE(kelas_input, ' ', ''), 'kelas', '') AS kelas_bersih,
  -- Normalisasi nama mapel ke format standar
  CASE UPPER(TRIM(mapel_input))
    WHEN 'MTK' THEN 'Matematika'
    WHEN 'MATEMATIKA' THEN 'Matematika'
    WHEN 'BIN' THEN 'Bahasa Indonesia'
    ELSE mapel_input
  END AS mapel_bersih,
  CAST(skor_input AS INTEGER) AS skor
FROM deduplicated
WHERE rn = 1;

-- FASE 4: Validasi hasil cleaning
SELECT 'Baris setelah cleaning' AS validasi, COUNT(*) AS hasil FROM nilai_cleaned
UNION ALL
SELECT 'Range skor valid', COUNT(*) FROM nilai_cleaned WHERE skor BETWEEN 0 AND 100
UNION ALL
SELECT 'Duplikat tersisa', COUNT(*) - COUNT(DISTINCT nama_bersih || kelas_bersih || mapel_bersih)
  FROM nilai_cleaned;
Pelajaran kunci latihan D: Perhatikan bahwa Abbas tidak mengubah tabel asli (import_nilai_raw). Prinsip non-destructive cleaning: selalu pertahankan data mentah apa adanya, hasilkan tabel baru yang sudah bersih. Ini memungkinkan audit trail dan pemulihan jika ada kesalahan logika cleaning.
Latihan E Self-Assessment: 10 Soal Campuran Bulan 4
Level: Mixed
Kerjakan tanpa melihat catatan, ukur pemahaman Abbas sendiri
Gunakan tabel siswa, nilai, dan guru dari Latihan B. Kerjakan satu per satu, baru cek jawabannya dengan menjalankan query di DB Browser. Target Abbas: bisa menjawab 8 dari 10 dengan benar.
-- Soal 1 (Basic): Tampilkan semua siswa kelas 10-A
-- diurutkan alfabetis berdasarkan nama.

-- Soal 2 (Filter): Siapa saja siswa yang nilai Matematikanya
-- lebih tinggi dari nilai rata-rata Matematika semua siswa?
-- (gunakan subquery)

-- Soal 3 (Agregasi): Berapa rata-rata nilai per mata pelajaran,
-- tampilkan hanya mapel dengan rata-rata di atas 75?

-- Soal 4 (JOIN): Tampilkan nama siswa, mata pelajaran, nama guru,
-- dan skor. Urutkan dari skor tertinggi.

-- Soal 5 (NULL): Tampilkan siswa yang belum punya nilai sama sekali.
-- (petunjuk: LEFT JOIN + IS NULL)

-- Soal 6 (CASE WHEN): Tambahkan kolom "status" ke setiap baris nilai:
-- Skor >= 75 = "Lulus", Skor < 75 = "Remedi"

-- Soal 7 (String): Tampilkan nama siswa dalam format
-- "NAMA (KELAS)" misalnya "AHMAD FAUZI (10-A)".
-- (petunjuk: UPPER, ||, nama, kelas)

-- Soal 8 (Subquery): Tampilkan nama guru yang mengajar siswa
-- dengan nilai tertinggi di mata pelajaran masing-masing.

-- Soal 9 (Cleaning): Siswa dengan nama yang mengandung spasi
-- lebih dari satu berturut-turut. Tampilkan nama asli dan nama setelah
-- diperbaiki. (petunjuk: REPLACE(nama, '  ', ' '))

-- Soal 10 (CTE): Buat CTE bernama "top_per_kelas" yang berisi
-- satu siswa dengan nilai tertinggi per kelas, lalu tampilkan
-- nama, kelas, dan rata-rata nilai mereka.
Cara mengukur pemahaman Abbas: 10/10 benar: siap lanjut ke Bulan 5 (Visualisasi). 8-9 benar: review sekali lagi bagian yang salah. 6-7 benar: ulangi minggu yang relevan. Di bawah 6: kerjakan ulang semua latihan minggu 1-4 sebelum lanjut.