0 to Hero SQL
EN
Materi 1 dari 10 · 16m

Model relasional, dan query pertamamu

Apa yang sebenarnya dijanjikan sebuah tabel, skema yang dipakai semua materi di kursus ini, dan SELECT / WHERE / ORDER BY / LIMIT di kedua mesin — termasuk empat tempat PostgreSQL dan MySQL sudah berbeda sejak awal.

Tabel adalah himpunan baris, masing-masing punya kolom bernama yang sama. Kedengaran sepele, dan punya dua akibat yang langsung bikin orang tersandung.

Himpunan tidak punya urutan. Tabel tidak punya baris pertama. Kalau kamu tidak menulis ORDER BY, mesinnya boleh mengembalikan baris dalam urutan apa pun, dan “urutan apa pun” itu boleh berubah antara dua kali menjalankan query yang sama — setelah sebuah update, setelah ada index baru, setelah naik versi. Semua bug pagination di dunia dimulai dari sini.

Satu kolom punya satu tipe dan satu makna. status menyimpan status. Dia tidak menyimpan status dan kadang-kadang alasan statusnya. Begitu satu kolom berarti dua hal, setiap query tentang dia akan menumbuhkan OR.

SQL itu deklaratif: kamu menjelaskan hasil yang kamu mau, bukan cara mendapatkannya. Komponen bernama planner (PostgreSQL) atau optimizer (MySQL) yang memutuskan cara mengeksekusinya. Jarak itulah alasan materi 8 ada — dua query yang artinya sama bisa berbeda seribu kali lipat.

Skema untuk kursus ini

Semua materi memakai empat tabel ini. Buat sekarang, sekali saja.

PostgreSQL:

