SQL dan Data Cleaning
Kuasai bahasa query database universal dan teknik pembersihan data agar setiap dataset siap dianalisis oleh Abbas.
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)
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.
-- 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
-- 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;
-- 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%';
-- 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;
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.
-- 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;
-- 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;
-- 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;
-- 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;
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.
-- 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;
-- 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;
-- 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;
-- 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;
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.
-- 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;
-- 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;
-- 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;
-- 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;
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.
Tools SQL untuk Abbas
Mulai dari SQLite untuk latihan lokal di MacBook Air M5, hingga BigQuery untuk merasakan SQL di data skala produksi nyata.
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.
-- 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';
-- 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;
-- 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;
-- 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;
-- 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;
-- 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;
-- 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;
-- ============================================================
-- 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;
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.
-- 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;
-- 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;
-- 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;
-- 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;
-- 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.