Dasar-Dasar Query Database: SELECT, JOIN, dan Masalah N+1 yang Sering Terlewat
Anatomi Query SQL Paling Dasar
Kalau Anda baru mulai belajar database relasional, semuanya berawal dari empat kata kunci ini:
SELECT title, author -- Kolom apa saja yang mau diambilFROM blog.posts -- Dari tabel manaWHERE status = 'published' -- Filter baris mana sajaORDER BY published_at DESC; -- Urutkan hasilnya
Query di atas artinya: "Ambil kolom title dan author, dari tabel blog.posts, tapi hanya baris yang status-nya published, lalu urutkan dari yang paling baru."
JOIN: Menggabungkan Data dari Banyak Tabel
Data di database relasional biasanya dipecah ke banyak tabel supaya tidak ada duplikasi (disebut normalisasi). Misalnya, tabel posts dan tabel categories terpisah, dihubungkan lewat category_id.
SELECT posts.title, categories.name AS category_nameFROM postsJOIN categories ON posts.category_id = categories.id;
JOIN menggabungkan baris dari dua tabel berdasarkan kondisi kecocokan (di sini, posts.category_id harus sama dengan categories.id). Tanpa JOIN, Anda hanya akan punya category_id berupa angka yang tidak berarti apa-apa buat manusia, bukan nama kategorinya.
Masalah N+1 Query: Jebakan Klasik yang Sering Terlewat
Ini salah satu kesalahan performa paling umum yang dilakukan developer pemula (bahkan kadang menengah). Bayangkan Anda ingin menampilkan 20 artikel blog beserta nama penulisnya, dan kode Anda ditulis seperti ini:
const posts = await db.query("SELECT * FROM posts LIMIT 20") // 1 queryfor (const post of posts) {// Query TERPISAH untuk SETIAP post, di dalam loop!const author = await db.query("SELECT * FROM authors WHERE id = ?", [post.authorId])}
Ini disebut masalah N+1: 1 query untuk mengambil daftar post, ditambah N query tambahan (satu per post) untuk mengambil data penulisnya masing-masing. Untuk 20 post, itu artinya 21 query ke database—padahal seharusnya bisa selesai dengan 1 atau 2 query saja.
Solusinya, gunakan JOIN (untuk data yang selalu dibutuhkan bersamaan):
SELECT posts.*, authors.name AS author_nameFROM postsJOIN authors ON posts.author_id = authors.idLIMIT 20;
Atau, kalau relasinya lebih kompleks (misalnya data many-to-many seperti post dengan banyak tag), gunakan batch loading—ambil semua ID yang dibutuhkan sekaligus, lalu satu query tambahan yang mengambil semuanya sekaligus dengan WHERE id IN (...), alih-alih satu query per baris.
const posts = await db.query("SELECT * FROM posts LIMIT 20")const authorIds = posts.map(p => p.authorId)// SATU query tambahan untuk SEMUA author sekaligus, bukan satu per postconst authors = await db.query("SELECT * FROM authors WHERE id IN (?)", [authorIds])
Kenapa Index Membantu (Ringkasan Singkat)
Tanpa index, WHERE status = 'published' berarti database harus memeriksa setiap baris di tabel satu per satu (sequential scan). Index memungkinkan database "melompat" langsung ke baris yang relevan, mirip daftar isi buku. Pembahasan lebih dalam soal jenis-jenis index (B-Tree, GIN, GiST) dan kapan memakainya adalah topik tersendiri yang lebih cocok dibahas terpisah karena cukup dalam.
Kesimpulan
SELECT, WHERE, dan JOIN adalah fondasi yang akan terus Anda pakai di hampir semua pekerjaan backend. Tapi yang membedakan kode "asal jalan" dengan kode yang benar-benar efisien biasanya bukan soal sintaks—melainkan kesadaran soal berapa banyak query yang sebenarnya dijalankan ke database. Masalah N+1 khususnya adalah salah satu bug performa paling umum, paling mudah tidak disadari, dan paling mudah diperbaiki begitu Anda tahu cara mengenalinya.
Artikel Terkait
Diskusi Pembaca (1)
Baru sadar kode saya kena N+1 di banyak tempat setelah baca ini. Langsung mau refactor besok!
