September 27, 2026
Script Dasar SQL (MySQL).
Contekan praktis DDL, DML, dan DQL di MySQL: CREATE, INSERT, SELECT dengan WHERE/ORDER/LIMIT, JOIN, GROUP BY/HAVING, subquery, sampai UPDATE/DELETE yang aman.
SQL (Structured Query Language) adalah bahasa standar untuk berbicara dengan database relasional. Catatan ini merangkum perintah yang paling sering dipakai sehari-hari di MySQL 8, lengkap dengan contoh yang bisa langsung dijalankan di mysql CLI, phpMyAdmin
, atau TablePlus.
Perintah SQL biasanya dikelompokkan menjadi tiga:
| Kelompok | Kepanjangan | Fungsi | Contoh perintah |
|---|---|---|---|
| DDL | Data Definition Language | Mendefinisikan struktur | CREATE, ALTER, DROP, TRUNCATE |
| DML | Data Manipulation Language | Mengubah isi data | INSERT, UPDATE, DELETE |
| DQL | Data Query Language | Membaca data | SELECT |
1. DDL: Membuat Database dan Tabel
CREATE DATABASE IF NOT EXISTS toko
CHARACTER SET utf8mb4
COLLATE utf8mb4_0900_ai_ci;
USE toko;
CREATE TABLE customers (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(150) NOT NULL UNIQUE,
city VARCHAR(50),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;
CREATE TABLE orders (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
customer_id BIGINT UNSIGNED NOT NULL,
total DECIMAL(12,2) NOT NULL DEFAULT 0,
status ENUM('pending','paid','cancelled') NOT NULL DEFAULT 'pending',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_orders_customer
FOREIGN KEY (customer_id) REFERENCES customers(id)
) ENGINE=InnoDB;
Beberapa kebiasaan baik:
- Gunakan
utf8mb4, bukanutf8(di MySQL,utf8adalah aliasutf8mb3yang tidak bisa menyimpan emoji). - Untuk uang, pakai
DECIMAL, janganFLOAT/DOUBLE(ada error pembulatan). - Beri nama constraint secara eksplisit (
fk_orders_customer) supaya mudah di-drop nanti.
Mengubah struktur tabel:
ALTER TABLE customers ADD COLUMN phone VARCHAR(20) AFTER email;
ALTER TABLE customers MODIFY COLUMN city VARCHAR(100);
ALTER TABLE customers RENAME COLUMN phone TO phone_number;
ALTER TABLE customers DROP COLUMN phone_number;
-- lihat struktur
DESCRIBE customers;
SHOW CREATE TABLE orders\G
DROP TABLE menghapus tabel beserta isinya; TRUNCATE TABLE mengosongkan isi tapi mempertahankan struktur (dan me-reset AUTO_INCREMENT).
Peringatan: Perintah DDL di MySQL menyebabkan implicit commit. Artinya
DROP/ALTER/TRUNCATEtidak bisa di-rollback walaupun dijalankan di dalam transaksi .
2. DML: INSERT
-- satu baris
INSERT INTO customers (name, email, city)
VALUES ('Budi', '[email protected]', 'Bandung');
-- banyak baris sekaligus (jauh lebih cepat daripada INSERT satu per satu)
INSERT INTO customers (name, email, city) VALUES
('Siti', '[email protected]', 'Jakarta'),
('Andi', '[email protected]', 'Surabaya'),
('Rina', '[email protected]', 'Jakarta');
INSERT INTO orders (customer_id, total, status) VALUES
(1, 150000, 'paid'),
(1, 75000, 'pending'),
(2, 320000, 'paid'),
(3, 50000, 'cancelled');
Upsert — insert, atau update jika kunci unik sudah ada:
INSERT INTO customers (name, email, city)
VALUES ('Budi Santoso', '[email protected]', 'Bandung') AS new
ON DUPLICATE KEY UPDATE name = new.name, city = new.city;
Sintaks alias AS new tersedia sejak MySQL 8.0.19; versi lama memakai VALUES(name) yang kini deprecated.
3. DQL: SELECT Dasar
SELECT id, name, city
FROM customers
WHERE city = 'Jakarta'
ORDER BY name ASC
LIMIT 10;
Urutan penulisan klausa wajib seperti ini:
SELECT → FROM → JOIN → WHERE → GROUP BY → HAVING → ORDER BY → LIMIT
Operator di WHERE
| Operator | Contoh | Keterangan |
|---|---|---|
=, <>, <, >= | total >= 100000 | Perbandingan |
AND, OR, NOT | city = 'Jakarta' AND id > 2 | Logika (pakai kurung jika campur) |
IN | status IN ('paid','pending') | Salah satu dari daftar |
BETWEEN | total BETWEEN 50000 AND 200000 | Inklusif di kedua ujung |
LIKE | email LIKE '%@mail.com' | % = banyak karakter, _ = satu karakter |
IS NULL | city IS NULL | Jangan city = NULL (selalu tidak cocok) |
Pagination
-- halaman 3, 20 baris per halaman
SELECT * FROM orders ORDER BY id LIMIT 20 OFFSET 40;
OFFSET besar itu lambat karena MySQL tetap membaca lalu membuang baris sebelumnya. Untuk tabel besar, gunakan keyset pagination:
SELECT * FROM orders WHERE id > 12345 ORDER BY id LIMIT 20;
4. JOIN
SELECT o.id, c.name, o.total, o.status
FROM orders o
INNER JOIN customers c ON c.id = o.customer_id;
| Jenis | Hasil |
|---|---|
INNER JOIN | Hanya baris yang punya pasangan di kedua tabel |
LEFT JOIN | Semua baris tabel kiri; kolom kanan NULL jika tak ada pasangan |
RIGHT JOIN | Kebalikan LEFT JOIN (jarang dipakai, cukup tukar urutan tabel) |
CROSS JOIN | Semua kombinasi (kartesian) |
MySQL tidak punya FULL OUTER JOIN; tiru dengan LEFT JOIN ... UNION ... RIGHT JOIN.
Mencari customer yang belum pernah order:
SELECT c.id, c.name
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
WHERE o.id IS NULL;
Tips: Filter untuk tabel kanan pada
LEFT JOINtaruh diON, bukan diWHERE. MenulisWHERE o.status = 'paid'diam-diam mengubahLEFT JOINmenjadiINNER JOINkarena barisNULLikut tersaring.
5. Agregasi: GROUP BY dan HAVING
SELECT c.city,
COUNT(o.id) AS jumlah_order,
SUM(o.total) AS omzet,
AVG(o.total) AS rata_rata
FROM customers c
JOIN orders o ON o.customer_id = c.id
WHERE o.status = 'paid'
GROUP BY c.city
HAVING SUM(o.total) > 100000
ORDER BY omzet DESC;
WHEREmenyaring baris sebelum dikelompokkan.HAVINGmenyaring grup setelah agregasi.
MySQL 8 mengaktifkan ONLY_FULL_GROUP_BY secara default: setiap kolom di SELECT harus ada di GROUP BY atau dibungkus fungsi agregat. Kalau muncul error “isn’t in GROUP BY”, perbaiki query-nya, jangan mematikan mode tersebut.
Fungsi agregat umum: COUNT(*), COUNT(DISTINCT col), SUM, AVG, MIN, MAX, GROUP_CONCAT.
6. Subquery dan CTE
Subquery di WHERE:
-- customer yang pernah order di atas 200rb
SELECT name FROM customers
WHERE id IN (SELECT customer_id FROM orders WHERE total > 200000);
-- versi EXISTS (sering lebih jelas maksudnya)
SELECT name FROM customers c
WHERE EXISTS (
SELECT 1 FROM orders o WHERE o.customer_id = c.id AND o.total > 200000
);
Subquery di FROM (derived table):
SELECT AVG(total_per_customer) AS rata2_belanja
FROM (
SELECT customer_id, SUM(total) AS total_per_customer
FROM orders GROUP BY customer_id
) t;
Sejak MySQL 8.0 tersedia CTE (WITH) yang membuat query panjang lebih mudah dibaca:
WITH belanja AS (
SELECT customer_id, SUM(total) AS total
FROM orders WHERE status = 'paid'
GROUP BY customer_id
)
SELECT c.name, b.total
FROM belanja b JOIN customers c ON c.id = b.customer_id
ORDER BY b.total DESC;
Hati-hati dengan NOT IN jika subquery bisa mengembalikan NULL: hasilnya jadi kosong. Pakai NOT EXISTS untuk kasus itu.
7. UPDATE dan DELETE dengan Aman
UPDATE orders SET status = 'paid' WHERE id = 2;
DELETE FROM orders WHERE status = 'cancelled' AND created_at < '2026-01-01';
Kesalahan klasik: lupa WHERE, lalu seluruh tabel ter-update. Kebiasaan yang menyelamatkan:
1. Jalankan SELECT dengan WHERE yang sama terlebih dulu.
SELECT COUNT(*) FROM orders WHERE status = 'cancelled' AND created_at < '2026-01-01';
2. Bungkus dalam transaksi agar bisa dibatalkan:
START TRANSACTION;
UPDATE orders SET total = total * 0.9 WHERE customer_id = 1;
SELECT * FROM orders WHERE customer_id = 1; -- cek hasil
ROLLBACK; -- atau COMMIT jika sudah benar
3. Aktifkan safe updates di sesi interaktif:
SET SESSION sql_safe_updates = 1;
Atau jalankan client dengan mysql --safe-updates (alias --i-am-a-dummy). Dengan mode ini, UPDATE/DELETE tanpa WHERE yang memakai kolom ber-key atau tanpa LIMIT akan ditolak.
4. Batasi jumlah baris untuk hapus data besar secara bertahap:
DELETE FROM logs WHERE created_at < '2026-01-01' ORDER BY id LIMIT 5000;
-- ulangi sampai affected rows = 0
UPDATE dengan JOIN:
UPDATE orders o
JOIN customers c ON c.id = o.customer_id
SET o.status = 'cancelled'
WHERE c.email = '[email protected]' AND o.status = 'pending';
8. Kesalahan Umum
| Masalah | Penyebab | Solusi |
|---|---|---|
WHERE kolom = NULL tidak mengembalikan apa pun | NULL bukan nilai, perbandingan menghasilkan UNKNOWN | Pakai IS NULL / IS NOT NULL |
SELECT * di kode aplikasi | Mengambil kolom yang tidak perlu, rawan rusak saat skema berubah | Sebutkan kolom secara eksplisit |
| Query dibangun dengan string concat | Rawan SQL injection | Gunakan prepared statement / parameter binding |
Hasil COUNT(kolom) lebih kecil dari COUNT(*) | COUNT(kolom) tidak menghitung NULL | Pilih sesuai maksud |
| Error 1452 saat insert | Nilai foreign key tidak ada di tabel induk | Insert data induk dulu |
| Urutan hasil “acak” | Tanpa ORDER BY, urutan tidak dijamin | Selalu tulis ORDER BY jika urutan penting |
Referensi

Hey! I’m Fanny, the software engineer tending to this digital garden. You can read more about me, or subscribe by email.