TL;DR
Kesalahan paling fatal: lupa WHERE di UPDATE/DELETE. Yang sering: salah urutan clause, NULL handling, Cartesian product. Solusi: selalu test di dev dulu, pake transaction, dan validasi input.
Semua orang pasti pernah bikin kesalahan waktu belajar SQL. Aku sendiri pernah hampir hapus semua data di production karena lupa WHERE clause. Untung ada backup.
Daripada kamu ngalamin hal yang sama, mending belajar dari kesalahan umum yang sering dilakukan pemula.
Ini kesalahan paling fatal dan paling sering terjadi. Hasilnya? Semua data ke-update atau ke-delete.
-- Niat mau update satu customer
UPDATE customers SET status = 'inactive';
-- Yang terjadi: SEMUA customer jadi inactive
-- Niat mau hapus order yang cancelled
DELETE FROM orders;
-- Yang terjadi: SEMUA orders terhapus
Biasanya karena:
- Copy-paste query tapi lupa tambahin WHERE
- Terburu-buru submit query
- Testing di production environment
-- Tulis ini dulu
DELETE FROM orders WHERE status = 'cancelled';
-- Baru jalanin kalau udah yakin
-- Lihat data yang bakal ke-affect
SELECT * FROM orders WHERE status = 'cancelled';
-- Kalau udah bener, baru DELETE
DELETE FROM orders WHERE status = 'cancelled';
BEGIN;
DELETE FROM orders WHERE status = 'cancelled';
-- Cek hasilnya
SELECT * FROM orders WHERE status = 'cancelled';
-- Kalau bener, COMMIT. Kalau salah, ROLLBACK
COMMIT; -- atau ROLLBACK;
SET SQL_SAFE_UPDATES = 1;
-- Sekarang UPDATE/DELETE tanpa WHERE bakal error
SQL punya urutan clause yang strict. Salah urutan = error.
-- SALAH: HAVING sebelum GROUP BY
SELECT kategori, COUNT(*)
FROM products
HAVING COUNT(*) > 5
GROUP BY kategori;
-- Error: syntax error
SELECT kolom
FROM tabel
WHERE kondisi_baris
GROUP BY kolom_grouping
HAVING kondisi_agregasi
ORDER BY kolom_urutan
LIMIT jumlah;
Inget urutan ini: S-F-W-G-H-O-L (Select From Where Group Having Order Limit)
Bikin akronim atau cerita:
- "Siti Fatimah Waktu Guru Hadir Order Late"
- Atau: "Selasa Februari Wawan Gi Home Only Learning"
Alias bikin query lebih readable, tapi sering salah pakenya.
-- SALAH: Pake alias sebelum didefinisiin
SELECT nama, total_order
FROM customers
WHERE total_order > 10; -- Error! total_order belum ada
-- Karena alias didefinisiin di SELECT, tapi WHERE dieksekusi duluan
-- Pake subquery atau CTE
SELECT *
FROM (
SELECT nama, COUNT(*) as total_order
FROM customers c
JOIN orders o ON c.id = o.customer_id
GROUP BY nama
) AS subq
WHERE total_order > 10;
-- Atau pake HAVING (kalau filter hasil agregasi)
SELECT nama, COUNT(*) as total_order
FROM customers c
JOIN orders o ON c.id = o.customer_id
GROUP BY nama
HAVING COUNT(*) > 10;
-- Kasih alias yang meaningful
SELECT
c.nama AS nama_customer, -- Jelas
COUNT(o.id) AS jumlah_order, -- Deskriptif
SUM(o.total) AS total_belanja
FROM customers AS c -- AS opsional tapi lebih jelas
JOIN orders AS o ON c.id = o.customer_id
GROUP BY c.nama;
Ini terjadi kalau kamu JOIN tabel tanpa kondisi ON yang proper.
-- SALAH: Lupa ON clause
SELECT *
FROM customers, orders;
-- Hasil: Setiap customer dipasangkan dengan SETIAP order
-- 100 customers x 1000 orders = 100,000 rows!
-- SALAH: Kondisi JOIN yang salah
SELECT *
FROM customers c
JOIN orders o ON c.nama = o.nama; -- Harusnya c.id = o.customer_id
-- BENAR
SELECT *
FROM customers c
INNER JOIN orders o ON c.id = o.customer_id;
-- Cek dulu estimasi hasilnya
SELECT COUNT(*)
FROM customers c
JOIN orders o ON c.id = o.customer_id;
SELECT *
FROM customers c
JOIN orders o ON c.id = o.customer_id
LIMIT 10;
NULL di SQL itu tricky. NULL bukan 0, bukan empty string, NULL itu nilai yang belum diketahui.
-- SALAH: NULL ga bisa di-compare pake =
SELECT * FROM users WHERE phone = NULL;
-- Hasil: 0 rows (padahal ada yang NULL)
-- SALAH: NULL ga sama dengan empty string
SELECT * FROM users WHERE phone = '';
-- Ini cuma dapet yang empty string, bukan NULL
-- Pake IS NULL atau IS NOT NULL
SELECT * FROM users WHERE phone IS NULL;
SELECT * FROM users WHERE phone IS NOT NULL;
-- Pake COALESCE untuk default value
SELECT
nama,
COALESCE(phone, 'Tidak ada') AS phone
FROM users;
-- Pake NULLIF untuk convert value ke NULL
SELECT NULLIF(phone, '') AS phone FROM users;
-- Empty string jadi NULL
-- COUNT(*) hitung semua rows termasuk NULL
-- COUNT(kolom) skip NULL values
SELECT
COUNT(*) AS total_rows,
COUNT(phone) AS yang_punya_phone,
COUNT(*) - COUNT(phone) AS yang_null
FROM users;
-- Anything + NULL = NULL
SELECT 100 + NULL; -- Hasil: NULL
-- Gunakan COALESCE
SELECT 100 + COALESCE(discount, 0) FROM products;
Index bikin query cepat, tapi banyak pemula ga paham pentingnya.
-- Query lambat di tabel besar
SELECT * FROM orders WHERE customer_id = 12345;
-- Full table scan: 1 juta rows di-cek satu-satu
-- Buat index di kolom yang sering di-filter
CREATE INDEX idx_orders_customer_id ON orders(customer_id);
-- Sekarang query jadi jauh lebih cepat
SELECT * FROM orders WHERE customer_id = 12345;
-- Index scan: langsung ke data yang relevan
SELECT * gampang ditulis tapi banyak masalahnya.
-- Ambil semua kolom
SELECT * FROM users;
-- Sebutin kolom yang dibutuhkan aja
SELECT id, nama, email, created_at
FROM users;
Ini sering bikin bingung karena behavior-nya beda di tiap database.
-- Di PostgreSQL, ini case-sensitive
SELECT * FROM users WHERE nama = 'Budi';
-- Ga akan dapet 'BUDI' atau 'budi'
-- Di MySQL dengan collation default, ini case-insensitive
SELECT * FROM users WHERE nama = 'Budi';
-- Bisa dapet 'BUDI', 'budi', 'BuDi'
-- Pake LOWER() atau UPPER() untuk konsistensi
SELECT * FROM users WHERE LOWER(nama) = LOWER('Budi');
-- Atau pake ILIKE di PostgreSQL
SELECT * FROM users WHERE nama ILIKE 'budi';
-- Standardize waktu INSERT
INSERT INTO users (email) VALUES (LOWER('Budi@Email.com'));
-- Atau buat constraint
ALTER TABLE users
ADD CONSTRAINT email_lowercase
CHECK (email = LOWER(email));
Masalah date format sering bikin query ga jalan atau hasil salah.
-- Format Indonesia: DD-MM-YYYY
-- Format SQL standard: YYYY-MM-DD
-- SALAH: Ini bisa jadi ambigu
SELECT * FROM orders WHERE tanggal = '01-02-2024';
-- Ini 1 Feb atau 2 Jan?
-- Selalu pake ISO format: YYYY-MM-DD
SELECT * FROM orders WHERE tanggal = '2024-02-01';
-- Atau pake explicit conversion
SELECT * FROM orders
WHERE tanggal = TO_DATE('01-02-2024', 'DD-MM-YYYY');
-- Simpan dalam UTC
INSERT INTO events (waktu) VALUES (NOW() AT TIME ZONE 'UTC');
-- Convert ke WIB waktu display
SELECT
waktu AT TIME ZONE 'Asia/Jakarta' AS waktu_wib
FROM events;
-- SALAH: Ini bisa miss data
SELECT * FROM orders WHERE tanggal = '2024-02-01';
-- Kalau tanggal ada time component, ga akan match
-- BENAR: Pake range
SELECT * FROM orders
WHERE tanggal >= '2024-02-01'
AND tanggal < '2024-02-02';
-- Atau pake DATE()
SELECT * FROM orders
WHERE DATE(tanggal) = '2024-02-01';
Ini kesalahan keamanan yang serius. SQL injection bisa bikin hacker akses atau hapus data.
username = request.get('username')
query = f"SELECT * FROM users WHERE username = '{username}'"
-- Input malicious:
-- username: '; DROP TABLE users; --
-- Query jadi:
SELECT * FROM users WHERE username = ''; DROP TABLE users; --'
-- Tabel users terhapus!
cursor.execute(
"SELECT * FROM users WHERE username = %s",
(username,)
)
session.query(User).filter(User.username == username)
import re
if not re.match(r'^[a-zA-Z0-9_]+$', username):
raise ValueError("Invalid username format")
ORM kayak SQLAlchemy, Django ORM, atau Prisma otomatis handle parameterization.
Database user aplikasi jangan dikasih permission DROP TABLE atau DELETE tanpa WHERE.
ERROR: column "nama_lengkap" does not exist
LINE 1: SELECT nama_lengkap FROM users
^
HINT: Perhaps you meant to reference the column "nama".
Error message biasanya kasih tau persis di mana masalahnya.
-- Query kompleks yang error
SELECT a.*, b.*, c.*
FROM a
JOIN b ON ...
JOIN c ON ...
WHERE ...
GROUP BY ...
HAVING ...
-- Pecah jadi bagian-bagian
SELECT * FROM a LIMIT 5; -- Test tabel a
SELECT * FROM b LIMIT 5; -- Test tabel b
SELECT * FROM a JOIN b ON ... LIMIT 5; -- Test join
-- dst
EXPLAIN SELECT * FROM orders WHERE customer_id = 123;
-- Lihat apakah pake index atau full scan
-- Lihat struktur tabel
\d orders -- PostgreSQL
DESCRIBE orders; -- MySQL
Tulis WHERE clause-nya dulu sebelum jalanin query, jangan langsung eksekusi. Coba tes pake SELECT dengan kondisi yang sama buat lihat data mana aja yang bakal kena. Bungkus query pake TRANSACTION biar kamu bisa ROLLBACK kalau hasilnya salah. Di MySQL, kamu bisa nyalain SQL_SAFE_UPDATES biar query tanpa WHERE otomatis kena error.
SQL punya urutan clause yang baku, disingkat S-F-W-G-H-O-L. Artinya SELECT, FROM, WHERE, GROUP BY, HAVING, ORDER BY, LIMIT. GROUP BY ngelompokin data dulu, baru HAVING bisa filter hasil agregasinya kayak COUNT atau SUM. Kalau kamu tulis HAVING sebelum GROUP BY, database bakal langsung kasih syntax error soalnya urutan eksekusinya emang udah ditentuin gitu.
NULL di SQL beda dari angka 0 atau empty string. NULL artinya nilai yang belum diketahui, jadi dia nggak bisa dibandingin pake tanda sama dengan. Query WHERE phone = NULL bakal selalu balikin 0 baris walaupun ada data yang NULL. Buat nyari data kosong, pake IS NULL atau IS NOT NULL. Kalau mau kasih nilai pengganti, pake COALESCE.
Boleh, tapi cuma buat eksplorasi data pas development atau debugging cepat. Di production mending sebutin kolom yang emang dibutuhin aja. SELECT * narik semua kolom termasuk yang gak kepake, bikin transfer data lebih berat, dan gampang bikin aplikasi error kalau ada kolom baru ditambahin ke tabel nanti.
Pake parameterized query, jangan nyambungin input user langsung ke string query kayak f-string di Python. Tambahin validasi input biar karakter aneh kayak tanda kutip atau titik koma ke-filter duluan. Kalau kamu pake ORM kayak SQLAlchemy atau Django ORM, parameterization-nya udah otomatis jalan. Batasin juga permission user database biar gak bisa DROP TABLE sembarangan.
10 kesalahan yang udah kita bahas:
Yang paling penting: selalu test di development environment dulu, pake transaction untuk operasi berbahaya, dan backup data secara rutin.
Mau praktek langsung? Mulai latihan SQL gratis
Latihan interaktif, langsung di browser.
Cara pakai pandas cut dan qcut buat binning data numerik jadi kelompok. Bedanya, kapan pakai yang mana, plus contoh segmentasi pelanggan UMKM.
Cara pakai pandas rolling buat hitung moving average dan haluskan data time series yang naik-turun. Lengkap dengan contoh penjualan harian UMKM.
Data transaksi harian sering terlalu ramai buat dilihat. Pandas resample ngeringkasnya jadi total per minggu atau per bulan cuma dengan satu baris, asal indeksnya tanggal.