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:

KelompokKepanjanganFungsiContoh perintah
DDLData Definition LanguageMendefinisikan strukturCREATE, ALTER, DROP, TRUNCATE
DMLData Manipulation LanguageMengubah isi dataINSERT, UPDATE, DELETE
DQLData Query LanguageMembaca dataSELECT

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, bukan utf8 (di MySQL, utf8 adalah alias utf8mb3 yang tidak bisa menyimpan emoji).
  • Untuk uang, pakai DECIMAL, jangan FLOAT/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/TRUNCATE tidak 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

OperatorContohKeterangan
=, <>, <, >=total >= 100000Perbandingan
AND, OR, NOTcity = 'Jakarta' AND id > 2Logika (pakai kurung jika campur)
INstatus IN ('paid','pending')Salah satu dari daftar
BETWEENtotal BETWEEN 50000 AND 200000Inklusif di kedua ujung
LIKEemail LIKE '%@mail.com'% = banyak karakter, _ = satu karakter
IS NULLcity IS NULLJangan 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;
JenisHasil
INNER JOINHanya baris yang punya pasangan di kedua tabel
LEFT JOINSemua baris tabel kiri; kolom kanan NULL jika tak ada pasangan
RIGHT JOINKebalikan LEFT JOIN (jarang dipakai, cukup tukar urutan tabel)
CROSS JOINSemua 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 JOIN taruh di ON, bukan di WHERE. Menulis WHERE o.status = 'paid' diam-diam mengubah LEFT JOIN menjadi INNER JOIN karena baris NULL ikut 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;
  • WHERE menyaring baris sebelum dikelompokkan.
  • HAVING menyaring 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

MasalahPenyebabSolusi
WHERE kolom = NULL tidak mengembalikan apa punNULL bukan nilai, perbandingan menghasilkan UNKNOWNPakai IS NULL / IS NOT NULL
SELECT * di kode aplikasiMengambil kolom yang tidak perlu, rawan rusak saat skema berubahSebutkan kolom secara eksplisit
Query dibangun dengan string concatRawan SQL injectionGunakan prepared statement / parameter binding
Hasil COUNT(kolom) lebih kecil dari COUNT(*)COUNT(kolom) tidak menghitung NULLPilih sesuai maksud
Error 1452 saat insertNilai foreign key tidak ada di tabel indukInsert data induk dulu
Urutan hasil “acak”Tanpa ORDER BY, urutan tidak dijaminSelalu 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.

Comments