PostgreSQL'de JOIN, iki tablonun satırlarını bir koşula göre eşleştirip tek bir sonuç tablosu üretir. INNER JOIN yalnızca eşleşen satırları, LEFT JOIN soldaki tablonun bütün satırlarını (eşleşme yoksa sağ taraf NULL olarak), RIGHT JOIN sağdakinin bütün satırlarını, FULL JOIN her iki tarafın bütün satırlarını, CROSS JOIN ise bütün olası satır çiftlerini döndürür. LATERAL anahtar sözcüğü ise FROM içindeki bir alt sorgunun kendisinden önce gelen tabloların sütunlarına başvurmasına izin vererek "her satır için ilk N kaydı getir" gibi sorguları mümkün kılar.

Bu yazıda birleştirme türlerini, ON, USING ve NATURAL farkını, dış birleştirmelerde koşulu ON ya da WHERE içine yazmanın sonucu nasıl değiştirdiğini, kendi kendine birleştirmeyi ve LATERAL alt sorguları PostgreSQL 18.1 üzerinde çalıştırılmış örneklerle anlatıyoruz. Birleştirmelerin performansı ve alt sorgu ile JOIN arasındaki seçim SQL sorgu optimizasyonu yazısının konusu; burada davranış ve sözdizimine odaklanıyoruz.

Örnek tablolar

Örnek tablolar

İller ve şubeler üzerinden çalışacağız. Bilerek iki uç durum bıraktık: Bursa'da hiç şube yok, Ortahisar şubesinin plakası (61) ise il tablosunda bulunmuyor. Dış birleştirmelerin farkı tam da bu satırlarda görünür.

sql
CREATE TABLE il (plaka integer PRIMARY KEY, ad text NOT NULL);
INSERT INTO il VALUES (6, 'Ankara'), (16, 'Bursa'), (34, 'İstanbul'), (35, 'İzmir');

CREATE TABLE sube (id integer PRIMARY KEY, ad text NOT NULL, plaka integer, acilis integer);
INSERT INTO sube VALUES
    (1, 'Kızılay',  6, 2015),
    (2, 'Kadıköy',  34, 2012),
    (3, 'Beşiktaş', 34, 2019),
    (4, 'Konak',    35, 2021),
    (5, 'Ortahisar', 61, 2023),
    (6, 'Ümraniye', 34, 2024);

CROSS JOIN: kartezyen çarpım

T1 CROSS JOIN T2, iki tablodaki satırların bütün kombinasyonlarını üretir; N ve M satırlı iki tablodan N x M satır çıkar. 4 il ve 6 şube için:

sql
SELECT count(*) FROM il CROSS JOIN sube;
text
 count
-------
    24
(1 row)

Kılavuza göre FROM T1 CROSS JOIN T2, FROM T1 INNER JOIN T2 ON TRUE ile ve FROM T1, T2 ile eşdeğerdir. Ancak ikiden fazla tablo olduğunda virgül ile JOIN arasında bir öncelik farkı vardır: JOIN virgülden daha sıkı bağlar. Bu yüzden FROM T1, T2 INNER JOIN T3 ON koşul yazdığınızda koşul T1'e başvuramaz. Parantez kullanmadığınız sürece JOIN yan tümceleri soldan sağa iç içe geçer.

INNER JOIN ve ON koşulu

İç birleştirmede T1'in her satırı için, birleştirme koşulunu sağlayan her T2 satırına karşılık sonuçta bir satır oluşur. INNER sözcüğü varsayılan olduğu için yazılması isteğe bağlıdır; düz JOIN aynı anlama gelir:

sql
SELECT s.ad AS sube, i.ad AS il
FROM sube s
JOIN il i ON i.plaka = s.plaka
ORDER BY s.id;
text
   sube   |    il
----------+----------
 Kızılay  | Ankara
 Kadıköy  | İstanbul
 Beşiktaş | İstanbul
 Konak    | İzmir
 Ümraniye | İstanbul
(5 rows)

Ortahisar sonuçta yok, çünkü il tablosunda 61 plakası bulunmuyor; Bursa da yok, çünkü onunla eşleşen şube yok. İç birleştirme, eşleşmeyen satırları her iki taraftan da atar. Kılavuzun eğitim bölümündeki hava durumu örneği de aynı noktayı gösterir: şehir tablosunda karşılığı olmayan Hayward kaydı sonuçta yer almaz, çünkü iç birleştirme eşleşmeyen satırları yok sayar. Raporlarda "bazı kayıtlar neden eksik" sorusunun cevabı çoğu zaman budur.

