Join yang tidak jebol
ON versus WHERE dan kondisi yang diam-diam mengubah LEFT JOIN kembali jadi INNER, FULL OUTER JOIN di mesin yang tidak punya, anti-join, dan cross join yang tak sengaja.
Join mencocokkan baris dari dua tabel dan mengembalikan kombinasinya. Hanya itu yang dia lakukan — dan hampir semua bug join berasal dari lupa separuh kedua kalimat itu: kombinasi. Kalau satu baris di kiri cocok dengan tiga baris di kanan, kamu dapat tiga baris keluaran.
INNER JOIN
SELECT c.full_name, o.id, o.statusFROM customer cJOIN "order" o ON o.customer_id = c.id;JOIN berarti INNER JOIN. Baris bertahan hanya ketika kondisinya benar di kedua
sisi — customer tanpa order hilang, dan order dengan customer_id menggantung juga
hilang. Di skema kita foreign key membuat kasus kedua mustahil, dan itulah gunanya
dia ada.
LEFT JOIN, dan jebakannya
LEFT JOIN menyimpan setiap baris dari tabel kiri, mengisi sisi kanan dengan NULL
ketika tidak ada yang cocok.
SELECT c.full_name, o.idFROM customer cLEFT JOIN "order" o ON o.customer_id = c.id;Sekarang inilah kesalahannya, dan ini bug join paling umum yang ada:
-- SALAH: ini INNER JOIN yang memakai kostum LEFT JOINSELECT c.full_name, o.idFROM customer cLEFT JOIN "order" o ON o.customer_id = c.idWHERE o.status = 'paid';LEFT JOIN-nya patuh menghasilkan baris NULL untuk customer yang tidak punya order
berbayar — lalu WHERE o.status = 'paid' bernilai NULL untuk baris-baris itu dan
membuangnya. Kamu meminta semua customer dan hanya dapat yang punya order berbayar.
Perbaikannya adalah memutuskan kondisi itu untuk apa:
-- Filter pada tabel kanan tempatnya di ON: dia menentukan apa yang dianggap cocokSELECT c.full_name, o.idFROM customer cLEFT JOIN "order" o ON o.customer_id = c.id AND o.status = 'paid';
-- Filter pada tabel kiri tempatnya di WHERE: dia menentukan baris mana yang ditimbangSELECT c.full_name, o.idFROM customer cLEFT JOIN "order" o ON o.customer_id = c.idWHERE c.country = 'ID';Untuk INNER JOIN perbedaan ini tidak mengubah hasil, dan justru itu sebabnya orang
terbiasa menaruh semuanya di WHERE lalu tergigit begitu pertama kali mengganti
jenis join-nya.
RIGHT JOIN
RIGHT JOIN adalah LEFT JOIN dengan tabel yang ditukar. Dia ada, kedua mesin
mendukungnya, dan jarang dipakai karena satu alasan: pembacanya harus memegang dua
tabel dalam urutan terbalik dari cara penulisannya. Tulis LEFT JOIN dan tukar
urutan tabelnya.
FULL OUTER JOIN
Menyimpan baris tak-cocok dari kedua sisi.
| PostgreSQL | MySQL | |
|---|---|---|
FULL OUTER JOIN |
didukung | tidak didukung |
Di MySQL kamu menyusunnya dari dua outer join dan sebuah UNION:
-- PostgreSQLSELECT c.id, o.idFROM customer cFULL OUTER JOIN "order" o ON o.customer_id = c.id;
-- MySQL: hasil yang samaSELECT c.id AS customer_id, o.id AS order_idFROM customer c LEFT JOIN `order` o ON o.customer_id = c.idUNIONSELECT c.id, o.idFROM customer c RIGHT JOIN `order` o ON o.customer_id = c.id;Pakai UNION, bukan UNION ALL — baris yang cocok muncul di kedua paruh dan
UNION membuang duplikatnya. Pembuangan duplikat itu juga sebabnya ini lebih lambat
daripada yang aslinya.
CROSS JOIN, yang disengaja dan yang tidak
Cross join mengembalikan setiap kombinasi: 1.000 customer dikali 500 produk adalah 500.000 baris.
-- Disengaja, dan sah: kisi untuk di-left-join dengan data sungguhanSELECT c.id, p.id FROM customer c CROSS JOIN product p;Versi tak sengajanya datang dari sintaks koma lama dengan kondisi yang terlupa:
-- Dua tabel, tanpa kondisi join. Ini cross join.SELECT * FROM customer c, "order" o;Tidak ada yang memperingatkanmu. Di tabel kecil dia tampak seperti query lambat; di
tabel besar dia mengisi disk. Ini argumen terkuat untuk selalu menulis JOIN ... ON
secara eksplisit: ON yang hilang adalah error sintaks, sementara WHERE yang
hilang cuma hari Selasa.
Semi-join dan anti-join
Seringkali kamu tidak butuh kolom dari tabel sebelah — hanya ingin tahu apakah ada
yang cocok. Men-join untuk alasan itu menggandakan baris lalu butuh DISTINCT untuk
membereskan kerusakannya.
-- Semi-join: customer yang punya setidaknya satu order. Tanpa duplikat, tanpa DISTINCT.SELECT * FROM customer cWHERE EXISTS (SELECT 1 FROM "order" o WHERE o.customer_id = c.id);
-- Anti-join: customer yang tidak punyaSELECT * FROM customer cWHERE NOT EXISTS (SELECT 1 FROM "order" o WHERE o.customer_id = c.id);
-- Anti-join, ejaan lain: LEFT JOIN lalu simpan yang tidak cocokSELECT c.* FROM customer cLEFT JOIN "order" o ON o.customer_id = c.idWHERE o.id IS NULL;Ketiganya sah. EXISTS menyatakan maksudnya, dan dia tidak pernah punya masalah
NULL NOT IN dari materi 2. Kedua planner biasanya mengeksekusi bentuk EXISTS dan
LEFT JOIN secara identik.
Self join
Tabel yang di-join ke dirinya sendiri butuh alias, karena tanpa itu tidak ada rujukan kolom yang tidak ambigu.
-- Pasangan order dari customer yang sama dalam selang satu jamSELECT a.id, b.id, a.customer_idFROM "order" aJOIN "order" b ON b.customer_id = a.customer_id AND b.id > a.id -- setiap pasangan sekali, bukan dua kali AND b.placed_at < a.placed_at + interval '1 hour';b.id > a.id mengerjakan hal nyata: tanpa dia setiap pasangan muncul dua kali, satu
per arah, ditambah setiap baris yang di-join ke dirinya sendiri. Di MySQL
intervalnya a.placed_at + INTERVAL 1 HOUR.
USING, dan kenapa NATURAL JOIN itu jebakan
-- ON, ditulis lengkapJOIN order_item i ON i.order_id = o.id
-- USING: hanya ketika nama kolomnya sama di kedua tabelSELECT * FROM "order" o JOIN order_item i USING (order_id); -- kalau o punya order_idUSING juga menyatukan kolomnya sehingga muncul sekali di SELECT *, yang
sesekali memang yang kamu mau.
NATURAL JOIN men-join berdasarkan setiap kolom yang namanya sama, tanpa kamu
menyebutkannya. Kedua mesin mendukungnya, dan sebaiknya dihindari: tambahkan kolom
created_at di kedua tabel dan join itu diam-diam mulai mencocokkannya juga,
mengubah hasil query yang tidak disentuh siapa pun.
Urutan join, dan apa yang dilakukan planner terhadapnya
Urutan penulisan join tidak menentukan urutan eksekusinya. Kedua mesin menyusun ulang join berdasarkan statistik ukuran tabel dan sebaran nilai.
EXPLAINSELECT c.full_name, i.quantityFROM customer cJOIN "order" o ON o.customer_id = c.idJOIN order_item i ON i.order_id = o.idWHERE c.country = 'ID';Keduanya bisa mulai dari customer (kalau country = 'ID' selektif) atau dari
order_item (kalau tidak). Materi 8 membahas cara membaca keputusan itu.
Sesekali planner-nya salah dan kamu perlu memaksa:
| PostgreSQL | MySQL | |
|---|---|---|
| Memaksa urutan join | join_collapse_limit = 1 (setelan sesi) |
STRAIGHT_JOIN |
| Hint optimizer | tidak ada hint query; ekstensi pg_hint_plan |
/*+ JOIN_ORDER(...) */ dan lainnya |
Keduanya jalan terakhir. Sebuah hint adalah statistik yang dibekukan pada saat kamu menulisnya, dan dia berhenti benar begitu datanya bertambah.
Banyak join, dan fan-out lagi
Setiap join satu-ke-banyak menggandakan baris, dan beberapa join semacam itu saling menggandakan:
Bayangkan ada tabel shipment di samping empat tabel kita — satu order bisa punya
beberapa, sama seperti dia punya beberapa item:
-- Kalau satu order punya 3 item dan 2 pengiriman, ini mengembalikan 6 baris per orderSELECT o.id, i.product_id, s.tracking_codeFROM "order" oJOIN order_item i ON i.order_id = o.idJOIN shipment s ON s.order_id = o.id;Tidak ada yang salah secara sintaks di sini, dan agregat apa pun di atasnya adalah omong kosong. Kalau kamu butuh dua relasi satu-ke-banyak yang independen dalam satu hasil, agregasi masing-masing dulu secara terpisah — seperti di materi 3 — atau pakai dua query.
Yang perlu dibawa pulang
- Filter pada tabel yang di-outer-join tempatnya di
ON. DiWHEREdia membatalkan outer join-nya. - MySQL tidak punya
FULL OUTER JOIN; susun denganLEFT+RIGHT+UNION. JOIN ... ONeksplisit, jangan join koma —ONyang hilang gagal dengan berisik.- Pakai
EXISTS/NOT EXISTSkalau kamu hanya butuh keberadaan. - Hindari
NATURAL JOIN; kolom baru diam-diam mengubah join-nya. - Dua join satu-ke-banyak dalam satu query saling menggandakan. Agregasi sebelum join.
Diskusi
Memuat komentar…