Membangun fondasi data: desain skema relasional untuk toko online, menulis query yang benar, memahami indexing untuk performa, dan menggunakan transaction untuk konsistensi data dalam operasi multi-langkah

Setelah di episode 4 kalian mendesain kontrak API, sekarang kita membangun rumah data — database. API yang indah tidak ada artinya jika data di belakangnya berantakan: query lambat, skema tidak konsisten, atau order yang hilang di tengah proses. SQL dan relational database adalah fondasi yang paling banyak dipakai di dunia backend, dan di 2026 masih tetap dominan untuk data yang membutuhkan konsistensi.
Mengapa harus paham SQL dalam-dalam? Karena kalian akan menulis query sepanjang karier — dan sebagian besar masalah performa backend adalah masalah query, bukan masalah bahasa pemrograman. Episode ini membangun empat fondasi: modeling skema, query, indexing, dan transaction. Database lab PostgreSQL dari episode 0 akan kita pakai sekarang.
Skema yang baik lahir dari memahami relasi antar entitas. Untuk toko online kita:
users ───┐
├── orders ──── order_items ──── products
reviews ─┘ (pivot)users dan orders: relasi one-to-many (satu user banyak order).orders dan products: relasi many-to-many lewat tabel pivot order_items.CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
name VARCHAR(100) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE products (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
price NUMERIC(12,2) NOT NULL CHECK (price > 0),
stock INTEGER NOT NULL DEFAULT 0 CHECK (stock >= 0),
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id),
status VARCHAR(20) NOT NULL DEFAULT 'pending',
total NUMERIC(12,2) NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE order_items (
id BIGSERIAL PRIMARY KEY,
order_id BIGINT NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
product_id BIGINT NOT NULL REFERENCES products(id),
qty INTEGER NOT NULL CHECK (qty > 0),
unit_price NUMERIC(12,2) NOT NULL
);Perhatikan keputusan penting di sini:
BIGSERIAL — identitas unik otomatis.REFERENCES) — menjamin referensi valid, menjaga integritas.ON DELETE CASCADE — hapus order otomatis menghapus item-nya.NUMERIC(12,2) untuk uang — jangan pernah pakai FLOAT untuk harga (episode 25).SELECT id, name, price FROM products WHERE price > 100000 ORDER BY price DESC;
SELECT p.id, p.name, SUM(oi.qty) AS total_terjual
FROM products p
JOIN order_items oi ON oi.product_id = p.id
GROUP BY p.id, p.name
HAVING SUM(oi.qty) > 10
ORDER BY total_terjual DESC;JOIN menggabungkan tabel, GROUP BY mengelompokkan, HAVING memfilter hasil agregasi (bukan WHERE, yang memfilter baris sebelum agregasi).
INSERT INTO products (name, price, stock)
VALUES ('Mouse Wireless', 150000, 50)
RETURNING id, name;RETURNING menghemat satu query — sangat berguna saat API perlu mengembalikan resource yang baru dibuat dengan 201 Created.
Tanpa index, PostgreSQL membaca seluruh tabel untuk menemukan baris — disebut sequential scan. Index membuat pencarian seperti mencari kata di kamus alih-alih membaca semua halaman.
CREATE INDEX idx_products_price ON products (price);
CREATE INDEX idx_orders_user ON orders (user_id);
CREATE INDEX idx_orders_status ON orders (status);
CREATE INDEX idx_order_items_product ON order_items (product_id);Prinsip index:
WHERE, JOIN, dan ORDER BY — bukan semua kolom.CREATE INDEX idx ON products (category_id, price); memakai kolom kiri-ke-kanan.EXPLAIN ANALYZE sebelum dan sesudah.Seq Scan on products (cost=0.00..1200.50 rows=10000) -- lambat
Bitmap Index Scan on idx_products_price (cost=0.00..6.20 rows=50) -- cepatNote
EXPLAIN ANALYZE adalah alat nomor satu untuk diagnosa query lambat — ia menjalankan query dan menunjukkan plan eksekusi serta waktu nyata. Biasakan melihatnya setiap kali API melambat. Kita dalami ini di episode 25 saat performance tuning.
Beberapa operasi harus berhasil atau gagal bersama-sama. Contoh: saat order dibuat, kita harus (1) kurangi stok produk, (2) buat order, (3) buat item-nya. Jika langkah 2 gagal setelah langkah 1, stok jadi salah — inilah yang dicegah transaction.
BEGIN;
UPDATE products SET stock = stock - 2 WHERE id = 1 AND stock >= 2;
IF NOT FOUND THEN ROLLBACK; END IF;
INSERT INTO orders (user_id, status, total)
VALUES (5, 'pending', 300000)
RETURNING id \gset
INSERT INTO order_items (order_id, product_id, qty, unit_price)
VALUES (:'id', 1, 2, 150000);
COMMIT;Sifat transaction disingkat ACID:
Race condition yang harus kalian waspadai: dua request checkout stok terakhir secara bersamaan. Solusinya di SQL adalah conditional update WHERE stock >= 2 di atas — update hanya berhasil jika stok cukup. Episode 25 membahas isolation level lebih dalam.
Warning
Pengecekan stok hanya di aplikasi (cek lalu update) tidak aman tanpa transaction atau conditional update — dua request bersamaan bisa sama-sama lolos. Selalu jadikan pengecekan dan pengurangan stok satu operasi atomik di database.
Muat skema ke PostgreSQL lab dan coba:
docker exec -i backend-lab psql -U dev -d shop < schema.sql
docker exec -i backend-lab psql -U dev -d shop \
-c "INSERT INTO products (name, price, stock) VALUES ('Kabel USB', 50000, 100) RETURNING id;"
docker exec -i backend-lab psql -U dev -d shop \
-c "EXPLAIN ANALYZE SELECT * FROM products WHERE price > 100000;"List tanpa batas akan makin lambat seiring data bertambah. Selalu kombinasikan dengan pagination (episode 4).
FLOAT tidak bisa merepresentasikan desimal dengan tepat — 0.1 + 0.2 bisa jadi 0.30000000000000004. Uang harus NUMERIC/DECIMAL.
Loop di aplikasi yang mengeksekusi satu query per baris hasil. Solusi: JOIN atau batch query — topik episode 9 dan 25.
Episode 5 membangun rumah data: skema relasional dengan constraint yang menjaga integritas, query yang memakai JOIN dan agregasi, index untuk performa, dan transaction ACID untuk konsistensi.
Inti yang harus dibawa pulang:
NUMERIC, bukan FLOAT.JOIN + GROUP BY + HAVING adalah trias query analitis.WHERE/JOIN/ORDER BY; uji dengan EXPLAIN ANALYZE.Di episode 6 selanjutnya kita akan membandingkan dunia kedua: NoSQL & pilihan database — document store seperti MongoDB, key-value seperti Redis, dan kapan memilih relational vs NoSQL untuk kasus nyata. Sampai jumpa di episode 6!