ON koşulu, WHERE içinde kullanılabilen her tür boolean ifade olabilir; eşitlik zorunlu değildir. Aynı iç birleştirmeyi eski sözdizimiyle FROM sube s, il i WHERE i.plaka = s.plaka biçiminde de yazabilirsiniz. Kılavuz iki yazımın aynı sonucu verdiğini, ama açık JOIN ... ON sözdiziminin, birleştirme koşulunu diğer süzme koşullarından ayırdığı için okuyana daha anlaşılır geldiğini belirtir.

Birleştirmeye katılan iki tabloda aynı adlı sütun varsa sütunu tablo adı ya da takma adla nitelemeniz gerekir. Örneğimizde hem il hem sube tablosunda ad sütunu var:

sql
SELECT ad FROM sube JOIN il USING (plaka);
text
ERROR:  column reference "ad" is ambiguous
LINE 1: SELECT ad FROM sube JOIN il USING (plaka);
               ^

Kılavuz, birleştirme sorgularında bütün sütunları nitelemeyi iyi üslup olarak önerir; böylece tablolardan birine sonradan aynı adlı bir sütun eklendiğinde sorgu bozulmaz. Takma ad verdiğinizde ise tablonun asıl adı o sorgu içinde gizlenir: FROM sube s yazdıktan sonra sube.ad yazamazsınız, s.ad yazmanız gerekir.

USING ve NATURAL: kısayollar ve riskleri

USING ve NATURAL: kısayollar ve riskleri

İki tarafta birleştirme sütunu aynı adı taşıyorsa USING (plaka) yazabilirsiniz; bu, ON sube.plaka = il.plaka koşulunun kısaltmasıdır. USING çıktıda ortak sütunu bir kez gösterir: önce listelenen ortak sütunlar, sonra T1'in kalan sütunları, sonra T2'nin kalan sütunları gelir.

sql
SELECT * FROM sube JOIN il USING (plaka) WHERE sube.id <= 2;
text
 plaka | id |   ad    | acilis |    ad
-------+----+---------+--------+----------
     6 |  1 | Kızılay |   2015 | Ankara
    34 |  2 | Kadıköy |   2012 | İstanbul
(2 rows)

NATURAL, USING'in daha da kısa biçimidir: iki tabloda da bulunan bütün sütun adlarını birleştirme koşuluna ekler. Kılavuz bunun belirgin biçimde daha riskli olduğu konusunda uyarır, çünkü tablolardan birine ortak adlı yeni bir sütun eklendiğinde birleştirme koşulu sessizce değişir. Örneğimiz bu tehlikeyi açıkça gösteriyor: iki tabloda da hem plaka hem ad sütunu olduğu için NATURAL JOIN şube adının il adına eşit olmasını da şart koşar ve hiçbir satır dönmez:

sql
SELECT count(*) FROM sube NATURAL JOIN il;
text
 count
-------
     0
(1 row)

Ortak sütun yoksa NATURAL JOIN kılavuza göre CROSS JOIN gibi davranır; bu da beklenmedik biçimde büyük bir sonuç demektir. Üretim sorgularında ON ya da hangi sütunların kullanıldığını açıkça yazan USING tercih edin.

LEFT, RIGHT ve FULL JOIN

LEFT JOIN önce bir iç birleştirme yapar, sonra sağ tarafta hiçbir eşleşmesi olmayan her sol satır için sağ sütunları NULL olan bir satır ekler. Böylece sonuçta sol tablonun her satırı en az bir kez bulunur:

sql
SELECT i.ad AS il, s.ad AS sube
FROM il i
LEFT JOIN sube s ON s.plaka = i.plaka
ORDER BY i.plaka, s.id;
text
    il    |   sube
----------+----------
 Ankara   | Kızılay
 Bursa    |
 İstanbul | Kadıköy
 İstanbul | Beşiktaş
 İstanbul | Ümraniye
 İzmir    | Konak
(6 rows)

