INNER JOIN ile İki Tabloyu Birleştirme
Veritabanlarıyla ciddi şekilde çalışmaya başladığınızda, er ya da geç karşınıza çıkan en temel ama bir o kadar da güçlü kavramlardan biri JOIN işlemleridir. Özellikle INNER JOIN, ilişkisel veritabanlarının ruhunu yansıtan, birden fazla tablodaki veriyi anlamlı şekilde bir araya getiren operasyondur. Bu yazıda MariaDB ve MySQL ortamlarında INNER JOIN kullanımını, gerçek dünya senaryolarıyla birlikte ele alacağız.
INNER JOIN Nedir ve Neden Kullanırız?
İlişkisel veritabanlarında veriyi normalize ederiz. Yani bir müşterinin bilgilerini bir tabloda, o müşterinin siparişlerini başka bir tabloda tutarız. Bu sayede veri tekrarını önlemiş oluruz. Peki bu iki tabloyu birlikte sorgulamak istediğimizde ne yapacağız?
İşte burada JOIN devreye girer. INNER JOIN, iki tablodaki eşleşen kayıtları birleştirir. Yani her iki tabloda da ortak bir değere sahip olan satırları döndürür. Eğer bir kayıt yalnızca bir tabloda varsa ve diğerinde karşılığı yoksa, INNER JOIN o kaydı sonuçlara dahil etmez.
Bunu şöyle düşünebilirsiniz: Elinizde iki liste var. Birinde çalışan isimleri ve departman kodları, diğerinde departman kodları ve departman isimleri. INNER JOIN, her iki listede de aynı departman koduna sahip olan satırları eşleştirip tek bir liste halinde sunar.
Test Ortamımızı Kuralım
Önce çalışacağımız veritabanını ve tabloları oluşturalım. Gerçek bir e-ticaret senaryosu üzerinden ilerleyeceğiz.
mysql -u root -p
CREATE DATABASE eticaret;
USE eticaret;
CREATE TABLE musteriler (
musteri_id INT PRIMARY KEY AUTO_INCREMENT,
ad VARCHAR(50) NOT NULL,
soyad VARCHAR(50) NOT NULL,
email VARCHAR(100) UNIQUE,
sehir VARCHAR(50),
kayit_tarihi DATE
);
CREATE TABLE siparisler (
siparis_id INT PRIMARY KEY AUTO_INCREMENT,
musteri_id INT NOT NULL,
urun_adi VARCHAR(100),
miktar INT,
fiyat DECIMAL(10,2),
siparis_tarihi DATETIME,
durum VARCHAR(20),
FOREIGN KEY (musteri_id) REFERENCES musteriler(musteri_id)
);
CREATE TABLE urunler (
urun_id INT PRIMARY KEY AUTO_INCREMENT,
urun_adi VARCHAR(100),
kategori VARCHAR(50),
stok_miktari INT,
birim_fiyat DECIMAL(10,2)
);
CREATE TABLE siparis_detay (
detay_id INT PRIMARY KEY AUTO_INCREMENT,
siparis_id INT,
urun_id INT,
adet INT,
birim_fiyat DECIMAL(10,2),
FOREIGN KEY (siparis_id) REFERENCES siparisler(siparis_id),
FOREIGN KEY (urun_id) REFERENCES urunler(urun_id)
);
Şimdi bu tablolara örnek veri ekleyelim:
INSERT INTO musteriler (ad, soyad, email, sehir, kayit_tarihi) VALUES
('Ahmet', 'Yilmaz', '[email protected]', 'Istanbul', '2023-01-15'),
('Fatma', 'Kaya', '[email protected]', 'Ankara', '2023-02-20'),
('Mehmet', 'Demir', '[email protected]', 'Izmir', '2023-03-10'),
('Ayse', 'Celik', '[email protected]', 'Bursa', '2023-04-05'),
('Ali', 'Sahin', '[email protected]', 'Istanbul', '2023-05-18'),
('Zeynep', 'Arslan', '[email protected]', 'Antalya', '2023-06-22');
INSERT INTO urunler (urun_adi, kategori, stok_miktari, birim_fiyat) VALUES
('Laptop Dell XPS', 'Elektronik', 15, 25000.00),
('iPhone 15', 'Elektronik', 30, 45000.00),
('Mekanik Klavye', 'Aksesuvar', 50, 1500.00),
('Oyuncu Mouse', 'Aksesuvar', 75, 800.00),
('27 Inc Monitor', 'Elektronik', 20, 12000.00);
INSERT INTO siparisler (musteri_id, urun_adi, miktar, fiyat, siparis_tarihi, durum) VALUES
(1, 'Laptop', 1, 25000.00, '2024-01-10 10:30:00', 'Tamamlandi'),
(2, 'Telefon', 1, 45000.00, '2024-01-12 14:15:00', 'Kargoda'),
(1, 'Klavye', 2, 3000.00, '2024-01-15 09:00:00', 'Tamamlandi'),
(3, 'Monitor', 1, 12000.00, '2024-01-18 16:45:00', 'Iptal'),
(5, 'Mouse', 3, 2400.00, '2024-01-20 11:20:00', 'Tamamlandi');
INSERT INTO siparis_detay (siparis_id, urun_id, adet, birim_fiyat) VALUES
(1, 1, 1, 25000.00),
(2, 2, 1, 45000.00),
(3, 3, 2, 1500.00),
(4, 5, 1, 12000.00),
(5, 4, 3, 800.00);
Dikkat ettin mi? 4. ve 6. müşteriler (Ayse ve Zeynep) hiç sipariş vermemiş. Bu INNER JOIN’in davranışını test ederken işimize yarayacak.
Temel INNER JOIN Sözdizimi
INNER JOIN’in temel yapısı şöyle:
SELECT tablo1.sutun1, tablo2.sutun2
FROM tablo1
INNER JOIN tablo2 ON tablo1.ortak_sutun = tablo2.ortak_sutun;
INNER kelimesini yazmak zorunlu değil, sadece JOIN yazmak da aynı sonucu verir. Ancak kodun okunabilirliği için INNER JOIN yazmayı alışkanlık haline getirmenizi öneririm. Özellikle birden fazla JOIN tipi kullandığınızda hangisinin hangisi olduğu anında anlaşılır.
İlk Gerçek Dünya Sorgumuz: Müşteri ve Sipariş Bilgilerini Birleştirme
En klasik senaryo ile başlayalım. Hangi müşterinin hangi siparişi verdiğini görmek istiyoruz:
SELECT
m.musteri_id,
m.ad,
m.soyad,
m.sehir,
s.siparis_id,
s.urun_adi,
s.fiyat,
s.durum,
s.siparis_tarihi
FROM musteriler m
INNER JOIN siparisler s ON m.musteri_id = s.musteri_id
ORDER BY s.siparis_tarihi DESC;
Bu sorguyu çalıştırdığınızda Ayse ve Zeynep’in sonuçlarda görünmediğini fark edeceksiniz. Çünkü bu müşterilerin siparisler tablosunda eşleşen kaydı yok. INNER JOIN yalnızca her iki tabloda da eşleşme olan satırları döndürür. Bu davranış bazen istediğiniz şeydir, bazen değil. Eğer siparişi olmayan müşterileri de görmek istiyorsanız LEFT JOIN kullanmanız gerekir ama o başka bir yazının konusu.
Yukarıdaki sorguda tablo alias (takma ad) kullandığımıza dikkat edin. musteriler m ve siparisler s şeklinde kısaltmalar tanımladık. Bu sayede musteriler.musteri_id yerine m.musteri_id yazabiliyoruz. Büyük sorgularda bu okunabilirliği ciddi ölçüde artırır.
WHERE ile INNER JOIN Kombinasyonu
Gerçek hayatta sorgularınızı sadece JOIN ile bırakmayacaksınız. Filtreler ekleyeceksiniz. Örneğin yalnızca İstanbul’daki müşterilerin tamamlanmış siparişlerini görmek istiyorsunuz:
SELECT
m.ad,
m.soyad,
m.sehir,
s.siparis_id,
s.urun_adi,
s.fiyat,
s.durum
FROM musteriler m
INNER JOIN siparisler s ON m.musteri_id = s.musteri_id
WHERE m.sehir = 'Istanbul'
AND s.durum = 'Tamamlandi'
ORDER BY s.fiyat DESC;
WHERE koşulu JOIN işleminden sonra uygulanır. Yani önce iki tablo birleştirilir, ardından WHERE filtrelemesi yapılır. Bu sıralamayı anlamak, özellikle büyük tablolarda performans optimizasyonu yaparken önemli.
Üç Tabloyu INNER JOIN ile Birleştirme
Şimdi işleri biraz daha karmaşık hale getirelim. Sipariş, müşteri ve ürün detaylarını aynı anda görmek istiyoruz:
SELECT
m.ad AS musteri_adi,
m.soyad AS musteri_soyadi,
s.siparis_id,
s.siparis_tarihi,
u.urun_adi,
u.kategori,
sd.adet,
sd.birim_fiyat,
(sd.adet * sd.birim_fiyat) AS toplam_tutar
FROM musteriler m
INNER JOIN siparisler s ON m.musteri_id = s.musteri_id
INNER JOIN siparis_detay sd ON s.siparis_id = sd.siparis_id
INNER JOIN urunler u ON sd.urun_id = u.urun_id
ORDER BY s.siparis_tarihi DESC;
Burada dört tabloyu zincir şeklinde birbirine bağladık. Her INNER JOIN bir öncekinin sonucuna ekleniyor. Dikkat etmeniz gereken nokta şu: Her JOIN için mantıklı bir bağlantı koşulu olması gerekiyor. Rastgele tablolar arasında JOIN yaparsanız kartezyen çarpım gibi anlamsız sonuçlar elde edersiniz.
Ayrıca AS anahtar kelimesiyle sütunlara alias verdik. (sd.adet * sd.birim_fiyat) AS toplam_tutar kısmı ise hesaplanmış bir sütun. JOIN içinde matematiksel işlemler yapabilirsiniz.
Aggregate Fonksiyonlar ile INNER JOIN
Sysadmin olarak veritabanı raporları hazırlamak durumunda kalırsınız. İşte burada GROUP BY ve aggregate fonksiyonlar devreye girer:
SELECT
m.ad,
m.soyad,
m.sehir,
COUNT(s.siparis_id) AS toplam_siparis,
SUM(s.fiyat) AS toplam_harcama,
AVG(s.fiyat) AS ortalama_siparis_tutari,
MAX(s.siparis_tarihi) AS son_siparis_tarihi
FROM musteriler m
INNER JOIN siparisler s ON m.musteri_id = s.musteri_id
GROUP BY m.musteri_id, m.ad, m.soyad, m.sehir
HAVING COUNT(s.siparis_id) > 1
ORDER BY toplam_harcama DESC;
Bu sorgu oldukça kapsamlı şeyler yapıyor:
- COUNT(s.siparis_id): Her müşterinin toplam sipariş sayısını hesaplar
- SUM(s.fiyat): Her müşterinin toplam harcamasını bulur
- AVG(s.fiyat): Ortalama sipariş tutarını hesaplar
- MAX(s.siparis_tarihi): En son sipariş tarihini bulur
- HAVING COUNT(s.siparis_id) > 1: Yalnızca birden fazla sipariş veren müşterileri filtreler
WHERE ile HAVING arasındaki farka dikkat edin. WHERE, JOIN ve GROUP BY işlemi yapılmadan önce ham satırları filtreler. HAVING ise GROUP BY sonucunda oluşan grupları filtreler.
Performans İpuçları: Explain ile Sorgu Analizi
Gerçek üretim ortamında milyonlarca satır içeren tablolarla çalışacaksınız. Bu durumda sorgu performansı kritik hale gelir. EXPLAIN komutu sorgunuzun nasıl çalıştığını gösterir:
EXPLAIN SELECT
m.ad,
m.soyad,
s.siparis_id,
s.fiyat
FROM musteriler m
INNER JOIN siparisler s ON m.musteri_id = s.musteri_id
WHERE s.durum = 'Tamamlandi';
EXPLAIN çıktısında dikkat etmeniz gereken başlıca noktalar:
- type: “ALL” görüyorsanız full table scan yapılıyor demektir, bu kötü bir işaret
- key: Kullanılan index adını gösterir, NULL ise index kullanılmıyor
- rows: Taranacak tahmini satır sayısı, bu sayı düşük olmalı
- Extra: “Using filesort” veya “Using temporary” görüyorsanız optimizasyon gerekiyor
JOIN sorgularında performansı artırmak için şu adımları uygulayın:
- JOIN koşulunda kullanılan sütunlara index ekleyin
- WHERE koşulundaki sütunlara index ekleyin
- Sık kullanılan JOIN sorgularını view olarak tanımlayın
-- musteri_id sutununa index eklemek (foreign key oldugu icin genellikle otomatik eklenir)
CREATE INDEX idx_siparis_musteri ON siparisler(musteri_id);
CREATE INDEX idx_siparis_durum ON siparisler(durum);
CREATE INDEX idx_siparis_tarih ON siparisler(siparis_tarihi);
Pratik Senaryo: Stok Yönetimi Raporu
Sysadmin olarak ERP sistemlerini yönetirken sık sık karşılaşılan bir senaryo: Hangi ürünlerin satıldığını ve stok durumunu tek sorguda görmek.
SELECT
u.urun_id,
u.urun_adi,
u.kategori,
u.stok_miktari,
u.birim_fiyat,
COUNT(sd.detay_id) AS toplam_siparis_adedi,
COALESCE(SUM(sd.adet), 0) AS toplam_satilan_adet,
(u.stok_miktari - COALESCE(SUM(sd.adet), 0)) AS kalan_stok,
COALESCE(SUM(sd.adet * sd.birim_fiyat), 0) AS toplam_ciro
FROM urunler u
INNER JOIN siparis_detay sd ON u.urun_id = sd.urun_id
INNER JOIN siparisler s ON sd.siparis_id = s.siparis_id
WHERE s.durum != 'Iptal'
GROUP BY u.urun_id, u.urun_adi, u.kategori, u.stok_miktari, u.birim_fiyat
ORDER BY toplam_ciro DESC;
Bu sorguda dikkat çekici birkaç nokta var:
COALESCE fonksiyonu: Eğer SUM sonucu NULL dönerse (hiç sipariş yoksa) 0 olarak kabul eder. Aksi halde matematiksel işlemlerde NULL sorunlarıyla karşılaşırsınız.
WHERE s.durum != ‘Iptal’: İptal edilen siparişleri stok hesabının dışında tutuyoruz. Bu iş mantığı açısından kritik.
Hesaplanmış kalan stok: u.stok_miktari - COALESCE(SUM(sd.adet), 0) ifadesiyle anlık stok durumunu hesaplıyoruz.
ON Koşuluna Birden Fazla Kriter Eklemek
Bazen sadece bir sütun üzerinden JOIN yapmak yeterli olmaz. Birleşik anahtar kullanılan tablolarda veya ek filtreleme gerektiğinde ON koşuluna AND ekleyebilirsiniz:
SELECT
m.ad,
m.soyad,
s.siparis_id,
s.urun_adi,
s.fiyat,
s.durum
FROM musteriler m
INNER JOIN siparisler s
ON m.musteri_id = s.musteri_id
AND s.siparis_tarihi >= '2024-01-01'
AND s.durum IN ('Tamamlandi', 'Kargoda')
WHERE m.sehir IN ('Istanbul', 'Ankara')
ORDER BY m.soyad, s.siparis_tarihi;
Teknik olarak ON koşulundaki filtreleri WHERE’e de taşıyabilirsiniz. INNER JOIN için sonuç genellikle aynı olur. Ancak okunabilirlik açısından JOIN’e özgü koşulları ON’da, genel filtreleri WHERE’de tutmak iyi bir pratiktir.
Subquery ile INNER JOIN Kombinasyonu
Bazen bir alt sorgunun sonucunu JOIN’de kullanmanız gerekir. Bu oldukça güçlü bir teknik:
SELECT
m.ad,
m.soyad,
yuksek.toplam_harcama
FROM musteriler m
INNER JOIN (
SELECT
musteri_id,
SUM(fiyat) AS toplam_harcama
FROM siparisler
WHERE durum = 'Tamamlandi'
GROUP BY musteri_id
HAVING SUM(fiyat) > 10000
) AS yuksek ON m.musteri_id = yuksek.musteri_id
ORDER BY yuksek.toplam_harcama DESC;
Bu sorgu, yalnızca tamamlanmış siparişlerde 10.000 TL üzerinde harcama yapmış müşterileri listeliyor. Subquery içinde GROUP BY ve HAVING kullanarak filtrelenmiş bir veri seti oluşturduk, ardından bunu ana sorguya INNER JOIN ile bağladık.
Bu yaklaşımın faydaları:
- Karmaşık iş mantığını parçalara bölebilirsiniz
- Performans açısından bazen doğrudan JOIN’den daha verimlidir
- Kodu daha okunabilir ve bakımı kolay hale getirir
View Oluşturarak Tekrar Kullanım
Sık kullandığınız INNER JOIN sorgularını view olarak kaydedebilirsiniz:
CREATE VIEW musteri_siparis_ozeti AS
SELECT
m.musteri_id,
m.ad,
m.soyad,
m.email,
m.sehir,
COUNT(s.siparis_id) AS siparis_sayisi,
SUM(CASE WHEN s.durum = 'Tamamlandi' THEN s.fiyat ELSE 0 END) AS tamamlanan_harcama,
MAX(s.siparis_tarihi) AS son_siparis
FROM musteriler m
INNER JOIN siparisler s ON m.musteri_id = s.musteri_id
GROUP BY m.musteri_id, m.ad, m.soyad, m.email, m.sehir;
-- View'i kullanalim
SELECT * FROM musteri_siparis_ozeti WHERE sehir = 'Istanbul';
View tanımladıktan sonra onu sanki normal bir tablo gibi sorgulayabilirsiniz. Uygulama geliştiricilerine karmaşık JOIN sorgularını gizleyip sade bir arayüz sunmak için de kullanışlıdır.
Yaygın Hatalar ve Çözümleri
Sysadminlerin ve geliştiricilerin INNER JOIN kullanırken sık yaptığı hatalar:
Sütun adı belirsizliği: İki tabloda aynı isimde sütun varsa hangi tabloya ait olduğunu belirtmezseniz hata alırsınız.
-- YANLIS: ambiguous column
SELECT musteri_id, ad FROM musteriler INNER JOIN siparisler ON ...
-- DOGRU: tablo adiyla nitele
SELECT m.musteri_id, m.ad, s.siparis_id FROM musteriler m INNER JOIN siparisler s ON ...
ON yerine WHERE’de JOIN koşulu yazmak: Eski SQL sözdiziminde WHERE’de JOIN koşulu yazılırdı. Bu modern kodda önerilmez çünkü JOIN mantığı ile filtreleme mantığı karışır, okunması güçleşir.
-- ESKI VE ONERILMEYEN:
SELECT * FROM musteriler m, siparisler s
WHERE m.musteri_id = s.musteri_id;
-- MODERN VE ONERILEN:
SELECT * FROM musteriler m
INNER JOIN siparisler s ON m.musteri_id = s.musteri_id;
İndeks eksikliği: JOIN koşulundaki sütunlarda index yoksa büyük tablolarda sorgular çok yavaş çalışır. Her zaman foreign key sütunlarının indexli olduğundan emin olun.
Sonuç
INNER JOIN, ilişkisel veritabanlarının temel taşlarından biri. Birkaç tablo arasında anlamlı bağlantılar kurarak veriyi zenginleştirmenizi, raporlar oluşturmanızı ve iş mantığını SQL düzeyinde uygulamanızı sağlıyor.
Bu yazıda ele aldığımız konuları özetlersek:
- Temel INNER JOIN sözdizimi ve alias kullanımı
- WHERE ve HAVING ile birlikte filtreleme
- Üç ve daha fazla tabloyu zincir JOIN ile birleştirme
- GROUP BY ve aggregate fonksiyonlarla raporlama
- EXPLAIN ile sorgu performans analizi
- Subquery ile INNER JOIN kombinasyonu
- View kullanarak tekrar kullanılabilir sorgu şablonları
Bir sysadmin olarak bu yapıları iyi kavramak, yönettiğiniz sistemlerdeki yavaş sorguları teşhis etmek, veritabanı raporları oluşturmak ve uygulama sorunlarını veritabanı düzeyinde debug etmek için vazgeçilmez. Bir sonraki adım olarak LEFT JOIN ve RIGHT JOIN kavramlarını incelemenizi, ardından EXPLAIN çıktılarını derinlemesine anlamak için query optimization konusuna dalmanızı öneririm.
Pratik yapmanın en iyi yolu, kendi test veritabanınızı oluşturup farklı senaryoları denemeniz. Yukarıdaki örnekleri birebir çalıştırın, sonra kendi varyasyonlarınızı yazın. SQL’in güzelliği deneme yanılmayla öğrenmeye çok uygun olması.
