TL;DR
Urutan eksekusi logis query SQL adalah FROM, JOIN, WHERE, GROUP BY, HAVING, SELECT, DISTINCT, ORDER BY, lalu LIMIT. SELECT diproses hampir paling akhir, jadi alias yang kamu bikin di SELECT nggak bisa dipakai di WHERE tapi bisa dipakai di ORDER BY. Paham urutan ini langsung jelasin kenapa agregat harus difilter pakai HAVING dan kenapa LEFT JOIN bisa berubah jadi INNER JOIN gara-gara satu kondisi di WHERE.
Query SQL ditulis mulai dari SELECT, tapi dijalanin mulai dari FROM.
Satu kalimat itu jelasin sebagian besar error SQL yang bikin orang bingung di tahun pertama.
Kenapa alias error di WHERE tapi aman di ORDER BY. Kenapa SUM harus difilter pakai HAVING. Kenapa LEFT JOIN kamu diam-diam berubah jadi INNER JOIN.
Semuanya satu penyebab: urutan pemrosesan.
Urutan logisnya FROM, lalu ON dan JOIN, WHERE, GROUP BY, HAVING, SELECT, DISTINCT, ORDER BY, dan terakhir LIMIT. Tiap tahap ngasilin tabel sementara yang jadi masukan tahap berikutnya. SELECT diproses di urutan keenam, jauh setelah WHERE, dan itu sumber sebagian besar kebingungan.
| Urutan jalan | Klausa | Yang dikerjain |
|---|---|---|
| 1 | FROM | Ambil tabel sumbernya |
| 2 | ON dan JOIN | Gabungin tabel sesuai kondisi |
| 3 | WHERE | Buang baris yang nggak lolos syarat |
| 4 | GROUP BY | Kelompokkan baris yang tersisa |
| 5 | HAVING | Buang kelompok yang nggak lolos syarat |
| 6 | SELECT | Pilih kolom, hitung ekspresi, bikin alias |
| 7 | DISTINCT | Buang baris kembar |
| 8 | ORDER BY | Urutin hasilnya |
| 9 | LIMIT atau OFFSET | Potong jumlah baris |
Satu catatan penting: ini urutan logis, bukan urutan fisik. Mesin database bebas nyusun ulang langkahnya demi kecepatan, asal hasil akhirnya sama persis dengan urutan logis di atas. Penjelasan resminya ada di dokumentasi PostgreSQL soal pemrosesan query.
Karena WHERE jalan di langkah ketiga sedangkan alias baru dibikin di langkah keenam. Waktu database mengevaluasi WHERE, nama alias itu belum ada sama sekali. Ini bukan kelemahan SQL, tapi konsekuensi langsung dari urutan pemrosesannya.
-- Error: kolom subtotal nggak dikenal
SELECT harga * qty AS subtotal
FROM transaksi
WHERE subtotal > 100000;
-- Benar: ulang ekspresinya
SELECT harga * qty AS subtotal
FROM transaksi
WHERE harga * qty > 100000;
Kalau ekspresinya panjang dan kamu males ngulang, bungkus jadi subquery atau CTE lalu filter di lapisan luar.
WITH hitung AS (
SELECT id, harga * qty AS subtotal
FROM transaksi
)
SELECT *
FROM hitung
WHERE subtotal > 100000;
Anehnya, alias yang sama aman dipakai di ORDER BY. Wajar, soalnya ORDER BY jalan di langkah kedelapan, setelah SELECT selesai bikin aliasnya.
Perbandingan performa antara CTE dan subquery dibahas terpisah di CTE vs subquery.
Pakai WHERE buat nyaring baris sebelum dikelompokkan, dan HAVING buat nyaring hasil setelah dikelompokkan. WHERE nggak bisa nyentuh fungsi agregat kayak SUM atau COUNT, soalnya angka itu baru dihitung di langkah GROUP BY. Salah taruh bikin query-nya error, atau lebih bahaya lagi, jalan tapi hasilnya beda dari yang kamu maksud.
SELECT
pelanggan_id,
SUM(total) AS total_belanja
FROM transaksi
WHERE tanggal >= DATE '2024-01-01'
GROUP BY pelanggan_id
HAVING SUM(total) > 1000000
ORDER BY total_belanja DESC;
Baca query itu ngikutin urutan jalannya, bukan urutan tulisannya.
transaksi.total_belanja.Urutan itu juga ngasih petunjuk performa. WHERE mbuang baris lebih awal, jadi taruh sebanyak mungkin syarat di WHERE dan sisain HAVING cuma buat syarat yang beneran butuh hasil agregat. Konsep dasarnya ada di penjelasan aggregate function.
Karena kondisi tabel kanan ditaruh di WHERE, bukan di ON. LEFT JOIN ngisi baris tanpa pasangan dengan NULL di langkah kedua, lalu WHERE jalan di langkah ketiga dan mbuang semua baris NULL itu. Hasil akhirnya identik sama INNER JOIN, dan nggak ada pesan error yang ngasih tau.
-- Salah: semua pelanggan tanpa transaksi 2024 ikut kebuang
SELECT p.nama, t.total
FROM pelanggan p
LEFT JOIN transaksi t ON t.pelanggan_id = p.id
WHERE t.tanggal >= DATE '2024-01-01';
-- Benar: pelanggan tanpa transaksi tetap muncul dengan NULL
SELECT p.nama, t.total
FROM pelanggan p
LEFT JOIN transaksi t
ON t.pelanggan_id = p.id
AND t.tanggal >= DATE '2024-01-01';
Bedanya cuma posisi satu baris kondisi. Tapi di dataset toko_berkah, query pertama ngasih 1.187 baris pelanggan dan query kedua ngasih 1.402 baris, karena 215 pelanggan yang terakhir belanja di 2023 tetap kebawa.
Kesalahan ini masuk daftar pola query bermasalah yang dibahas di SQL antipattern.
Window function dihitung di tahap SELECT, tepatnya setelah GROUP BY dan HAVING selesai. Waktu WHERE jalan, nilai ROW_NUMBER atau RANK belum ada. Solusinya bungkus query-nya jadi subquery atau CTE, lalu filter hasilnya di lapisan luar.
SELECT *
FROM (
SELECT
pelanggan_id,
tanggal,
total,
ROW_NUMBER() OVER (
PARTITION BY pelanggan_id
ORDER BY tanggal DESC
) AS urutan
FROM transaksi
) x
WHERE urutan = 1;
Query di atas ngambil transaksi terakhir tiap pelanggan. Di dataset toko_berkah hasilnya 1.187 baris, satu per pelanggan unik.
Bedanya window function sama GROUP BY dibahas lengkap di window function vs GROUP BY.
Dataset toko_berkah punya 4.312 transaksi sepanjang 2024 dari 1.187 pelanggan unik. Misalnya kamu diminta bikin laporan pelanggan bernilai tinggi.
Permintaannya: nama pelanggan asal Bandung, total belanja 2024 di atas Rp 1.000.000, urut dari terbesar, ambil 10 teratas.
SELECT
p.nama,
p.kota,
SUM(t.total) AS total_belanja,
COUNT(*) AS jumlah_transaksi
FROM pelanggan p
JOIN transaksi t ON t.pelanggan_id = p.id
WHERE p.kota = 'Bandung'
AND t.tanggal BETWEEN DATE '2024-01-01' AND DATE '2024-12-31'
GROUP BY p.nama, p.kota
HAVING SUM(t.total) > 1000000
ORDER BY total_belanja DESC
LIMIT 10;
Dua syarat di WHERE mbuang baris lebih dulu, jadi GROUP BY cuma ngolah sisa data yang jauh lebih kecil. Syarat total belanja nggak bisa pindah ke WHERE, soalnya SUM belum ada di tahap itu.
Dari dataset ini, query tersebut ngasih 87 pelanggan yang lolos ambang satu juta sebelum dipotong LIMIT. Artinya 7,3 persen dari total pelanggan nyumbang porsi belanja terbesar.
Satu hal soal LIMIT: dia jalan paling akhir, jadi database tetap ngitung 87 baris itu sebelum motong ke 10. LIMIT ngirit waktu pengiriman hasil, bukan waktu ngitungnya.
Urutan logisnya FROM, lalu ON dan JOIN, WHERE, GROUP BY, HAVING, SELECT, DISTINCT, ORDER BY, dan terakhir LIMIT atau OFFSET. Urutan ini beda dari urutan penulisan yang mulai dari SELECT. Paham bedanya langsung jelasin kenapa alias dari SELECT nggak dikenali di WHERE, dan kenapa filter agregat harus ditaruh di HAVING.
Karena WHERE diproses sebelum SELECT, jadi waktu database mengevaluasi WHERE, alias itu belum ada. Solusinya ulang ekspresinya di WHERE, atau bungkus query-nya jadi subquery lalu filter di lapisan luar. Alias tetap bisa dipakai di ORDER BY, soalnya ORDER BY jalan setelah SELECT. Beberapa database juga ngizinin alias di GROUP BY sebagai kelonggaran, tapi jangan diandalkan.
WHERE nyaring baris sebelum pengelompokan, sedangkan HAVING nyaring hasil setelah GROUP BY jalan. Jadi WHERE nggak bisa pakai fungsi agregat kayak SUM atau COUNT, karena angka itu belum dihitung. Kalau kamu mau ambil transaksi 2024 lalu cuma nampilin pelanggan dengan total di atas satu juta, filter tanggalnya di WHERE dan filter totalnya di HAVING.
Karena kamu naruh kondisi tabel kanan di WHERE, bukan di ON. LEFT JOIN ngisi baris yang nggak ketemu pasangan dengan NULL, lalu WHERE jalan setelahnya dan mbuang semua baris NULL itu. Hasilnya identik sama INNER JOIN. Kalau kamu mau tetap nyimpen baris tanpa pasangan, taruh kondisinya di klausa ON.
Nggak selalu. LIMIT diproses paling akhir, jadi database tetap harus baca, gabung, dan urutin datanya dulu sebelum motong hasilnya. Kalau ada ORDER BY tanpa index yang cocok, seluruh baris tetap diurutin dulu. LIMIT baru beneran ngirit kerja kalau ada index yang urutannya udah sesuai ORDER BY, jadi database bisa berhenti lebih awal.
Dua hal yang bikin urutan ini kepakai tiap hari: baca query dari FROM ke bawah waktu debugging, dan taruh filter sedini mungkin biar tahap berikutnya ngolah data lebih sedikit.
Coba ambil satu query panjang yang pernah bikin kamu bingung, terus tulis ulang urutannya di kertas dari FROM sampai LIMIT. Biasanya sumber masalahnya kelihatan di langkah ketiga atau kelima.
Kalau kamu lagi belajar bedain kapan pakai GROUP BY dan kapan pakai window function, lanjut ke window function vs GROUP BY.
Mau praktek langsung? Mulai latihan SQL gratis
Latihan interaktif, langsung di browser.
Kolom waktu isinya angka gede kayak 1704067200 dan bikin bingung? Itu Unix timestamp. Ini cara ngubahnya jadi tanggal beneran di SQL, plus balik lagi.
Data transaksi tersimpan UTC, tapi laporan harus jam WIB. Kalau salah konversi, angka penjualan tengah malam bisa kecatat di tanggal yang salah. Ini cara handle timezone di SQL dengan benar.
Nambah 30 hari ke tanggal invoice, ngurangin sebulan buat cari periode lalu, ngitung selisih hari antar order. Semua itu aritmetika tanggal, dan SQL punya operator INTERVAL buat ngerjainnya.