RIGHT JOIN bunun tam tersidir ve sağ tablonun bütün satırlarını korur; a RIGHT JOIN b ile b LEFT JOIN a aynı satırları üretir, bu yüzden pratikte çoğu kişi tabloların sırasını değiştirip LEFT JOIN kullanır. FULL JOIN her iki tarafın eşleşmeyen satırlarını da ekler; Bursa ve Ortahisar birlikte görünür:

sql
SELECT i.ad AS il, s.ad AS sube
FROM il i
FULL JOIN sube s ON s.plaka = i.plaka
ORDER BY i.plaka, s.id;
text
    il    |   sube
----------+-----------
 Ankara   | Kızılay
 Bursa    |
 İstanbul | Kadıköy
 İstanbul | Beşiktaş
 İstanbul | Ümraniye
 İzmir    | Konak
          | Ortahisar
(7 rows)

OUTER sözcüğü bütün dış birleştirmelerde isteğe bağlıdır: LEFT OUTER JOIN ile LEFT JOIN aynıdır.

Dış birleştirmede ON ile WHERE aynı şey değildir

Bu, JOIN konusundaki en önemli inceliktir. Kılavuz bunu şöyle açıklar: ON içine yazılan bir kısıt birleştirmeden önce, WHERE içine yazılan ise birleştirmeden sonra uygulanır. İç birleştirmede fark etmez, ama dış birleştirmede sonucu kökten değiştirir. Aynı koşulu, "2019 ve sonrasında açılmış şubeler", iki yere yazalım:

sql
SELECT i.ad AS il, s.ad AS sube
FROM il i
LEFT JOIN sube s ON s.plaka = i.plaka AND s.acilis >= 2019
ORDER BY i.plaka, s.id;

SELECT i.ad AS il, s.ad AS sube
FROM il i
LEFT JOIN sube s ON s.plaka = i.plaka
WHERE s.acilis >= 2019
ORDER BY i.plaka, s.id;
text
    il    |   sube
----------+----------
 Ankara   |
 Bursa    |
 İstanbul | Beşiktaş
 İstanbul | Ümraniye
 İzmir    | Konak
(5 rows)

    il    |   sube
----------+----------
 İstanbul | Beşiktaş
 İstanbul | Ümraniye
 İzmir    | Konak
(3 rows)

İlk sorguda koşul hangi şubelerin eşleşmeye aday olduğunu belirliyor; Ankara'nın 2015 tarihli şubesi aday olmadığı için Ankara boş şubeyle listede kalıyor. İkinci sorguda ise önce bütün iller şubeleriyle birleştiriliyor, sonra WHERE sağ taraftaki bir sütuna koşul koyuyor. Eşleşmesi olmayan satırlarda s.acilis NULL olduğu için koşul doğru çıkmıyor ve bu satırlar atılıyor; sonuçta LEFT JOIN fiilen bir iç birleştirmeye dönüşüyor. Kılavuzun özeti: dış birleştirmenin ON veya USING yan tümcesi bir WHERE koşuluna eşdeğer değildir, çünkü satır eklemeyi de satır çıkarmayı da etkiler.

Eşleşmeyenleri bulmak: anti-join

Bu davranışın faydalı bir kullanımı, karşılığı olmayan satırları bulmaktır. LEFT JOIN sonrasında sağ tarafın birincil anahtarının NULL olması, "eşleşme yok" demektir. Aynı soru NOT EXISTS ile de sorulabilir; iki sorgu aynı sonucu verir:

sql
SELECT i.ad AS subesiz_il
FROM il i
LEFT JOIN sube s ON s.plaka = i.plaka
WHERE s.id IS NULL;

SELECT i.ad AS subesiz_il
FROM il i
WHERE NOT EXISTS (SELECT 1 FROM sube s WHERE s.plaka = i.plaka);
text
 subesiz_il
------------
 Bursa
(1 row)

 subesiz_il
------------
 Bursa
(1 row)

Burada NOT IN (SELECT plaka FROM sube) kullanmaktan kaçının: alt sorgu bir NULL plaka döndürürse NOT IN hiçbir satır için doğru sonuç vermez. Bu tuzak SELECT ve WHERE yazısında anlatılıyor.

Self join: tabloyu kendisiyle birleştirmek

Bir tabloyu kendisiyle birleştirmek için iki farklı takma ad vermeniz gerekir; kılavuz takma adların bu durumda zorunlu olduğunu belirtir. Klasik örnek, yöneticisini aynı tabloda tutan bir çalışan tablosudur. En üstteki yöneticinin de listede kalması için LEFT JOIN kullanıyoruz:

