TL;DR
Data cleaning itu 80% waktu kerja Data Analyst. 10 teknik yang wajib dikuasai: handling NULL (COALESCE, NULLIF), hapus duplikat (DISTINCT, ROW_NUMBER), standardisasi text (TRIM, UPPER), fix typos, validasi format, handle outliers, type conversion, date fixing, merge data, dan bikin clean dimension tables.
Kenapa Data Cleaning Itu Penting Banget?
"Garbage in, garbage out."
Pernah denger istilah itu kan? Mau sehebat apapun model atau dashboard kamu, kalau datanya kotor, hasilnya ya sampah juga.
Faktanya, Data Analyst ngabisin sekitar 80% waktunya buat data cleaning dan preparation. Cuma 20% buat analisis yang "keren-keren". Agak nyebelin sih, tapi ini realita.
Data kotor itu bisa berbentuk macam-macam: NULL values yang ga jelas, duplikat data, typo, format yang ga konsisten, outliers aneh, dan masih banyak lagi. Kalau ga di-handle dengan bener, analisis kamu bisa salah total.
Kamu bakal belajar 10 teknik data cleaning yang wajib dikuasai. Semua pakai SQL, dan contohnya pakai dataset Indonesia biar lebih relate.
Apa yang Akan Kamu Pelajari
- [ ] Teknik 1: Handling NULL values
- [ ] Teknik 2: Menghapus duplikat
- [ ] Teknik 3: Standardisasi text
- [ ] Teknik 4: Fixing typos
- [ ] Teknik 5: Validasi format data
- [ ] Teknik 6: Handling outliers
- [ ] Teknik 7: Data type conversion
- [ ] Teknik 8: Fixing inconsistent dates
- [ ] Teknik 9: Merging fragmented data
- [ ] Teknik 10: Creating clean dimension tables
Dataset yang Akan Kita Pakai
Kamu dapet dataset hasil survey pelanggan. Datanya berantakan banget karena input manual.
Tabel: survey_raw
| id | nama | kota | no_hp | tanggal_lahir | pendapatan | tanggal_survey | |
|---|---|---|---|---|---|---|---|
| 1 | Budi Santoso | budi@email.com | Jakarta | 081234567890 | 1990-05-15 | 15000000 | 2024-01-15 |
| 2 | SITI RAHAYU | siti@email.com | jakarta | 08-2345-6789 | 15/08/1995 | NULL | 2024-01-15 |
| 3 | andi wijaya | NULL | Bandung | 6283456789012 | 1988-12-03 | 25000000 | 2024-01-16 |
| 4 | Budi Santoso | budi@email.com | Jakarta | 081234567890 | 1990-05-15 | 15000000 | 2024-01-15 |
| 5 | Dewi Lestari | dewi@gmail.com | Jakrta | 0811-234-5678 | 03-02-1992 | 8500000 | 2024-01-16 |
| 6 | Maya Putri | maya@email | Bandung | 812345678 | 1997/07/20 | 50000000000 | 2024-01-17 |
| 7 | Rudi Hartono | rudi@email.com | Surabaya | +62 856 1234 5678 | NULL | 12000000 | 2024-01-17 |
| 8 | NULL | unknown@test.com | Medan | 081234567890 | 1985-11-11 | -5000000 | 2024-01-18 |
| 9 | Agus Pratama | agus@email.com | bandung | tidak punya hp | 2024-06-15 | 9000000 | 2024-01-18 |
| 10 | Linda Sari | linda@email.com | SEMARANG | 085678901234 | 1993-04-25 | 0 | 2024-01-19 |
Liat kan betapa berantakannya? Ada nama yang uppercase, lowercase, mixed. Kota yang typo. Format HP yang ga konsisten. Tanggal lahir yang formatnya beda-beda. Pendapatan yang aneh. Kita beresin satu per satu.
Teknik 1: Handling NULL Values
NULL itu bukan 0, bukan empty string, tapi "nggak ada nilai". Cara handle-nya tergantung konteks.
COALESCE: Ganti NULL dengan Default Value
SELECT
id,
COALESCE(nama, 'Tidak Diketahui') AS nama,
COALESCE(email, 'tidak.ada@email.com') AS email,
COALESCE(pendapatan, 0) AS pendapatan
FROM survey_raw;
COALESCE bakal return nilai pertama yang bukan NULL. Bisa juga chain multiple values:
SELECT
id,
COALESCE(nama, email, 'Anonymous') AS identifier
FROM survey_raw;
NULLIF: Ubah Nilai Tertentu Jadi NULL
Kadang ada nilai yang harusnya NULL tapi diisi dengan placeholder kayak 0, -1, atau 'N/A'.
SELECT
id,
nama,
NULLIF(pendapatan, 0) AS pendapatan, -- 0 jadi NULL
NULLIF(no_hp, 'tidak punya hp') AS no_hp -- placeholder jadi NULL
FROM survey_raw;
Cek NULL Count per Kolom
Sebelum cleaning, selalu cek dulu seberapa banyak NULL di setiap kolom:
SELECT
COUNT(*) AS total_rows,
COUNT(*) - COUNT(nama) AS null_nama,
COUNT(*) - COUNT(email) AS null_email,
COUNT(*) - COUNT(kota) AS null_kota,
COUNT(*) - COUNT(no_hp) AS null_no_hp,
COUNT(*) - COUNT(tanggal_lahir) AS null_tgl_lahir,
COUNT(*) - COUNT(pendapatan) AS null_pendapatan
FROM survey_raw;
Hasil:
| total_rows | null_nama | null_email | null_kota | null_no_hp | null_tgl_lahir | null_pendapatan |
|---|---|---|---|---|---|---|
| 10 | 1 | 1 | 0 | 0 | 1 | 1 |
Teknik 2: Menghapus Duplikat
Identifikasi Duplikat
Pertama, cari dulu row yang duplikat:
SELECT
nama,
email,
COUNT(*) AS jumlah
FROM survey_raw
GROUP BY nama, email
HAVING COUNT(*) > 1;
Hasil:
| nama | jumlah | |
|---|---|---|
| Budi Santoso | budi@email.com | 2 |
Hapus Duplikat dengan ROW_NUMBER
WITH ranked AS (
SELECT
*,
ROW_NUMBER() OVER (
PARTITION BY nama, email
ORDER BY tanggal_survey DESC
) AS rn
FROM survey_raw
)
SELECT *
FROM ranked
WHERE rn = 1;
Query ini keep row terbaru (tanggal_survey paling recent) dan buang sisanya.
Hapus Duplikat dengan DISTINCT
Kalau mau simple dan semua kolom sama persis:
SELECT DISTINCT *
FROM survey_raw;
Tapi hati-hati, DISTINCT ngebandingin SEMUA kolom. Kalau ada satu kolom beda (misal id), ga bakal dianggap duplikat.
Teknik 3: Standardisasi Text
TRIM: Hapus Spasi di Awal/Akhir
SELECT
id,
TRIM(kota) AS kota_clean
FROM survey_raw;
Contoh: ' Surabaya ' jadi 'Surabaya'
UPPER / LOWER: Standardisasi Case
SELECT
id,
UPPER(nama) AS nama_upper,
LOWER(email) AS email_lower,
INITCAP(nama) AS nama_proper -- PostgreSQL: capitalize each word
FROM survey_raw;
Kombinasi untuk Standardisasi Kota
SELECT
id,
INITCAP(TRIM(LOWER(kota))) AS kota_standard
FROM survey_raw;
Hasil transformasi:
- 'jakarta' → 'Jakarta'
- 'SEMARANG' → 'Semarang'
- ' Surabaya ' → 'Surabaya'
Teknik 4: Fixing Typos dengan CASE WHEN
Untuk typo yang udah diketahui, pake CASE WHEN:
SELECT
id,
CASE TRIM(LOWER(kota))
WHEN 'jakrta' THEN 'Jakarta'
WHEN 'jkt' THEN 'Jakarta'
WHEN 'bandung' THEN 'Bandung'
WHEN 'bdg' THEN 'Bandung'
WHEN 'sby' THEN 'Surabaya'
WHEN 'surabaya' THEN 'Surabaya'
WHEN 'semarang' THEN 'Semarang'
WHEN 'smg' THEN 'Semarang'
WHEN 'medan' THEN 'Medan'
ELSE INITCAP(TRIM(kota))
END AS kota_clean
FROM survey_raw;
Bikin Mapping Table (Best Practice)
Untuk production, lebih baik bikin lookup table:
-- Tabel mapping
CREATE TABLE kota_mapping (
kota_dirty VARCHAR(100),
kota_clean VARCHAR(100)
);
INSERT INTO kota_mapping VALUES
('jakrta', 'Jakarta'),
('jkt', 'Jakarta'),
('jakarta', 'Jakarta'),
('bdg', 'Bandung'),
('bandung', 'Bandung'),
('sby', 'Surabaya'),
('surabaya', 'Surabaya');
-- Pakai JOIN untuk cleaning
SELECT
s.id,
COALESCE(m.kota_clean, INITCAP(TRIM(s.kota))) AS kota_clean
FROM survey_raw s
LEFT JOIN kota_mapping m ON LOWER(TRIM(s.kota)) = m.kota_dirty;
Teknik 5: Validasi Format Data
Validasi Email dengan REGEXP (PostgreSQL)
SELECT
id,
email,
CASE
WHEN email ~ '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$' THEN 'Valid'
ELSE 'Invalid'
END AS email_status
FROM survey_raw;
Hasil:
| id | email_status | |
|---|---|---|
| 1 | budi@email.com | Valid |
| 6 | maya@email | Invalid |
Validasi Format Nomor HP
SELECT
id,
no_hp,
CASE
WHEN no_hp ~ '^(\\+62|62|0)[0-9]{9,12}$' THEN 'Valid'
WHEN no_hp ~ '^[0-9\\-\\s\\+]+$' THEN 'Needs Formatting'
ELSE 'Invalid'
END AS hp_status
FROM survey_raw;
Standardisasi Nomor HP Indonesia
SELECT
id,
no_hp AS original,
-- Remove semua karakter non-digit
REGEXP_REPLACE(no_hp, '[^0-9]', '', 'g') AS digits_only,
-- Format ke standard Indonesia (08xxx)
CASE
WHEN REGEXP_REPLACE(no_hp, '[^0-9]', '', 'g') ~ '^62' THEN
'0' || SUBSTRING(REGEXP_REPLACE(no_hp, '[^0-9]', '', 'g') FROM 3)
WHEN REGEXP_REPLACE(no_hp, '[^0-9]', '', 'g') ~ '^8' THEN
'0' || REGEXP_REPLACE(no_hp, '[^0-9]', '', 'g')
ELSE
REGEXP_REPLACE(no_hp, '[^0-9]', '', 'g')
END AS hp_standard
FROM survey_raw
WHERE no_hp IS NOT NULL
AND no_hp !~ '[a-zA-Z]'; -- Exclude yang ada huruf
Hasil:
| id | original | digits_only | hp_standard |
|---|---|---|---|
| 1 | 081234567890 | 081234567890 | 081234567890 |
| 2 | 08-2345-6789 | 0823456789 | 0823456789 |
| 3 | 6283456789012 | 6283456789012 | 083456789012 |
| 7 | +62 856 1234 5678 | 628561234567 | 08561234567 |
Teknik 6: Handling Outliers
Identifikasi Outliers dengan Percentile
WITH stats AS (
SELECT
PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY pendapatan) AS q1,
PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY pendapatan) AS q3
FROM survey_raw
WHERE pendapatan IS NOT NULL AND pendapatan > 0
)
SELECT
s.id,
s.nama,
s.pendapatan,
CASE
WHEN s.pendapatan < (st.q1 - 1.5 * (st.q3 - st.q1)) THEN 'Low Outlier'
WHEN s.pendapatan > (st.q3 + 1.5 * (st.q3 - st.q1)) THEN 'High Outlier'
ELSE 'Normal'
END AS outlier_status
FROM survey_raw s
CROSS JOIN stats st
WHERE s.pendapatan IS NOT NULL;
Cap Outliers dengan LEAST/GREATEST
WITH bounds AS (
SELECT
PERCENTILE_CONT(0.05) WITHIN GROUP (ORDER BY pendapatan) AS lower_bound,
PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY pendapatan) AS upper_bound
FROM survey_raw
WHERE pendapatan IS NOT NULL AND pendapatan > 0
)
SELECT
id,
nama,
pendapatan AS original,
GREATEST(LEAST(pendapatan, b.upper_bound), b.lower_bound) AS capped_pendapatan
FROM survey_raw s
CROSS JOIN bounds b
WHERE pendapatan IS NOT NULL;
Filter Nilai yang Ga Masuk Akal
SELECT *
FROM survey_raw
WHERE pendapatan > 0 -- Ga mungkin negatif
AND pendapatan < 1000000000 -- Max 1 Milyar (reasonable)
AND (tanggal_lahir IS NULL OR tanggal_lahir < CURRENT_DATE) -- Ga mungkin lahir di masa depan
AND (tanggal_lahir IS NULL OR tanggal_lahir > '1920-01-01'); -- Ga mungkin > 100 tahun
Teknik 7: Data Type Conversion
CAST dan :: (PostgreSQL)
SELECT
id,
CAST(pendapatan AS NUMERIC) AS pendapatan_numeric,
pendapatan::TEXT AS pendapatan_text,
pendapatan::DECIMAL(15,2) AS pendapatan_decimal
FROM survey_raw;
Safe Conversion dengan TRY_CAST (SQL Server) / NULLIF
-- PostgreSQL approach: handle error dengan CASE
SELECT
id,
no_hp,
CASE
WHEN no_hp ~ '^[0-9]+$' THEN no_hp::BIGINT
ELSE NULL
END AS no_hp_numeric
FROM survey_raw;
Convert String ke Date
SELECT
id,
tanggal_lahir AS original,
CASE
-- Format: YYYY-MM-DD
WHEN tanggal_lahir ~ '^[0-9]{4}-[0-9]{2}-[0-9]{2}$' THEN
tanggal_lahir::DATE
-- Format: DD/MM/YYYY
WHEN tanggal_lahir ~ '^[0-9]{2}/[0-9]{2}/[0-9]{4}$' THEN
TO_DATE(tanggal_lahir, 'DD/MM/YYYY')
-- Format: DD-MM-YYYY
WHEN tanggal_lahir ~ '^[0-9]{2}-[0-9]{2}-[0-9]{4}$' THEN
TO_DATE(tanggal_lahir, 'DD-MM-YYYY')
-- Format: YYYY/MM/DD
WHEN tanggal_lahir ~ '^[0-9]{4}/[0-9]{2}/[0-9]{2}$' THEN
TO_DATE(tanggal_lahir, 'YYYY/MM/DD')
ELSE NULL
END AS tanggal_lahir_clean
FROM survey_raw;
Teknik 8: Fixing Inconsistent Dates
Identifikasi Format Tanggal yang Berbeda
SELECT
tanggal_lahir,
CASE
WHEN tanggal_lahir ~ '^[0-9]{4}-[0-9]{2}-[0-9]{2}' THEN 'ISO (YYYY-MM-DD)'
WHEN tanggal_lahir ~ '^[0-9]{2}/[0-9]{2}/[0-9]{4}' THEN 'DD/MM/YYYY'
WHEN tanggal_lahir ~ '^[0-9]{2}-[0-9]{2}-[0-9]{4}' THEN 'DD-MM-YYYY'
WHEN tanggal_lahir ~ '^[0-9]{4}/[0-9]{2}/[0-9]{2}' THEN 'YYYY/MM/DD'
ELSE 'Unknown'
END AS format_detected,
COUNT(*) AS count
FROM survey_raw
WHERE tanggal_lahir IS NOT NULL
GROUP BY tanggal_lahir,
CASE
WHEN tanggal_lahir ~ '^[0-9]{4}-[0-9]{2}-[0-9]{2}' THEN 'ISO (YYYY-MM-DD)'
WHEN tanggal_lahir ~ '^[0-9]{2}/[0-9]{2}/[0-9]{4}' THEN 'DD/MM/YYYY'
WHEN tanggal_lahir ~ '^[0-9]{2}-[0-9]{2}-[0-9]{4}' THEN 'DD-MM-YYYY'
WHEN tanggal_lahir ~ '^[0-9]{4}/[0-9]{2}/[0-9]{2}' THEN 'YYYY/MM/DD'
ELSE 'Unknown'
END;
Validasi Tanggal yang Masuk Akal
WITH parsed_dates AS (
SELECT
id,
nama,
tanggal_lahir AS original,
-- Parse ke DATE (simplified)
CASE
WHEN tanggal_lahir ~ '^[0-9]{4}-[0-9]{2}-[0-9]{2}$'
THEN tanggal_lahir::DATE
ELSE NULL
END AS parsed_date
FROM survey_raw
)
SELECT
*,
CASE
WHEN parsed_date IS NULL THEN 'Parse Error'
WHEN parsed_date > CURRENT_DATE THEN 'Future Date (Invalid)'
WHEN parsed_date < '1920-01-01' THEN 'Too Old (Invalid)'
WHEN EXTRACT(YEAR FROM AGE(parsed_date)) < 18 THEN 'Under 18'
ELSE 'Valid'
END AS validation_status
FROM parsed_dates;
Teknik 9: Merging Fragmented Data
Kadang data yang harusnya satu record malah ke-split jadi beberapa row.
Contoh: Aggregate String Values
-- Tabel dengan multiple rows per customer
CREATE TABLE customer_phones (
customer_id INT,
phone_number VARCHAR(20)
);
INSERT INTO customer_phones VALUES
(1, '081234567890'),
(1, '082345678901'),
(2, '083456789012');
-- Merge jadi satu row per customer
SELECT
customer_id,
STRING_AGG(phone_number, ', ') AS all_phones
FROM customer_phones
GROUP BY customer_id;
Hasil:
| customer_id | all_phones |
|---|---|
| 1 | 081234567890, 082345678901 |
| 2 | 083456789012 |
Merge dengan Prioritas
-- Ambil nilai non-NULL pertama dari multiple sources
WITH customer_data AS (
SELECT 1 AS id, 'Budi' AS nama_sistem_a, NULL AS nama_sistem_b UNION ALL
SELECT 2, NULL, 'Siti' UNION ALL
SELECT 3, 'Andi', 'Andi Wijaya'
)
SELECT
id,
COALESCE(nama_sistem_b, nama_sistem_a) AS nama_final -- Prioritas B > A
FROM customer_data;
Teknik 10: Creating Clean Dimension Tables
Best practice: bikin tabel terpisah untuk data yang udah di-clean.
Contoh: Clean Customer Dimension
CREATE TABLE dim_customer AS
WITH cleaned AS (
SELECT
id,
-- Clean nama
INITCAP(TRIM(COALESCE(nama, 'Unknown'))) AS nama,
-- Clean email
LOWER(TRIM(email)) AS email,
-- Clean kota
CASE TRIM(LOWER(kota))
WHEN 'jakrta' THEN 'Jakarta'
WHEN 'jkt' THEN 'Jakarta'
ELSE INITCAP(TRIM(kota))
END AS kota,
-- Clean no_hp
CASE
WHEN no_hp ~ '[a-zA-Z]' THEN NULL
ELSE REGEXP_REPLACE(no_hp, '[^0-9]', '', 'g')
END AS no_hp,
-- Clean pendapatan
CASE
WHEN pendapatan IS NULL THEN NULL
WHEN pendapatan <= 0 THEN NULL
WHEN pendapatan > 500000000 THEN NULL -- Cap at 500jt
ELSE pendapatan
END AS pendapatan,
tanggal_survey,
-- Dedupe
ROW_NUMBER() OVER (PARTITION BY LOWER(email) ORDER BY tanggal_survey DESC) AS rn
FROM survey_raw
WHERE email IS NOT NULL -- Email sebagai unique identifier
)
SELECT
id AS customer_id,
nama,
email,
kota,
no_hp,
pendapatan,
tanggal_survey AS last_survey_date
FROM cleaned
WHERE rn = 1; -- Ambil yang terbaru per email
Data Quality Report
Setelah cleaning, selalu bikin report untuk validasi:
SELECT
'Total Records' AS metric,
COUNT(*)::TEXT AS value
FROM dim_customer
UNION ALL
SELECT
'Unique Emails',
COUNT(DISTINCT email)::TEXT
FROM dim_customer
UNION ALL
SELECT
'Records with NULL kota',
COUNT(*)::TEXT
FROM dim_customer
WHERE kota IS NULL
UNION ALL
SELECT
'Records with valid HP',
COUNT(*)::TEXT
FROM dim_customer
WHERE no_hp IS NOT NULL AND LENGTH(no_hp) >= 10
UNION ALL
SELECT
'Average Pendapatan',
ROUND(AVG(pendapatan))::TEXT
FROM dim_customer
WHERE pendapatan IS NOT NULL;
Common Mistakes yang Harus Dihindari
Mistake 1: Cleaning di Query Langsung (Tanpa Tabel Clean)
-- KURANG BAGUS (cleaning logic di setiap query)
SELECT
INITCAP(TRIM(nama)) AS nama,
CASE LOWER(kota) WHEN 'jakrta' THEN 'Jakarta' ELSE kota END AS kota
FROM survey_raw
WHERE ...;
-- LEBIH BAGUS (bikin tabel clean sekali, pakai berkali-kali)
CREATE TABLE survey_clean AS ...;
SELECT nama, kota
FROM survey_clean
WHERE ...;
Mistake 2: Lupa Handle Edge Cases
-- POTENSI ERROR
SELECT TRIM(LOWER(nama)) -- Gimana kalau nama NULL?
FROM survey_raw;
-- LEBIH AMAN
SELECT TRIM(LOWER(COALESCE(nama, '')))
FROM survey_raw;
Mistake 3: Overwrite Data Original
Jangan pernah UPDATE langsung ke tabel original. Selalu bikin tabel baru atau backup dulu.
-- JANGAN GINI
UPDATE survey_raw SET kota = 'Jakarta' WHERE kota = 'jakrta';
-- LEBIH BAIK
CREATE TABLE survey_clean AS SELECT ... FROM survey_raw;
Checklist Data Quality
Sebelum data siap dianalisis, pastikan:
- [ ] Ga ada duplikat (atau duplikat udah di-handle)
- [ ] NULL values udah di-handle sesuai konteks
- [ ] Format text konsisten (case, spacing)
- [ ] Typo udah difix
- [ ] Data types sesuai
- [ ] Tanggal dalam format yang sama
- [ ] Outliers udah di-cap atau dibuang
- [ ] Referential integrity ok (foreign keys valid)
- [ ] Nilai berada dalam range yang masuk akal
Latihan
Soal: Dari dataset survey_raw di atas, bikin query untuk:
1. Identifikasi semua masalah data quality (NULL, duplikat, format ga konsisten)
2. Bikin versi clean dari data tersebut
Klik untuk lihat hint
1. Untuk identifikasi, pake COUNT dengan berbagai kondisi 2. Untuk cleaning, combine semua teknik: COALESCE, TRIM, CASE WHEN, ROW_NUMBER 3. Output dalam satu query pake CTEKlik untuk lihat solusi
-- Part 1: Data Quality Report
SELECT 'Total Rows' AS issue, COUNT(*)::TEXT AS count FROM survey_raw
UNION ALL
SELECT 'Duplicate Emails', COUNT(*)::TEXT FROM (
SELECT email FROM survey_raw GROUP BY email HAVING COUNT(*) > 1
) sub
UNION ALL
SELECT 'NULL Nama', COUNT(*)::TEXT FROM survey_raw WHERE nama IS NULL
UNION ALL
SELECT 'NULL Email', COUNT(*)::TEXT FROM survey_raw WHERE email IS NULL
UNION ALL
SELECT 'Invalid Email Format', COUNT(*)::TEXT FROM survey_raw
WHERE email IS NOT NULL AND email NOT LIKE '%@%.%'
UNION ALL
SELECT 'Inconsistent Kota Case', COUNT(DISTINCT kota)::TEXT FROM survey_raw
UNION ALL
SELECT 'Suspicious Pendapatan', COUNT(*)::TEXT FROM survey_raw
WHERE pendapatan IS NOT NULL AND (pendapatan <= 0 OR pendapatan > 100000000);
-- Part 2: Clean Data
WITH cleaned AS (
SELECT
id,
INITCAP(TRIM(COALESCE(nama, 'Unknown'))) AS nama,
LOWER(TRIM(email)) AS email,
INITCAP(TRIM(
CASE LOWER(TRIM(kota))
WHEN 'jakrta' THEN 'Jakarta'
ELSE kota
END
)) AS kota,
REGEXP_REPLACE(no_hp, '[^0-9]', '', 'g') AS no_hp,
CASE
WHEN pendapatan <= 0 OR pendapatan > 100000000 THEN NULL
ELSE pendapatan
END AS pendapatan,
tanggal_survey,
ROW_NUMBER() OVER (PARTITION BY LOWER(email) ORDER BY tanggal_survey DESC) AS rn
FROM survey_raw
WHERE email IS NOT NULL
AND email LIKE '%@%.%'
)
SELECT id, nama, email, kota, no_hp, pendapatan, tanggal_survey
FROM cleaned
WHERE rn = 1;
FAQ
Data cleaning pakai SQL atau Excel, mana yang lebih cocok?
Tergantung ukuran datanya. Di bawah 10.000 baris, Excel masih nyaman kok. Lebih dari itu, Excel mulai lemot dan rawan salah klik. SQL bisa handle jutaan baris, dan cleaning-nya berbentuk query — bisa kamu jalanin ulang kapan aja tanpa ngulang kerja manual. Kalau datamu udah ada di database, langsung aja pakai SQL.
Boleh nggak langsung hapus row yang ada NULL-nya?
Jangan buru-buru. NULL di satu kolom nggak berarti seluruh row-nya sampah. Contohnya row dengan pendapatan NULL — nama, kota, dan no HP-nya masih kepake buat analisis lain. Hapus row cuma kalau kolom kuncinya kosong, misalnya email yang jadi unique identifier. Sisanya handle pakai COALESCE atau biarin NULL.
Urutan data cleaning yang bener gimana?
Cek data quality dulu — hitung NULL, duplikat, dan format aneh per kolom. Habis itu baru cleaning: standardisasi text, fix typo, konversi tipe data, dan dedup paling akhir. Dedup duluan itu bahaya lho, soalnya "Jakarta" dan "jakrta" bakal dianggap dua nilai beda. Terakhir, simpan hasilnya ke tabel clean yang terpisah.
Query di artikel ini jalan di MySQL nggak?
Sebagian besar jalan. COALESCE, NULLIF, TRIM, dan CASE WHEN ada di semua database. Yang beda: operator regex ~ itu punya PostgreSQL, di MySQL ganti pakai REGEXP. INITCAP juga nggak ada di MySQL — kamu perlu kombinasi UPPER dan SUBSTRING sendiri. STRING_AGG di MySQL namanya GROUP_CONCAT.
Kesimpulan
Data cleaning emang ga se-glamor bikin dashboard atau model ML, tapi ini skill yang bikin kamu valuable sebagai Data Analyst. Inget poin-poin ini:
- 80% waktu Data Analyst dihabiskan untuk data cleaning
- Selalu cek data quality dulu sebelum cleaning
- NULL handling tergantung konteks bisnis
- Pake mapping table untuk fix typos di production
- Validasi format dengan REGEXP
- Cap outliers, jangan langsung buang
- Bikin clean dimension table terpisah
- Jangan overwrite data original
Data bersih = analisis akurat = keputusan bisnis yang lebih baik. Worth the effort!
Happy cleaning!
Selanjutnya
Kalau kamu udah paham data cleaning, next step-nya:
- EDA dengan SQL - eksplorasi data yang udah clean
- Cohort Analysis - analisis retention
- Window Functions - analisis lebih advanced
Mau praktek langsung? Mulai latihan SQL gratis
Latihan interaktif, langsung di browser.
Artikel Terkait
Membuat Dashboard Metrics dengan SQL: KPI yang Wajib Ditrack
Panduan lengkap query SQL untuk membangun dashboard metrics, dari revenue metrics sampai retention analysis.
Funnel Analysis dengan SQL: Mengukur Conversion Rate Step-by-Step
Pelajari cara bikin funnel analysis untuk mengukur conversion rate di setiap step. Lengkap dengan template query dan cara present ke stakeholder.
SQL untuk A/B Testing Analysis: Panduan Praktis
Pelajari cara menganalisis A/B test dengan SQL. Dari hitung conversion rate sampai statistical significance dengan contoh e-commerce Indonesia.