CREATE TABLE customer (
id bigserial PRIMARY KEY,
email text NOT NULL UNIQUE,
full_name text NOT NULL,
country text NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE product (
id bigserial PRIMARY KEY,
sku text NOT NULL UNIQUE,
name text NOT NULL,
cents integer NOT NULL CHECK (cents >= 0)
);
CREATE TABLE "order" (
id bigserial PRIMARY KEY,
customer_id bigint NOT NULL REFERENCES customer(id),
status text NOT NULL,
placed_at timestamptz NOT NULL,
shipping_cents integer NOT NULL DEFAULT 0,
shipped_at timestamptz, -- NULL berarti belum dikirim
coupon_code text -- NULL berarti tanpa kupon
);
CREATE TABLE order_item (
order_id bigint NOT NULL REFERENCES "order"(id),
product_id bigint NOT NULL REFERENCES product(id),
quantity integer NOT NULL CHECK (quantity > 0),
unit_cents integer NOT NULL,
PRIMARY KEY (order_id, product_id)
);

MySQL:

CREATE TABLE customer (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
full_name VARCHAR(255) NOT NULL,
country VARCHAR(2) NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE product (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
sku VARCHAR(64) NOT NULL UNIQUE,
name VARCHAR(255) NOT NULL,
cents INT NOT NULL CHECK (cents >= 0)
);
CREATE TABLE `order` (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
customer_id BIGINT NOT NULL,
status VARCHAR(32) NOT NULL,
placed_at DATETIME NOT NULL,
shipping_cents INT NOT NULL DEFAULT 0,
shipped_at DATETIME NULL,
coupon_code VARCHAR(32) NULL,
FOREIGN KEY (customer_id) REFERENCES customer(id)
);
CREATE TABLE order_item (
order_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
quantity INT NOT NULL CHECK (quantity > 0),
unit_cents INT NOT NULL,
PRIMARY KEY (order_id, product_id),
FOREIGN KEY (order_id) REFERENCES `order`(id),
FOREIGN KEY (product_id) REFERENCES product(id)
);

Dua hal sudah pantas diperhatikan. order itu kata kunci, jadi harus dikutip — "order" di PostgreSQL, `order` di MySQL. Itu sendiri alasan bagus untuk tidak menamai tabel order; kursus ini tetap memakai nama itu justru supaya aturan kutipnya terus terlihat.

Dan tipe waktunya tidak setara. timestamptz PostgreSQL menyimpan satu titik waktu absolut dan mengonversinya saat masuk dan keluar. DATETIME MySQL menyimpan angka jam dinding tanpa zona sama sekali. Perbedaan itu menghasilkan bug nyata, dan materi 7 membahasnya dengan benar.

SELECT: proyeksi dan seleksi

Dua operasi terpisah yang sering dicampur pemula.

SELECT full_name, country -- proyeksi: kolom yang mana
FROM customer
WHERE country = 'ID'; -- seleksi: baris yang mana

SELECT * boleh di prompt dan berbahaya di kode: dia membuat hasilnya bergantung pada daftar kolom tabel saat itu, jadi menambah satu kolom mengubah apa yang diterima aplikasimu.

Ekspresi boleh dipakai, dan di sinilah perpecahan dialek pertama muncul.

-- PostgreSQL
SELECT full_name, cents / 100.0 AS price, 'SKU-' || sku AS label FROM product;
-- MySQL
SELECT full_name, cents / 100.0 AS price, CONCAT('SKU-', sku) AS label FROM product;

Di MySQL, || secara bawaan berarti OR, bukan penggabungan. 'SKU-' || sku tidak error — dia menilai kedua sisi sebagai boolean lalu mengembalikan 0 atau 1. Query yang mengembalikan hal salah tanpa bersuara lebih buruk daripada yang gagal, jadi pakai CONCAT.

WHERE

Operator perbandingannya seperti dugaanmu: =, <> (juga !=), <, >, <=, >=, ditambah BETWEEN, IN, LIKE.

SELECT * FROM "order"
WHERE status IN ('paid', 'shipped')
AND placed_at >= '2026-01-01'
AND coupon_code IS NULL;

Perhatikan IS NULL, bukan = NULL. Itu bukan soal selera — = NULL tidak pernah benar, dan dia jadi isi seluruh materi berikutnya.

BETWEEN inklusif di kedua ujung, yang justru membuatnya alat salah untuk tanggal: placed_at BETWEEN '2026-01-01' AND '2026-01-31' melewatkan semua yang terjadi pada tanggal 31 setelah tengah malam. Pakai rentang setengah terbuka:

WHERE placed_at >= '2026-01-01' AND placed_at < '2026-02-01'

Rentang setengah terbuka juga bersusun rapi — akhir satu bulan adalah awal bulan berikutnya, tanpa celah dan tanpa tumpang tindih yang harus dipikirkan.

Pencocokan pola, dan huruf besar-kecil

LIKE memakai % untuk rangkaian karakter apa pun dan _ untuk tepat satu.

Kebutuhan PostgreSQL MySQL
Peka huruf besar-kecil LIKE LIKE dengan collation _bin / _cs
Tidak peka huruf besar-kecil ILIKE LIKE (collation bawaannya tidak peka)
Regular expression ~ / ~* REGEXP / RLIKE

Bawaannya berkebalikan, dan itu jebakannya. Di PostgreSQL LIKE 'a%' tidak akan cocok dengan Alice. Di MySQL, dengan collation umum utf8mb4_0900_ai_ci, dia cocok — begitu juga =, artinya WHERE email = 'ALICE@EXAMPLE.COM' menemukan barisnya. Kode yang bergantung pada salah satu perilaku itu jebol saat dipindah.

ORDER BY

SELECT id, status, shipped_at
FROM "order"
ORDER BY shipped_at DESC, id DESC;

Kunci kedua itu bukan hiasan. Mengurutkan berdasar kolom yang punya nilai kembar membuat urutan di dalam kembaran itu tidak ditentukan, jadi daftar berhalaman bisa menampilkan baris yang sama di halaman 1 dan halaman 2. Tambahkan pemecah seri yang unik — biasanya primary key — di setiap ORDER BY yang kamu halamankan.

NULL diurutkan berbeda, dan di sini kedua mesin berbeda lagi:

PostgreSQL MySQL
Bawaan ASC NULL terakhir NULL pertama
Bawaan DESC NULL pertama NULL terakhir
Mengaturnya NULLS FIRST / NULLS LAST tidak ada klausanya; urutkan col IS NULL dulu
-- PostgreSQL
ORDER BY shipped_at DESC NULLS LAST;
-- MySQL: maksud yang sama
ORDER BY (shipped_at IS NULL), shipped_at DESC;

LIMIT dan OFFSET

SELECT id, placed_at FROM "order" ORDER BY placed_at DESC, id DESC LIMIT 20 OFFSET 40;

Kedua mesin menerima bentuk ini. OFFSET aman untuk halaman 3 dan ide buruk untuk halaman 3000, karena mesinnya harus menghasilkan lalu membuang setiap baris yang dilewati — OFFSET 60000 mengerjakan 60.020 baris untuk mengembalikan 20. Materi 10 menggantinya dengan keyset pagination.

DISTINCT

DISTINCT membuang duplikat pada baris hasil secara utuh, bukan per kolom:

SELECT DISTINCT country FROM customer; -- satu baris per negara
SELECT DISTINCT country, status FROM "order" o
JOIN customer c ON c.id = o.customer_id; -- satu baris per pasangan

Kalau kamu menambahkan DISTINCT untuk membetulkan baris ganda setelah join, DISTINCT itu biasanya gejala: join-nya menggandakan baris dan sebenarnya kamu ingin agregat atau semi-join. Materi 4 membahas itu.

PostgreSQL juga punya DISTINCT ON (expr), yang menyimpan baris pertama per grup sesuai urutan ORDER BY — sangat berguna dan tidak portabel:

-- Order terakhir per customer, hanya PostgreSQL
SELECT DISTINCT ON (customer_id) customer_id, id, placed_at
FROM "order"
ORDER BY customer_id, placed_at DESC;

Padanannya di MySQL adalah window function, yaitu materi 6.

Kutip pengenal dan pelipatan huruf

PostgreSQL MySQL
Karakter kutip "nama" `nama` (atau "nama" dengan ANSI_QUOTES)
Pengenal tanpa kutip dilipat ke huruf kecil disimpan apa adanya
"Order" vs order objek berbeda objek sama di sebagian besar build
Huruf nama tabel peka begitu dikutip tergantung filesystem

Aturan praktisnya: namai semuanya lower_snake_case dan jangan pernah mengutip apa pun. Dengan begitu tidak ada dialek, sistem operasi, atau migrasi masa depan yang bisa menafsirkan ulang namamu. Situs ini belajar hal yang sama satu lapis di atasnya — basis datanya bernama site-db, dan tanda hubung itu membuat setiap pernyataan yang menyebutnya harus dikutip selamanya.

Yang perlu dibawa pulang

  • Tanpa ORDER BY berarti tanpa urutan, dan tanpa jaminan dia tetap sama.
  • IS NULL, jangan pernah = NULL.
  • Rentang tanggal setengah terbuka, bukan BETWEEN.
  • CONCAT di MySQL; || di sana berarti OR.
  • Kepekaan huruf pada LIKE dan = berkebalikan antara kedua mesin.
  • lower_snake_case, tanpa kutip, di mana-mana.