sql
CREATE TABLE calisan (id integer PRIMARY KEY, ad text, yonetici_id integer REFERENCES calisan);
INSERT INTO calisan VALUES (1, 'Selin', NULL), (2, 'Burak', 1), (3, 'Ece', 1), (4, 'Murat', 2);

SELECT c.ad AS calisan, y.ad AS yonetici
FROM calisan c
LEFT JOIN calisan y ON y.id = c.yonetici_id
ORDER BY c.id;
text
 calisan | yonetici
---------+----------
 Selin   |
 Burak   | Selin
 Ece     | Selin
 Murat   | Burak
(4 rows)

Bu sorgu yalnızca bir seviye yukarıyı gösterir. Bütün hiyerarşiyi, yani Murat'ın yöneticisinin yöneticisini ve daha yukarısını tek sorguda dolaşmak için WITH RECURSIVE yazısındaki yöntemi kullanın.

PostgreSQL 18 sürüm notlarına göre planlayıcı artık bazı gereksiz kendi kendine birleştirmeleri otomatik olarak kaldırabiliyor; bu optimizasyon enable_self_join_elimination sunucu değişkeniyle kapatılabilir. Sonuç değişmez, yalnızca plan sadeleşir; planı okumak için EXPLAIN yazısına bakabilirsiniz.

LATERAL: satır başına alt sorgu

Normalde FROM içindeki her alt sorgu bağımsız hesaplanır ve diğer FROM öğelerine başvuramaz. Başvurmaya çalışırsanız PostgreSQL hatayı açıklar ve çözümü de ipucunda verir:

