SQL JOIN türlerini ezberlemek kolaydır, doğru olanı seçmek ise değildir. İki tabloyu birleştirdiğinizde asıl soru "hangi satırlar eşleşir" değil, "eşleşmeyen satırlara ne olur" sorusudur. INNER JOIN onları sessizce atar, LEFT JOIN sol taraftakileri NULL doldurarak korur ve bu tek fark, bir raporda müşteri sayısının neden 1.200 yerine 940 çıktığını açıklar.
Bu rehberde beş birleştirme türünü tek bir örnek veri kümesi üzerinde çalıştırıp çıktılarını yan yana koyacağım. Ardından JOIN öğrenirken en çok tökezlenen konuya, yani ON ile WHERE arasındaki farka geçeceğiz; bu farkı bilmeyen bir kişi LEFT JOIN yazdığını sanırken aslında INNER JOIN çalıştırır. Son bölümde de birleştirmelerin nasıl hızlandığını, indekslerin planı nasıl değiştirdiğini ve sık düşülen tuzakları göreceksiniz.
JOIN Nedir, İki Tablo Nasıl Birleşir#
Birleştirme, iki tablonun satırlarını bir koşula göre yan yana getirme işlemidir. Kavramsal olarak veritabanı önce iki tablonun tüm satır çiftlerini düşünür, sonra ON koşulunu sağlayanları tutar. Uygulamada motorlar bunu indeksler ve karma tablolar sayesinde çok daha verimli yapar, ama sonucun anlamı bu kavramsal modelden gelir.
Örneklerde şu iki tabloyu kullanacağım:
CREATE TABLE musteriler (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
ad_soyad VARCHAR(120) NOT NULL,
sehir VARCHAR(60) NOT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB;
CREATE TABLE siparisler (
siparis_no INT UNSIGNED NOT NULL AUTO_INCREMENT,
musteri_id INT UNSIGNED NULL, -- NULL: sahipsiz misafir siparişi
tutar DECIMAL(12,2) NOT NULL,
olusturuldu DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (siparis_no),
KEY idx_musteri (musteri_id)
) ENGINE=InnoDB;
Veri şöyle olsun: dört müşteri var (1 Ayşe, 2 Bora, 3 Cem, 4 Deniz), üç sipariş var (101 → müşteri 1, 102 → müşteri 1, 103 → müşteri 3) ve bir de müşterisi olmayan misafir siparişi var (104 → NULL). Bu kümede Bora ve Deniz'in hiç siparişi yok; 104 numaralı siparişin de sahibi yok. Tam da bu iki "eşleşmeyen" durum, JOIN türleri arasındaki farkı görünür kılar.
INNER JOIN: Yalnızca Eşleşenler#
INNER JOIN, her iki tarafta da karşılığı olan satır çiftlerini döner. Eşleşmeyen hiçbir satır sonuca girmez.
SELECT m.ad_soyad, s.siparis_no, s.tutar
FROM musteriler m
INNER JOIN siparisler s ON s.musteri_id = m.id
ORDER BY m.ad_soyad;
Bu sorgu üç satır döner: Ayşe'nin iki siparişi ve Cem'in bir siparişi. Bora ve Deniz sonuçta yoktur çünkü siparişleri yoktur; 104 numaralı misafir siparişi de yoktur çünkü musteri_id değeri NULLdur ve NULL = herhangi bir değer karşılaştırması hiçbir zaman doğru dönmez.
INNER JOIN yazarken INNER kelimesi isteğe bağlıdır; sadece JOIN yazmak aynı anlama gelir ve çoğu kod tabanında böyle görürsünüz. Birleştirme sütunlarının adı iki tabloda da aynıysa USING kısaltmasını kullanabilirsiniz:
-- İki tabloda da sütun adı "musteri_id" olsaydı:
SELECT * FROM siparisler JOIN musteri_notlari USING (musteri_id);
NATURAL JOIN ise aynı adlı tüm sütunları otomatik eşleştirir ve tam da bu yüzden kullanılmamalıdır: şemaya sonradan eklenen bir olusturuldu sütunu, sorgunun anlamını haberiniz olmadan değiştirir.
LEFT JOIN ve RIGHT JOIN: Eşleşmeyenleri Korumak#
LEFT JOIN, sol tablodaki tüm satırları döner; sağ tarafta eşleşme yoksa o sütunlar NULL gelir.
SELECT m.ad_soyad, s.siparis_no, s.tutar
FROM musteriler m
LEFT JOIN siparisler s ON s.musteri_id = m.id
ORDER BY m.ad_soyad;
Bu sorgu beş satır döner: Ayşe için iki satır, Bora için bir satır (siparis_no ve tutar NULL), Cem için bir satır, Deniz için bir satır (yine NULL). Misafir siparişi hâlâ yoktur, çünkü o sağ tarafta ve LEFT JOIN sağ tarafın eşleşmeyenlerini korumaz.
LEFT JOIN'in en güçlü kullanımı, "karşılığı olmayanları bul" sorusudur; buna anti-join denir:
-- Hiç sipariş vermemiş müşteriler
SELECT m.id, m.ad_soyad
FROM musteriler m
LEFT JOIN siparisler s ON s.musteri_id = m.id
WHERE s.siparis_no IS NULL;
Buradaki IS NULL koşulu, eşleşme bulunamadığı için NULL doldurulmuş satırları yakalar. Aynı işi NOT EXISTS ile de yapabilirsiniz; iki yaklaşımın performans karşılaştırmasını subquery mi JOIN mi yazısında ele alıyorum.
RIGHT JOIN ise aynı şeyin aynasıdır: sağ tablodaki tüm satırları korur. Pratikte nadir kullanılır çünkü tablo sırasını değiştirerek her RIGHT JOIN bir LEFT JOIN'e çevrilebilir ve okuyucular soldan sağa okumaya alışkındır. Ekip içinde tutarlılık adına LEFT JOIN kullanmayı standart hâline getirmek iyi bir alışkanlıktır.
| Tür | Sol tablonun eşleşmeyenleri | Sağ tablonun eşleşmeyenleri | Örnek satır sayısı |
|---|---|---|---|
| INNER JOIN | Düşer | Düşer | 3 |
| LEFT JOIN | Korunur | Düşer | 5 |
| RIGHT JOIN | Düşer | Korunur | 4 |
| FULL OUTER JOIN | Korunur | Korunur | 6 |
| CROSS JOIN | Tüm kombinasyonlar | Tüm kombinasyonlar | 16 |
FULL OUTER JOIN ve MySQL'deki Karşılığı#
FULL OUTER JOIN, iki tarafın da eşleşmeyen satırlarını korur. PostgreSQL bunu doğrudan destekler:
-- PostgreSQL
SELECT m.ad_soyad, s.siparis_no
FROM musteriler m
FULL OUTER JOIN siparisler s ON s.musteri_id = m.id;
MySQL'de FULL OUTER JOIN sözdizimi yoktur. Aynı sonucu iki sorguyu birleştirerek elde edersiniz:
-- MySQL karşılığı
SELECT m.ad_soyad, s.siparis_no
FROM musteriler m
LEFT JOIN siparisler s ON s.musteri_id = m.id
UNION
SELECT m.ad_soyad, s.siparis_no
FROM musteriler m
RIGHT JOIN siparisler s ON s.musteri_id = m.id;
Burada UNION (UNION ALL değil) kullanmak önemlidir; UNION tekrar eden satırları eler, aksi hâlde iki tarafta da eşleşen satırlar iki kez görünür. Bunun bedeli bir sıralama veya karma işlemidir, yani büyük tablolarda maliyetlidir. Pratikte FULL OUTER JOIN ihtiyacı genellikle veri uzlaştırma senaryolarında (iki kaynağı karşılaştırıp her iki taraftaki eksikleri bulmak) doğar ve bu işler zaten toplu çalıştığı için maliyet kabul edilebilir.
CROSS JOIN ve SELF JOIN#
CROSS JOIN koşulsuzdur ve iki tablonun kartezyen çarpımını üretir: 4 müşteri × 4 sipariş = 16 satır. Kulağa yararsız gelse de meşru kullanımı vardır; en yaygını, eksiksiz bir zaman veya kategori ızgarası oluşturmaktır:
-- Her şehir için her ayın satırı olsun; satış yoksa 0 yazsın
SELECT s.sehir, a.ay, COALESCE(SUM(sp.tutar), 0) AS ciro
FROM sehirler s
CROSS JOIN aylar a
LEFT JOIN musteriler m ON m.sehir = s.sehir_adi
LEFT JOIN siparisler sp ON sp.musteri_id = m.id
AND MONTH(sp.olusturuldu) = a.ay
GROUP BY s.sehir, a.ay;
CROSS JOIN'in tehlikesi kazara oluşmasıdır: ON koşulunu yazmayı unutursanız ya da eski virgüllü sözdizimini (FROM a, b) WHERE koşulu olmadan kullanırsanız, iki milyonluk tablo 10 üzeri 12 satırlık bir sonuç üretmeye çalışır ve sunucuyu kilitler.
SELF JOIN ise bir tablonun kendisiyle birleştirilmesidir; hiyerarşik verilerde kullanılır:
-- Her çalışanı yöneticisiyle birlikte listele
SELECT c.ad_soyad AS calisan, y.ad_soyad AS yonetici
FROM calisanlar c
LEFT JOIN calisanlar y ON y.id = c.yonetici_id;
Burada LEFT JOIN kullanmak zorunludur; aksi hâlde yöneticisi olmayan genel müdür sonuçtan düşer. Aynı tabloya iki farklı takma ad (c ve y) vermek de şarttır, yoksa hangi sütunun hangi kopyaya ait olduğu belirsizleşir.
ON ile WHERE Farkı: En Sık Yapılan Hata#
Bu bölüm rehberin en kritik kısmıdır. LEFT JOIN yaptıktan sonra sağ tablonun bir sütununu WHERE içinde filtrelerseniz, NULL doldurulmuş satırlar bu filtreden geçemez ve sorgunuz sessizce INNER JOIN'e dönüşür.
-- YANLIŞ: LEFT JOIN yazıldı ama INNER JOIN gibi davranıyor
SELECT m.ad_soyad, s.siparis_no
FROM musteriler m
LEFT JOIN siparisler s ON s.musteri_id = m.id
WHERE s.tutar > 100;
-- Siparişi olmayan Bora ve Deniz düşer, çünkü NULL > 100 doğru değildir
-- DOĞRU: filtre birleştirme koşulunun parçası olmalı
SELECT m.ad_soyad, s.siparis_no
FROM musteriler m
LEFT JOIN siparisler s ON s.musteri_id = m.id
AND s.tutar > 100;
-- Bora ve Deniz korunur, sipariş sütunları NULL gelir
Kural şudur: sağ tabloya ait koşullar ON içine, sol tabloya ait koşullar WHERE içine yazılır. INNER JOIN'de bu fark yoktur, ikisi de aynı sonucu verir; ama dış birleştirmelerde (LEFT, RIGHT, FULL) anlam tamamen değişir.
Tek istisna, anti-join kalıbıdır: WHERE s.siparis_no IS NULL yazmak bilinçlidir ve tam olarak eşleşmeyenleri yakalamak içindir. Yani kural "sağ tablonun sütunu asla WHERE'de olmaz" değil, "ne yaptığınızı bilerek yazın"dır.
Bir diğer yaygın tuzak, LEFT JOIN sonrası COUNT kullanmaktır:
-- YANLIŞ: siparişi olmayan müşteri için 1 döner (NULL satırı da sayar)
SELECT m.ad_soyad, COUNT(*) AS siparis_sayisi
FROM musteriler m LEFT JOIN siparisler s ON s.musteri_id = m.id
GROUP BY m.id, m.ad_soyad;
-- DOĞRU: NULL olmayan değerleri say
SELECT m.ad_soyad, COUNT(s.siparis_no) AS siparis_sayisi
FROM musteriler m LEFT JOIN siparisler s ON s.musteri_id = m.id
GROUP BY m.id, m.ad_soyad;
COUNT(*) satırları sayar, COUNT(sütun) ise o sütunun NULL olmayan değerlerini sayar. Aradaki fark, siparişi olmayan müşteriler için 1 ile 0 arasındaki farktır.
Performans: İndeksler ve Birleştirme Planı#
MySQL birleştirmeleri temelde iç içe döngüyle çözer: dış tablodan bir satır alır, iç tabloda karşılığını arar. Bu aramanın indeksli olması hız farkının tamamıdır. Yabancı anahtar sütunu indekssizse motor her satır için iç tabloyu baştan sona tarar; 10.000 satırlık dış tablo ile 100.000 satırlık iç tablo, bir milyar karşılaştırma demektir.
-- Birleştirme sütunlarını indeksleyin
CREATE INDEX idx_siparis_musteri ON siparisler (musteri_id);
-- Planı okuyun
EXPLAIN
SELECT m.ad_soyad, s.tutar
FROM musteriler m
JOIN siparisler s ON s.musteri_id = m.id
WHERE m.sehir = 'İzmir';
EXPLAIN çıktısındaki type sütunu erişim yöntemini gösterir ve şu sırayla iyiden kötüye gider:
| type | Anlamı | Değerlendirme |
|---|---|---|
| const / eq_ref | Benzersiz indeksten tek satır | Mükemmel |
| ref | İndeksten birkaç satır | İyi |
| range | İndeks üzerinde aralık taraması | Kabul edilebilir |
| index | Tüm indeksin taranması | Şüpheli |
| ALL | Tablonun tam taranması | Sorunlu |
Birleştirmelerin birinde ALL görüyorsanız ve o tablo büyükse, eksik indeks arayın. MySQL 8, indeksle çözülemeyen eşitlik birleştirmelerinde karma birleştirme (hash join) kullanabilir; bu, indekssiz durumu iyileştirir ama doğru indeksin yerini tutmaz. Sorgu planlarını okuma ve yavaş sorgu avı için MySQL yavaş sorgu bulma rehberine bakın; yabancı anahtar ve indeks tasarımı içinse primary key ve foreign key tasarımı yazısı tamamlayıcıdır.
Sık Yapılan Hatalar#
SELECT * ile birleştirme yapmak. İki tabloda da id sütunu varsa sonuçta iki id gelir, hangisinin hangisi olduğu belirsizleşir ve gereksiz veri taşınır. İhtiyacınız olan sütunları takma adlarıyla yazın.
Birleştirmeden sonra beklenmedik satır artışını fark etmemek. Bire-çok bir ilişkiyi birleştirdiğinizde sol tablonun satırları çoğalır. Toplam alırken bu çoğalma tutarları şişirir; bu durumda toplamayı alt sorguda yapıp sonucu birleştirmek daha doğrudur.
Farklı tipteki sütunları birleştirmek. VARCHAR bir sütunu INT bir sütunla eşleştirmek örtük dönüşüm yaratır ve indeksin kullanılmasını engeller. Birleştirme sütunlarının tipi ve karakter seti aynı olmalıdır.
Virgüllü eski sözdizimini kullanmak. FROM a, b WHERE a.id = b.a_id çalışır ama dış birleştirme yazamaz ve WHERE koşulunu unuttuğunuzda sessizce kartezyen çarpım üretir. Açık JOIN ... ON sözdizimi hem güvenli hem okunaklıdır.
Dış birleştirmede ON ile WHERE'i karıştırmak. Yukarıda ayrıntısını verdim; bu, rapor rakamlarının neden tutmadığının bir numaralı sebebidir.
Gereksiz tablo birleştirmek. Yalnızca varlık kontrolü için tablo eklemeyin; EXISTS bu iş için daha ucuzdur ve satır çoğalmasına yol açmaz.
Sıkça Sorulan Sorular#
INNER JOIN ile LEFT JOIN arasındaki fark nedir#
INNER JOIN yalnızca iki tarafta da karşılığı olan satırları döner; eşleşmeyenler sonuca hiç girmez. LEFT JOIN ise sol tablodaki tüm satırları korur ve sağ tarafta eşleşme yoksa o sütunları NULL ile doldurur. Bir müşteri listesinde herkesi görmek istiyorsanız LEFT JOIN, yalnızca alışveriş yapmış olanları istiyorsanız INNER JOIN kullanırsınız.
MySQL'de FULL OUTER JOIN neden yok#
MySQL bu sözdizimini desteklemez; bu bir eksiklik olarak kabul edilir ama uzun süredir böyledir. Aynı sonucu bir LEFT JOIN ile bir RIGHT JOIN sorgusunu UNION ile birleştirerek elde edebilirsiniz. UNION ALL değil UNION kullanmaya dikkat edin, aksi hâlde her iki tarafta eşleşen satırlar iki kez görünür.
LEFT JOIN her zaman INNER JOIN'den yavaş mıdır#
Genellikle biraz daha maliyetlidir çünkü motor eşleşmeyen satırlar için de sonuç üretmek zorundadır ve birleştirme sırasını değiştirme özgürlüğü azalır. Ancak fark, doğru indeksler varsa çoğu sorguda küçüktür. Asıl yavaşlık kaynağı birleştirme türü değil, indekssiz birleştirme sütunları ve gereksiz büyük ara sonuçlardır.
JOIN yerine subquery kullanmak daha mı iyi#
Duruma göre değişir. Modern MySQL ve PostgreSQL sürümlerinde birçok alt sorgu otomatik olarak birleştirmeye dönüştürülür ve performans farkı kapanır. Yine de "bu satırın karşılığı var mı" sorularında EXISTS, satır çoğalmasına yol açmadığı için daha güvenli bir tercihtir. Karşılaştırmayı ayrıntılı olarak ilgili rehberde bulabilirsiniz.
Kaç tabloyu birleştirmek normaldir#
Normalize bir şemada bir raporun beş-altı tabloyu birleştirmesi tamamen olağandır ve indeksler doğruysa hızlıdır. Sayı ondan yukarı çıkmaya başladığında sorgu hem okunmaz hâle gelir hem de sorgu planlayıcının doğru sırayı bulma ihtimali düşer. Bu noktada sorguyu ortak tablo ifadeleriyle parçalamak ya da bir özet tablo düşünmek daha sağlıklıdır.
Birleştirme sırası performansı etkiler mi#
Yazdığınız sıra ile motorun çalıştırdığı sıra farklıdır; planlayıcı istatistiklere bakarak kendi sırasını seçer. Yine de dış birleştirmeler bu özgürlüğü kısıtladığı için sıra önem kazanabilir. Planlayıcının belirgin biçimde kötü bir seçim yaptığını görürseniz önce tablo istatistiklerini güncelleyin; MySQL'de ANALYZE TABLE çalıştırmak çoğu zaman yeterlidir.
LEFT JOIN sonrası COUNT neden yanlış çıkıyor#
Çünkü COUNT(*) satır sayar ve eşleşme bulunamadığında oluşan NULL dolu satır da bir satırdır. Siparişi olmayan bir müşteri için bu, sıfır yerine bir sonucunu verir. Doğrusu, sağ tablodan bir sütunu saymaktır: COUNT(s.siparis_no) yalnızca NULL olmayan değerleri sayar ve eşleşme yoksa sıfır döner.
Kapanış#
Birleştirme türlerini seçerken sorulacak tek soru şudur: eşleşmeyen satırlar sonuca girmeli mi. Aklınızda kalması gereken dört alışkanlık: dış birleştirmelerde sağ tabloya ait filtreleri ON içine yazın, LEFT JOIN sonrası saymalarda COUNT(sütun) kullanın, birleştirme sütunlarını mutlaka indeksleyin ve EXPLAIN çıktısında ALL gördüğünüz her satırı ciddiye alın. Virgüllü eski sözdizimini de tamamen bırakın.
Karmaşık raporları çalıştıran veritabanları genellikle bellek ve disk hızından fayda görür. Kendi MySQL veya PostgreSQL ayarlarınızı özgürce yapabileceğiniz VDS ve bulut sunucu paketlerimiz bu tür iş yükleri için uygundur; küçük ölçekli projeler içinse hazır yapılandırmalı web hosting paketleri yeterli olur. Sorgu optimizasyonu ve indeks planlamasında destek isterseniz sunucu yönetimi hizmetimiz bu konuları da kapsıyor.