Birden Fazla Tablo JOIN ile Birleştirme: MariaDB ve MySQL Örnekleri
Veritabanlarıyla çalışırken en sık karşılaşılan ihtiyaçlardan biri birden fazla tablodan veri çekmektir. Tek bir tabloya sıkışıp kalmak gerçek dünyada pek mümkün değil; müşteri siparişleri, kullanıcı rolleri, ürün kategorileri gibi senaryolarda veriler kaçınılmaz olarak birden fazla tabloya dağılır. İşte bu noktada SQL’in en güçlü silahlarından biri olan JOIN ifadeleri devreye girer. Bu yazıda MariaDB ve MySQL ortamlarında birden fazla tabloyu JOIN ile birleştirmeyi, gerçek dünya senaryolarıyla, adım adım ele alacağız.
JOIN Nedir ve Neden Birden Fazla Tablo Gerekir?
İlişkisel veritabanlarının temel mantığı normalizasyon üzerine kuruludur. Yani veriyi tekrar etmeden, farklı tablolara bölüp ilişkilendirmek. Bir e-ticaret uygulaması düşündüğünüzde customers, orders, order_items, products, categories gibi onlarca tablo olabilir. Bu tabloları birbirine bağlamadan anlamlı bir rapor üretmek neredeyse imkansız.
JOIN, iki ya da daha fazla tablodaki satırları ortak bir sütun üzerinden birleştirerek tek bir sonuç kümesi oluşturur. Birden fazla JOIN kullandığınızda ise bu zincirleme bir yapıya dönüşür; her yeni JOIN bir önceki sonuç kümesine yeni bir tablo ekler.
Örnek Veritabanı Yapısı
Bu yazı boyunca kullanacağımız veritabanı yapısını önce oluşturalım. Bir e-ticaret senaryosu kullanacağız.
-- Veritabanını oluştur
CREATE DATABASE eticaret;
USE eticaret;
-- Müşteri tablosu
CREATE TABLE customers (
customer_id INT AUTO_INCREMENT PRIMARY KEY,
first_name VARCHAR(50),
last_name VARCHAR(50),
email VARCHAR(100),
city VARCHAR(50)
);
-- Kategori tablosu
CREATE TABLE categories (
category_id INT AUTO_INCREMENT PRIMARY KEY,
category_name VARCHAR(100),
parent_category_id INT NULL
);
-- Ürün tablosu
CREATE TABLE products (
product_id INT AUTO_INCREMENT PRIMARY KEY,
product_name VARCHAR(150),
price DECIMAL(10,2),
stock INT,
category_id INT,
FOREIGN KEY (category_id) REFERENCES categories(category_id)
);
-- Sipariş tablosu
CREATE TABLE orders (
order_id INT AUTO_INCREMENT PRIMARY KEY,
customer_id INT,
order_date DATE,
status VARCHAR(20),
FOREIGN KEY (customer_id) REFERENCES customers(customer_id)
);
-- Sipariş detay tablosu
CREATE TABLE order_items (
item_id INT AUTO_INCREMENT PRIMARY KEY,
order_id INT,
product_id INT,
quantity INT,
unit_price DECIMAL(10,2),
FOREIGN KEY (order_id) REFERENCES orders(order_id),
FOREIGN KEY (product_id) REFERENCES products(product_id)
);
-- Kargo tablosu
CREATE TABLE shipments (
shipment_id INT AUTO_INCREMENT PRIMARY KEY,
order_id INT,
carrier VARCHAR(50),
tracking_number VARCHAR(100),
shipped_date DATE,
delivered_date DATE,
FOREIGN KEY (order_id) REFERENCES orders(order_id)
);
Temel JOIN Türleri Hızlı Özeti
Birden fazla tablo JOIN’ine geçmeden önce temel JOIN türlerini kısaca hatırlayalım:
- INNER JOIN: Her iki tabloda da eşleşen kayıtları getirir
- LEFT JOIN: Sol tablodaki tüm kayıtları, sağ tabloda eşleşen varsa onu getirir; yoksa NULL
- RIGHT JOIN: Sağ tablodaki tüm kayıtları getirir; sol tarafta eşleşme yoksa NULL
- CROSS JOIN: Her satırı diğer tablonun her satırıyla birleştirir, kartezyen çarpım
- SELF JOIN: Bir tablonun kendisiyle birleştirilmesi
İki Tabloyu JOIN ile Birleştirme (Isınma Turu)
Üç veya daha fazla tabloya geçmeden önce iki tablo örneğini görelim. Müşteri adı ve sipariş tarihini birlikte çekelim:
SELECT
c.first_name,
c.last_name,
o.order_id,
o.order_date,
o.status
FROM customers c
INNER JOIN orders o ON c.customer_id = o.customer_id
WHERE o.order_date >= '2024-01-01'
ORDER BY o.order_date DESC;
Bu basit örnekte customers ile orders tablolarını customer_id üzerinden birleştirdik. Şimdi işleri biraz karıştıralım.
Üç Tablo JOIN: Sipariş, Müşteri ve Ürün Bilgilerini Birleştirme
Gerçek bir sipariş raporu için müşteri adını, sipariş tarihini ve hangi ürünlerin sipariş edildiğini tek sorguda çekmek isteriz. Bunun için customers, orders, order_items ve products tablolarını zincir halinde bağlamamız gerekiyor.
SELECT
c.first_name,
c.last_name,
o.order_id,
o.order_date,
p.product_name,
oi.quantity,
oi.unit_price,
(oi.quantity * oi.unit_price) AS line_total
FROM customers c
INNER JOIN orders o ON c.customer_id = o.customer_id
INNER JOIN order_items oi ON o.order_id = oi.order_id
INNER JOIN products p ON oi.product_id = p.product_id
ORDER BY o.order_id, p.product_name;
Burada dikkat etmemiz gereken nokta JOIN zincirinin mantıklı bir sıra izlemesidir. orders tablosu hem customers hem de order_items ile ilişkili olduğundan köprü görevi görüyor. MariaDB bu sorguyu yukarıdan aşağıya okurken her JOIN adımında bir önceki sonuç kümesine yeni tabloyu ekliyor.
Dört Tablo JOIN: Kategori Bilgisi de Ekleyelim
Bir adım daha ileri gidip ürünlerin hangi kategoride olduğunu da dahil edelim:
SELECT
c.first_name,
c.last_name,
o.order_id,
o.order_date,
cat.category_name,
p.product_name,
oi.quantity,
oi.unit_price,
(oi.quantity * oi.unit_price) AS line_total
FROM customers c
INNER JOIN orders o ON c.customer_id = o.customer_id
INNER JOIN order_items oi ON o.order_id = oi.order_id
INNER JOIN products p ON oi.product_id = p.product_id
INNER JOIN categories cat ON p.category_id = cat.category_id
WHERE o.status = 'tamamlandi'
ORDER BY cat.category_name, o.order_id;
Bu sorgu tamamlanmış siparişlerde her ürünün hangi kategoriye ait olduğunu gösteriyor. Mesela bir raporlama aracı kuruyorsunuz, her kategoriden ne kadar ciro geldiğini görmek istiyorsunuz; bu yapı tam olarak bunun için.
LEFT JOIN ile Tüm Müşterileri Dahil Etme
Bazen INNER JOIN ile kaybedilen veriler önemlidir. Hiç sipariş vermemiş müşterileri de görmek isteyebilirsiniz. İşte LEFT JOIN burada devreye giriyor:
SELECT
c.customer_id,
c.first_name,
c.last_name,
c.email,
COUNT(o.order_id) AS total_orders,
COALESCE(SUM(oi.quantity * oi.unit_price), 0) AS total_spent
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
LEFT JOIN order_items oi ON o.order_id = oi.order_id
GROUP BY c.customer_id, c.first_name, c.last_name, c.email
ORDER BY total_spent DESC;
Burada her iki JOIN de LEFT JOIN olmalı. Eğer ilk JOIN’i LEFT yapıp ikinciyi INNER yaparsanız, siparişi olmayan müşteriler ilk JOIN’den geçse bile order_items ile INNER JOIN yapılırken düşer. Bu ince ama kritik bir nokta; birden fazla JOIN kullanırken JOIN türlerinin tutarlı olmasına dikkat edin.
Beş Tablo JOIN: Kargo Bilgisini de Ekleyelim
Şimdi işi daha da gerçekçi hale getirelim. Müşteri, sipariş, ürün ve kargo bilgilerini tek sorguda çekelim. Bu, bir sipariş takip sayfasının arkasındaki tipik bir sorgu olabilir:
SELECT
c.first_name,
c.last_name,
o.order_id,
o.order_date,
o.status AS order_status,
p.product_name,
oi.quantity,
cat.category_name,
sh.carrier,
sh.tracking_number,
sh.shipped_date,
sh.delivered_date,
DATEDIFF(sh.delivered_date, sh.shipped_date) AS delivery_days
FROM orders o
INNER JOIN customers c ON o.customer_id = c.customer_id
INNER JOIN order_items oi ON o.order_id = oi.order_id
INNER JOIN products p ON oi.product_id = p.product_id
INNER JOIN categories cat ON p.category_id = cat.category_id
LEFT JOIN shipments sh ON o.order_id = sh.order_id
WHERE o.order_date BETWEEN '2024-01-01' AND '2024-12-31'
ORDER BY o.order_id;
shipments tablosunu LEFT JOIN ile bağladım çünkü bazı siparişler henüz kargoya verilmemiş olabilir; bu siparişlerin de sorguda görünmesini istiyorum, sadece kargo bilgileri NULL gelecek.
Ayrıca dikkat ettiniz mi, bu sefer FROM ile orders tablosundan başladım, customers‘dan değil. JOIN sırası sonucu değiştirmez; ama performans açısından ve okunabilirlik açısından en merkezi tablodan başlamak genellikle iyi bir pratiktir. orders tablosu diğer tüm tablolarla ilişkili olduğundan merkez seçtim.
Self JOIN ile Hiyerarşik Kategoriler
categories tablomuzdaki parent_category_id sütununu hatırlıyor musunuz? Bu sütun, kategorilerin hiyerarşik olmasını sağlıyor. “Elektronik” bir üst kategori, “Telefon” onun alt kategorisi gibi. Bu yapıyı sorgulamak için self JOIN kullanırız:
SELECT
alt.category_id,
alt.category_name AS alt_kategori,
ust.category_name AS ust_kategori
FROM categories alt
LEFT JOIN categories ust ON alt.parent_category_id = ust.category_id
ORDER BY ust.category_name, alt.category_name;
Bu örnekte categories tablosunu iki kez JOIN ediyoruz; biri alt kategori (alt alias’ı), biri üst kategori (ust alias’ı) olarak. Alias kullanımı burada zorunlu, yoksa MariaDB hangi tablonun hangi sütununu kastettiğinizi anlayamaz.
Alias Kullanımının Önemi
Birden fazla tablo kullanırken alias olmadan yazmak hem hataya açık hem de okuması imkansız hale gelir. Bazı pratik alias kuralları:
- c: customers
- o: orders
- oi: order_items
- p: products
- cat: categories
- sh: shipments
Tablonun baş harfini ya da kısaltmasını kullanmak, sorguyu okuyan herkesin (kendiniz dahil üç ay sonra) ne kastettiğinizi anlamasını kolaylaştırır.
Subquery ile JOIN Kombinasyonu
Bazen JOIN zincirini subquery ile güçlendirmek gerekir. Örneğin her kategoriden en pahalı ürünü sipariş eden müşterileri bulmak istiyoruz:
SELECT
c.first_name,
c.last_name,
p.product_name,
cat.category_name,
oi.unit_price
FROM customers c
INNER JOIN orders o ON c.customer_id = o.customer_id
INNER JOIN order_items oi ON o.order_id = oi.order_id
INNER JOIN products p ON oi.product_id = p.product_id
INNER JOIN categories cat ON p.category_id = cat.category_id
INNER JOIN (
SELECT
p2.category_id,
MAX(p2.price) AS max_price
FROM products p2
GROUP BY p2.category_id
) AS max_prices ON p.category_id = max_prices.category_id
AND oi.unit_price = max_prices.max_price
ORDER BY cat.category_name;
Burada bir subquery’yi sanki tablo gibi JOIN ile bağladık. Bu teknik derived table ya da inline view olarak bilinir. MariaDB bunu tam olarak destekler ve sorgu optimizasyonunda oldukça kullanışlıdır.
Performans için Dikkat Edilmesi Gerekenler
Birden fazla JOIN kullanırken performans kritik bir konu. Özellikle milyonlarca kayıt içeren tablolarda kötü yazılmış bir JOIN sorgusu sunucunuzu çökertebilir.
İndeks Kullanımı
JOIN yaptığınız sütunların mutlaka indeksli olması gerekir. Aşağıdaki indeks kontrol sorgusunu düzenli çalıştırın:
-- Tablolarınızdaki mevcut indeksleri kontrol edin
SHOW INDEX FROM orders;
SHOW INDEX FROM order_items;
SHOW INDEX FROM products;
-- Eksik indeksleri ekleyin
ALTER TABLE orders ADD INDEX idx_customer_id (customer_id);
ALTER TABLE order_items ADD INDEX idx_order_id (order_id);
ALTER TABLE order_items ADD INDEX idx_product_id (product_id);
ALTER TABLE products ADD INDEX idx_category_id (category_id);
ALTER TABLE shipments ADD INDEX idx_order_id (order_id);
Foreign key tanımlarken MariaDB otomatik indeks oluşturur; ama her zaman güvenmeyip kontrol etmek iyi alışkanlıktır.
EXPLAIN ile Sorgu Planını İnceleme
Yazdığınız karmaşık JOIN sorgusunun nasıl çalıştığını görmek için EXPLAIN kullanın:
EXPLAIN SELECT
c.first_name,
c.last_name,
o.order_id,
p.product_name,
cat.category_name
FROM customers c
INNER JOIN orders o ON c.customer_id = o.customer_id
INNER JOIN order_items oi ON o.order_id = oi.order_id
INNER JOIN products p ON oi.product_id = p.product_id
INNER JOIN categories cat ON p.category_id = cat.category_id
WHERE c.city = 'Istanbul';
EXPLAIN çıktısında şunlara dikkat edin:
- type sütununda
ALLgörüyorsanız tam tablo taraması yapılıyor demektir, bu kötü - type
refya daeq_refise indeks kullanılıyor, bu iyi - rows sütunu tahmini işlenecek satır sayısını gösterir; çok yüksekse sorguyu gözden geçirin
- Extra sütununda
Using filesortya daUsing temporarygörüyorsanız performans sorunu olabilir
JOIN Sırasının Önemi
MariaDB sorgu optimizatörü çoğu zaman doğru JOIN sırasını kendi bulur; ama bazen yardıma ihtiyaç duyar. STRAIGHT_JOIN ipucunu ihtiyatlı kullanabilirsiniz:
SELECT STRAIGHT_JOIN
c.first_name,
o.order_id,
p.product_name
FROM customers c
INNER JOIN orders o ON c.customer_id = o.customer_id
INNER JOIN order_items oi ON o.order_id = oi.order_id
INNER JOIN products p ON oi.product_id = p.product_id
WHERE c.city = 'Ankara'
LIMIT 100;
STRAIGHT_JOIN kullandığınızda MariaDB tabloları yazdığınız sırada işler. Bunu yalnızca EXPLAIN analizi yapıp optimizatörün yanlış karar verdiğini kanıtladıktan sonra kullanın.
Gerçek Dünya Senaryosu: Aylık Kategori Bazlı Satış Raporu
Bir e-ticaret yöneticisi her ay hangi kategorinin ne kadar sattığını görmek ister. İşte tam anlamıyla üretim ortamında kullanabileceğiniz bir sorgu:
SELECT
DATE_FORMAT(o.order_date, '%Y-%m') AS ay,
cat.category_name,
COUNT(DISTINCT o.order_id) AS siparis_sayisi,
COUNT(DISTINCT c.customer_id) AS musteri_sayisi,
SUM(oi.quantity) AS toplam_adet,
SUM(oi.quantity * oi.unit_price) AS toplam_ciro,
AVG(oi.unit_price) AS ortalama_birim_fiyat,
MIN(oi.unit_price) AS min_fiyat,
MAX(oi.unit_price) AS max_fiyat
FROM orders o
INNER JOIN customers c ON o.customer_id = c.customer_id
INNER JOIN order_items oi ON o.order_id = oi.order_id
INNER JOIN products p ON oi.product_id = p.product_id
INNER JOIN categories cat ON p.category_id = cat.category_id
WHERE o.status = 'tamamlandi'
AND o.order_date >= DATE_SUB(CURDATE(), INTERVAL 12 MONTH)
GROUP BY DATE_FORMAT(o.order_date, '%Y-%m'), cat.category_id, cat.category_name
ORDER BY ay DESC, toplam_ciro DESC;
Bu sorguyu bir stored procedure içine alarak aylık otomatik çalıştırabilir, sonuçları ayrı bir rapor tablosuna yazabilirsiniz. Böylece her rapor görüntüleme isteğinde bu ağır sorguyu yeniden çalıştırmazsınız.
Yaygın Hatalar ve Çözümleri
Birden fazla tablo JOIN’inde en sık yapılan hatalar şunlardır:
- Ambiguous column hatası: Aynı isimde sütunlar birden fazla tabloda olduğunda tablo adı ya da alias kullanmadan yazarsanız bu hatayı alırsınız. Çözüm: Her sütun referansında alias kullanmayı alışkanlık edinin
- Kartezyen çarpım: JOIN koşulunu unutmak ya da yanlış yazmak, tablolar arasında her satırın her satırla eşleşmesine neden olur. Bir tabloda 1000, diğerinde 1000 kayıt varsa sonuçta 1.000.000 satır görürsünüz
- LEFT JOIN’den sonra INNER JOIN: Az önce bahsettiğimiz gibi, LEFT JOIN ile tuttuğunuz NULL kayıtları sonraki bir INNER JOIN ile kaybedebilirsiniz
- GROUP BY eksikliği: Aggregate fonksiyon kullandığınızda (SUM, COUNT vb.) GROUP BY listesinde aggregate olmayan tüm SELECT sütunlarının bulunması gerekir. MariaDB bazen hata vermeden yanlış sonuç üretebilir;
ONLY_FULL_GROUP_BYmodunu aktif tutun
ONLY_FULL_GROUP_BY Modunu Kontrol Edin
-- Mevcut SQL modunu kontrol et
SELECT @@sql_mode;
-- ONLY_FULL_GROUP_BY aktif mi diye kontrol
SHOW VARIABLES LIKE 'sql_mode';
-- Gerekirse session seviyesinde aktif hale getir
SET SESSION sql_mode = 'STRICT_TRANS_TABLES,ONLY_FULL_GROUP_BY,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION';
Bu modu aktif tutmak başlangıçta bazı sorgularınızı kırar ama uzun vadede veri tutarlılığınızı korur.
Sonuç
Birden fazla tablo JOIN ile birleştirmek başlangıçta korkutucu görünebilir; ama mantığını kavradıktan sonra son derece güçlü bir araç olduğunu anlarsınız. Özetlemek gerekirse:
- Zincir mantığını kurun: Her tablo bir öncekiyle nasıl bağlanıyor?
- Alias kullanmayı alışkanlık edinin: Hem okunabilirlik hem hata önleme açısından kritik
- JOIN türünü doğru seçin: Tüm kayıtlar görünmeli mi yoksa sadece eşleşenler mi?
- İndeksleri kontrol edin: JOIN sütunlarının indeksli olduğundan emin olun
- EXPLAIN ile sorgu planını inceleyin: Tahminlere değil, verilere güvenin
- Büyük veri setlerinde WHERE koşullarını erken uygulayın: Mümkün olan en az kayıt üzerinde JOIN yapın
Birden fazla JOIN içeren sorgular yazarken en önemli şey her adımı mantıklı şekilde kurmak. Önce iki tabloyu birleştirin ve sonucu doğrulayın, sonra üçüncüyü ekleyin. Büyük sorguları bir anda yazmak yerine adım adım inşa etmek hem hata bulmayı kolaylaştırır hem de performans sorunlarını erkenden tespit etmenizi sağlar. Bu disiplini edindiğinizde en karmaşık raporlama sorgularını bile güvenle yazabileceksiniz.