sql
SELECT i.ad, son.ad
FROM il i, (SELECT s.ad FROM sube s WHERE s.plaka = i.plaka LIMIT 1) AS son;
text
ERROR:  invalid reference to FROM-clause entry for table "i"
LINE 2: ...OM il i, (SELECT s.ad FROM sube s WHERE s.plaka = i.plaka LI...
                                                             ^
DETAIL:  There is an entry for table "i", but it cannot be referenced from this part of the query.
HINT:  To reference that table, you must mark this subquery with LATERAL.

Alt sorgunun önüne LATERAL yazdığınızda, kendisinden önce gelen FROM öğelerinin sütunlarını kullanabilir. Kılavuzun anlattığı çalışma biçimi şudur: başvurulan sütunları sağlayan her satır için LATERAL öğe o satırın değerleriyle yeniden hesaplanır ve çıkan satırlar, hesaplandıkları satırla birleştirilir. Bu sayede "her il için en yeni iki şube" gibi, grup başına ilk N kaydı isteyen sorgular kolayca yazılır:

sql
SELECT i.ad AS il, son.ad AS sube, son.acilis
FROM il i
CROSS JOIN LATERAL (
    SELECT s.ad, s.acilis FROM sube s
    WHERE s.plaka = i.plaka
    ORDER BY s.acilis DESC
    LIMIT 2
) AS son
ORDER BY i.plaka, son.acilis DESC;
text
    il    |   sube   | acilis
----------+----------+--------
 Ankara   | Kızılay  |   2015
 İstanbul | Ümraniye |   2024
 İstanbul | Beşiktaş |   2019
 İzmir    | Konak    |   2021
(4 rows)

İstanbul'un üç şubesinden yalnızca en yeni ikisi geldi. Bursa ise sonuçta yok, çünkü CROSS JOIN LATERAL alt sorgu hiç satır üretmediğinde dış satırı da atar. Kılavuz bu durumda LEFT JOIN LATERAL ... ON true kullanmanın özellikle kullanışlı olduğunu belirtir; böylece alt sorgu boş dönse bile dış satır sonuçta kalır:

sql
SELECT i.ad AS il, son.ad AS sube
FROM il i
LEFT JOIN LATERAL (
    SELECT s.ad FROM sube s
    WHERE s.plaka = i.plaka
    ORDER BY s.acilis DESC
    LIMIT 1
) AS son ON true
ORDER BY i.plaka;
text
    il    |   sube
----------+----------
 Ankara   | Kızılay
 Bursa    |
 İstanbul | Ümraniye
 İzmir    | Konak
(4 rows)

Küme döndüren fonksiyonlar (unnest, generate_series gibi) FROM içinde kullanıldığında LATERAL sözcüğü isteğe bağlıdır; fonksiyonun argümanları önceki öğelerin sütunlarına her durumda başvurabilir. Tek satırlık bir sonuç için DISTINCT ON ya da row_number() pencere fonksiyonu da aynı işi görebilir; LATERAL'in üstünlüğü, alt sorguda LIMIT ile birden fazla satır alabilmeniz ve her satır için farklı bir hesaplama yapabilmenizdir.

Birleştirme türleri özeti

TürSonuçta ne varTipik kullanım
INNER JOINYalnızca iki tarafta da eşleşen satırlarSiparişi müşterisiyle göstermek
LEFT JOINSoldaki bütün satırlar, eşleşme yoksa sağ taraf NULLŞubesi olmayan illeri de listelemek
RIGHT JOINSağdaki bütün satırlarGenellikle LEFT JOIN ile tersten yazılır
FULL JOINİki tarafın bütün satırlarıİki listeyi karşılaştırıp farkları bulmak
CROSS JOINBütün satır kombinasyonları (N x M)Tarih ve ürün listesinden tam ızgara üretmek
JOIN LATERALHer sol satır için ayrı hesaplanan alt sorgu sonucuGrup başına ilk N kayıt

Sık yapılan hatalar

Sık yapılan hatalar: Dış birleştirmede sağ tabloya WHERE koşulu koymak, NATURAL JOIN kullanmak, Birleştirme koşulunu unutmak, Birden fazla eşleşmeyi hesaba katmamak
  • Dış birleştirmede sağ tabloya WHERE koşulu koymak: LEFT JOIN sessizce iç birleştirmeye döner. Koşulu ON içine taşıyın.
  • NATURAL JOIN kullanmak: Ortak adlı bütün sütunlar koşula girer; yeni bir sütun sorgunun anlamını değiştirebilir.
  • Birleştirme koşulunu unutmak: FROM a, b yazıp koşulu atlamak kartezyen çarpım üretir; satır sayısı beklenenden katlanarak büyür.
  • Birden fazla eşleşmeyi hesaba katmamak: Bire çok ilişkide sol satır birden fazla kez tekrarlanır; bunun üzerine sum almak toplamları şişirir. Önce alt sorguda toplayıp sonra birleştirin (bkz. GROUP BY yazısı).
  • Nitelenmemiş sütun adları: Bugün çalışan sorgu, tabloya aynı adlı sütun eklendiğinde ambiguous hatası verir.

Sık Sorulan Sorular

JOIN ile virgülle yazılan FROM listesi aynı mı?

İç birleştirme için aynı sonucu verir: FROM a, b WHERE a.id = b.id ile FROM a JOIN b ON a.id = b.id eşdeğerdir. Dış birleştirmeler ise yalnızca JOIN sözdizimiyle yazılabilir. Ayrıca JOIN virgülden daha sıkı bağladığı için ikisini karıştırdığınızda koşulların hangi tablolara başvurabildiği değişir.

LEFT JOIN mi NOT EXISTS mi kullanmalıyım?

Eşleşmeyen satırları bulmak için ikisi de doğru sonuç verir. NOT EXISTS niyeti daha açık ifade eder; LEFT JOIN ... IS NULL ise sağ tablonun sütunlarına da ihtiyaç duyduğunuz sorgularla birleştirilebilir. Hangisinin daha hızlı olduğu plana bağlıdır, bunu EXPLAIN ile ölçün.

LATERAL ne zaman gerekir?

FROM içindeki bir alt sorgunun, kendisinden önce gelen bir tablonun o anki satırına göre farklı sonuç üretmesi gerektiğinde. En tipik örnek grup başına ORDER BY ... LIMIT n ile ilk N satırı almaktır.

USING ile ON arasında sonuç farkı var mı?

Eşleşen satırlar aynıdır; fark çıktı sütunlarındadır. USING ortak sütunu bir kez gösterir, ON ise iki tablonun bütün sütunlarını, aynı adlı olanlar dahil, ayrı ayrı döndürür.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: PostgreSQL öğrenme rehberi · SELECT, WHERE, ORDER BY, LIMIT · GROUP BY ve aggregate fonksiyonlar · WITH ve recursive CTE · Window function rehberi · SQL sorgu optimizasyonu