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 manaFROM customerWHERE country = 'ID'; -- seleksi: baris yang manaSELECT * 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.
-- PostgreSQLSELECT full_name, cents / 100.0 AS price, 'SKU-' || sku AS label FROM product;
-- MySQLSELECT 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_atFROM "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 |
-- PostgreSQLORDER BY shipped_at DESC NULLS LAST;
-- MySQL: maksud yang samaORDER 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 negaraSELECT DISTINCT country, status FROM "order" o JOIN customer c ON c.id = o.customer_id; -- satu baris per pasanganKalau 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 PostgreSQLSELECT DISTINCT ON (customer_id) customer_id, id, placed_atFROM "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 BYberarti tanpa urutan, dan tanpa jaminan dia tetap sama. IS NULL, jangan pernah= NULL.- Rentang tanggal setengah terbuka, bukan
BETWEEN. CONCATdi MySQL;||di sana berarti OR.- Kepekaan huruf pada
LIKEdan=berkebalikan antara kedua mesin. lower_snake_case, tanpa kutip, di mana-mana.
Diskusi
Memuat komentar…