MariaDB’de FULL OUTER JOIN Alternatifi Nasıl Kullanılır
Veritabanı yönetiminde sık karşılaşılan durumlardan biri, iki tablonun tüm kayıtlarını birleştirme ihtiyacıdır. Oracle, PostgreSQL gibi sistemlerde bu işlemi FULL OUTER JOIN ile kolayca yapabiliyorsunuz. Ancak MariaDB ve MySQL bu sözdizimini desteklemez. Peki ya projenizde MariaDB kullanıyorsunuz ve gerçekten bu davranışa ihtiyaç duyuyorsunuz? İşte bu yazıda tam olarak bunu ele alacağız. MariaDB’de FULL OUTER JOIN alternatiflerini, gerçek dünya senaryolarıyla birlikte adım adım inceleyeceğiz.
FULL OUTER JOIN Nedir, Neden Gereklidir?
Önce temel kavramı netleştirelim. FULL OUTER JOIN, iki tablonun tüm satırlarını döndürür. Sol tabloda eşleşme olmayan satırlar da, sağ tabloda eşleşme olmayan satırlar da sonuç kümesine dahil edilir. Eşleşme olmayan taraftaki sütunlar NULL değeri alır.
Bu birleştirme türü özellikle şu durumlarda işe yarar:
- Veri bütünlüğü kontrolü: İki sistemden gelen kayıtları karşılaştırıp hangisinde eksik olduğunu bulmak
- Raporlama: Hem müşteri hem de sipariş verilerini, eşleşme olsun ya da olmasın birlikte göstermek
- Veri taşıma sonrası doğrulama: Kaynak ve hedef tablolar arasındaki farkları tespit etmek
- Muhasebe uyumlaştırması: İki farklı sistemdeki finansal kayıtları karşılaştırmak
MariaDB’nin bu JOIN türünü desteklememesi bir eksiklik gibi görünse de, aynı sonuca UNION ve LEFT JOIN / RIGHT JOIN kombinasyonlarıyla ulaşmak mümkündür.
Temel Yaklaşım: LEFT JOIN UNION RIGHT JOIN
MariaDB’de FULL OUTER JOIN etkisini yaratmanın en yaygın ve güvenilir yöntemi şudur: Önce LEFT JOIN yaparsınız, ardından RIGHT JOIN yaparsınız ve ikisini UNION ile birleştirirsiniz.
Önce örnek tablolarımızı oluşturalım:
-- Çalışanlar tablosu
CREATE TABLE calisanlar (
id INT PRIMARY KEY AUTO_INCREMENT,
ad VARCHAR(100),
departman_id INT
);
-- Departmanlar tablosu
CREATE TABLE departmanlar (
id INT PRIMARY KEY AUTO_INCREMENT,
departman_adi VARCHAR(100)
);
-- Test verileri
INSERT INTO departmanlar (departman_adi) VALUES
('Yazılım'), ('Pazarlama'), ('Muhasebe'), ('İnsan Kaynakları');
INSERT INTO calisanlar (ad, departman_id) VALUES
('Ahmet Yılmaz', 1),
('Mehmet Kaya', 1),
('Ayşe Demir', 2),
('Fatma Çelik', 5), -- Var olmayan departman
('Ali Koç', NULL); -- Departmanı atanmamış
Şimdi FULL OUTER JOIN alternatifini uygulayalım:
-- MariaDB'de FULL OUTER JOIN alternatifi
SELECT
c.id AS calisan_id,
c.ad AS calisan_adi,
d.id AS departman_id,
d.departman_adi
FROM calisanlar c
LEFT JOIN departmanlar d ON c.departman_id = d.id
UNION
SELECT
c.id AS calisan_id,
c.ad AS calisan_adi,
d.id AS departman_id,
d.departman_adi
FROM calisanlar c
RIGHT JOIN departmanlar d ON c.departman_id = d.id;
Bu sorgu şunları döndürecektir:
- Departmanı olan tüm çalışanlar
- Departmanı olmayan veya geçersiz departmana sahip çalışanlar (
NULLdepartman bilgisiyle) - Hiç çalışanı olmayan departmanlar (
NULLçalışan bilgisiyle)
UNION vs UNION ALL: Hangisini Kullanmalısınız?
UNION ve UNION ALL arasındaki fark kritiktir. UNION tekrar eden satırları kaldırır (yani DISTINCT uygular), UNION ALL ise tüm satırları korur.
FULL OUTER JOIN davranışını doğru simüle etmek istiyorsanız genellikle UNION ALL kullanmak ve sadece RIGHT JOIN tarafında eşleşmeyen satırları almak daha verimlidir:
-- Daha verimli ve doğru yöntem: UNION ALL ile
SELECT
c.id AS calisan_id,
c.ad AS calisan_adi,
d.id AS departman_id,
d.departman_adi
FROM calisanlar c
LEFT JOIN departmanlar d ON c.departman_id = d.id
UNION ALL
SELECT
c.id AS calisan_id,
c.ad AS calisan_adi,
d.id AS departman_id,
d.departman_adi
FROM calisanlar c
RIGHT JOIN departmanlar d ON c.departman_id = d.id
WHERE c.id IS NULL;
Bu yaklaşımın avantajları:
UNION ALLtekrar eden satırları kaldırmak için ek işlem yapmaz, performans açısından daha hızlıdırWHERE c.id IS NULLkoşulu sadece sağ tarafta eşleşmeyen satırları (yani çalışanı olmayan departmanları) getirir- Böylece çakışma olmadan iki küme birleştirilmiş olur
Gerçek Dünya Senaryosu 1: E-Ticaret Veri Uyumlaştırması
Diyelim ki bir e-ticaret platformu yönetiyorsunuz. İki farklı sistem var: biri sipariş sistemi, diğeri ödeme sistemi. Bu iki sistemdeki kayıtları karşılaştırmanız gerekiyor.
-- Sipariş ve ödeme tablolarını oluştur
CREATE TABLE siparisler (
siparis_id VARCHAR(20) PRIMARY KEY,
musteri_adi VARCHAR(100),
tutar DECIMAL(10,2),
tarih DATE
);
CREATE TABLE odemeler (
odeme_id VARCHAR(20) PRIMARY KEY,
siparis_ref VARCHAR(20),
odenen_tutar DECIMAL(10,2),
odeme_tarihi DATE
);
-- Verileri ekle (kasıtlı tutarsızlıklar var)
INSERT INTO siparisler VALUES
('SIP001', 'Kemal Arslan', 250.00, '2024-01-15'),
('SIP002', 'Zeynep Yurt', 180.50, '2024-01-16'),
('SIP003', 'Bora Şahin', 320.00, '2024-01-17'),
('SIP004', 'Canan Öz', 95.00, '2024-01-18'); -- Ödemesi yok
INSERT INTO odemeler VALUES
('OD001', 'SIP001', 250.00, '2024-01-15'),
('OD002', 'SIP002', 180.50, '2024-01-16'),
('OD003', 'SIP003', 320.00, '2024-01-17'),
('OD999', 'SIP999', 150.00, '2024-01-19'); -- Siparişi yok, ödemesi var
-- FULL OUTER JOIN alternatifi ile uyumsuzlukları bul
SELECT
COALESCE(s.siparis_id, o.siparis_ref) AS referans_no,
s.musteri_adi,
s.tutar AS siparis_tutari,
o.odenen_tutar,
CASE
WHEN s.siparis_id IS NULL THEN 'Siparissiz Odeme'
WHEN o.odeme_id IS NULL THEN 'Odemesiz Siparis'
WHEN s.tutar != o.odenen_tutar THEN 'Tutar Uyumsuzlugu'
ELSE 'Eslesme Var'
END AS durum
FROM siparisler s
LEFT JOIN odemeler o ON s.siparis_id = o.siparis_ref
UNION ALL
SELECT
o.siparis_ref AS referans_no,
NULL AS musteri_adi,
NULL AS siparis_tutari,
o.odenen_tutar,
'Siparissiz Odeme' AS durum
FROM siparisler s
RIGHT JOIN odemeler o ON s.siparis_id = o.siparis_ref
WHERE s.siparis_id IS NULL;
Bu sorgu size hangi siparişlerin ödenmediğini ve hangi ödemelerin sisteme girilmiş bir siparişe karşılık gelmediğini gösterir. Gerçek hayatta bu tür tutarsızlıklar finansal açıdan kritik olabilir.
Gerçek Dünya Senaryosu 2: İnsan Kaynakları Sistemi
Bir insan kaynakları uygulamasında çalışanlar ve pozisyonlar arasındaki boşlukları bulmak gerekiyor olabilir. Hangi pozisyonlar doldurulamadı, hangi çalışanlar pozisyonsuz kaldı?
-- Pozisyonlar ve atamalar tabloları
CREATE TABLE pozisyonlar (
pozisyon_id INT PRIMARY KEY AUTO_INCREMENT,
pozisyon_adi VARCHAR(100),
departman VARCHAR(50),
acik_kontenjan INT DEFAULT 1
);
CREATE TABLE atamalar (
atama_id INT PRIMARY KEY AUTO_INCREMENT,
calisan_adi VARCHAR(100),
pozisyon_id INT,
baslangic_tarihi DATE
);
-- Veri girişi
INSERT INTO pozisyonlar (pozisyon_adi, departman) VALUES
('Kıdemli Yazılım Geliştirici', 'Yazılım'),
('DevOps Mühendisi', 'Altyapı'),
('Veri Bilimci', 'Analitik'),
('Proje Yöneticisi', 'PMO'),
('Sistem Yöneticisi', 'Altyapı');
INSERT INTO atamalar (calisan_adi, pozisyon_id, baslangic_tarihi) VALUES
('Sercan Kılıç', 1, '2024-01-01'),
('Deniz Acar', 2, '2024-01-15'),
('Gül Taş', NULL, '2024-02-01'), -- Pozisyonu atanmamış
('Tuna Bal', 99, '2024-02-10'); -- Geçersiz pozisyon
-- Boşluk analizi
SELECT
COALESCE(p.pozisyon_id, a.pozisyon_id) AS pozisyon_ref,
p.pozisyon_adi,
p.departman,
a.calisan_adi,
CASE
WHEN a.atama_id IS NULL THEN 'Bos Pozisyon - Isealim Gerekli'
WHEN p.pozisyon_id IS NULL THEN 'Pozisyonsuz Calisan - Duzeltme Gerekli'
ELSE 'Dolu Pozisyon'
END AS pozisyon_durumu
FROM pozisyonlar p
LEFT JOIN atamalar a ON p.pozisyon_id = a.pozisyon_id
UNION ALL
SELECT
a.pozisyon_id AS pozisyon_ref,
NULL AS pozisyon_adi,
NULL AS departman,
a.calisan_adi,
'Pozisyonsuz Calisan - Duzeltme Gerekli' AS pozisyon_durumu
FROM pozisyonlar p
RIGHT JOIN atamalar a ON p.pozisyon_id = a.pozisyon_id
WHERE p.pozisyon_id IS NULL
ORDER BY pozisyon_durumu;
Performans Optimizasyonu
FULL OUTER JOIN alternatifi olarak kullandığınız UNION sorguları büyük tablolarda performans sorununa yol açabilir. İşte bu sorunları minimize etmek için uygulayabileceğiniz teknikler:
-- İndeks oluşturma (JOIN koşulundaki sütunlar için kritik)
CREATE INDEX idx_calisanlar_dept ON calisanlar(departman_id);
CREATE INDEX idx_atamalar_pozisyon ON atamalar(pozisyon_id);
CREATE INDEX idx_odemeler_siparis ON odemeler(siparis_ref);
-- EXPLAIN ile sorgu planını inceleyin
EXPLAIN
SELECT
c.ad,
d.departman_adi
FROM calisanlar c
LEFT JOIN departmanlar d ON c.departman_id = d.id
UNION ALL
SELECT
c.ad,
d.departman_adi
FROM calisanlar c
RIGHT JOIN departmanlar d ON c.departman_id = d.id
WHERE c.id IS NULL;
Büyük veri setlerinde şunlara dikkat edin:
- JOIN sütunlarına mutlaka indeks ekleyin:
departman_id,siparis_refgibi birleştirme anahtarları indekslenmeli - WHERE koşullarını daraltın: Tarih aralığı veya durum filtresi ekleyerek sonuç setini küçültün
- LIMIT kullanın: Sayfalama yapıyorsanız her iki sorguya da uygun LIMIT/OFFSET uygulayın
- Geçici tablo yöntemi: Çok büyük ve karmaşık sorgularda ara sonuçları geçici tabloya yazıp oradan çekmek daha hızlı olabilir
Geçici Tablo ile Alternatif Yaklaşım
Çok büyük tablolarla çalışırken veya sorguyu birden fazla yerde kullanmanız gerektiğinde geçici tablo yaklaşımı daha temiz bir çözüm sunabilir:
-- Geçici tablo yöntemi
DROP TEMPORARY TABLE IF EXISTS full_join_sonuc;
CREATE TEMPORARY TABLE full_join_sonuc AS
SELECT
c.id AS calisan_id,
c.ad AS calisan_adi,
d.id AS departman_id,
d.departman_adi,
'LEFT' AS kaynak
FROM calisanlar c
LEFT JOIN departmanlar d ON c.departman_id = d.id;
INSERT INTO full_join_sonuc
SELECT
c.id AS calisan_id,
c.ad AS calisan_adi,
d.id AS departman_id,
d.departman_adi,
'RIGHT' AS kaynak
FROM calisanlar c
RIGHT JOIN departmanlar d ON c.departman_id = d.id
WHERE c.id IS NULL;
-- Geçici tablodan sorgula
SELECT
calisan_id,
calisan_adi,
departman_id,
departman_adi
FROM full_join_sonuc
ORDER BY departman_id, calisan_id;
-- İşlem sonrası temizlik
DROP TEMPORARY TABLE IF EXISTS full_join_sonuc;
Bu yaklaşımın avantajları:
- Okunabilirlik: Sorgu mantığı iki aşamaya bölündüğü için daha okunabilir
- Yeniden kullanılabilirlik: Aynı geçici tabloya birden fazla sorgu yöneltebilirsiniz
- Hata ayıklama kolaylığı: Ara sonuçları ayrı ayrı kontrol edebilirsiniz
- İndeks eklenebilirlik: Geçici tabloya da indeks ekleyebilirsiniz
Stored Procedure ile Tekrar Kullanılabilir Yapı
Aynı FULL OUTER JOIN mantığını farklı tablolar veya farklı filtrelerle sık kullanıyorsanız, bu işlemi bir stored procedure içine almak mantıklıdır:
DELIMITER //
CREATE PROCEDURE calisan_departman_raporu(
IN baslangic_tarihi DATE,
IN bitis_tarihi DATE
)
BEGIN
-- Sol taraf: Departman bilgisiyle veya departmansız tüm çalışanlar
SELECT
c.id AS calisan_id,
c.ad AS calisan_adi,
d.departman_adi,
CASE
WHEN d.id IS NULL THEN 'Departmansiz'
ELSE 'Atanmis'
END AS durum
FROM calisanlar c
LEFT JOIN departmanlar d ON c.departman_id = d.id
-- Tarih filtresi için join_tarihi sütunu olsaydı buraya eklenirdi
UNION ALL
-- Sağ taraf: Hiç çalışanı olmayan departmanlar
SELECT
NULL AS calisan_id,
NULL AS calisan_adi,
d.departman_adi,
'Bos Departman' AS durum
FROM calisanlar c
RIGHT JOIN departmanlar d ON c.departman_id = d.id
WHERE c.id IS NULL
ORDER BY durum, calisan_adi;
END //
DELIMITER ;
-- Procedure'ü çağır
CALL calisan_departman_raporu('2024-01-01', '2024-12-31');
View ile Kalıcı Çözüm
Eğer bu birleştirme mantığını kalıcı olarak kullanmak istiyorsanız bir view oluşturmak da iyi bir seçenektir:
-- FULL OUTER JOIN davranışını simüle eden view
CREATE OR REPLACE VIEW v_calisan_departman_tam AS
SELECT
c.id AS calisan_id,
c.ad AS calisan_adi,
c.departman_id AS calisan_dept_id,
d.id AS departman_id,
d.departman_adi,
CASE
WHEN c.id IS NULL THEN 'Bos Departman'
WHEN d.id IS NULL THEN 'Gecersiz Departman'
ELSE 'Eslesen Kayit'
END AS esleme_durumu
FROM calisanlar c
LEFT JOIN departmanlar d ON c.departman_id = d.id
UNION ALL
SELECT
c.id AS calisan_id,
c.ad AS calisan_adi,
c.departman_id AS calisan_dept_id,
d.id AS departman_id,
d.departman_adi,
'Bos Departman' AS esleme_durumu
FROM calisanlar c
RIGHT JOIN departmanlar d ON c.departman_id = d.id
WHERE c.id IS NULL;
-- View'ı kullan
SELECT * FROM v_calisan_departman_tam WHERE esleme_durumu != 'Eslesen Kayit';
SELECT * FROM v_calisan_departman_tam ORDER BY departman_adi;
View kullanmanın avantajları:
- Merkezi mantık: JOIN mantığı tek yerde tanımlı, her sorguda tekrarlamıyorsunuz
- Bakım kolaylığı: Mantık değiştiğinde sadece view’ı güncelliyorsunuz
- Soyutlama: Uygulama geliştiricileri JOIN karmaşıklığıyla uğraşmak zorunda kalmaz
- Yetkilendirme: View üzerine farklı yetki seviyeleri tanımlayabilirsiniz
Sık Yapılan Hatalar ve Çözümleri
MariaDB’de FULL OUTER JOIN alternatifi uygularken karşılaşılan yaygın sorunlar şunlardır:
- UNION yerine UNION ALL kullanmayı unutmak: Eğer
UNIONkullanır ve sağ taraftaWHERE sol.id IS NULLyazmayı unutursanız bazı satırlar iki kez gelir, bazıları ise hiç gelmez. Her zaman ya safUNIONkullanın, ya daUNION ALLileWHERE NULLfiltresi birleştirin.
- Sütun sırası hatası:
UNIONyapısında her SELECT aynı sayıda ve aynı türde sütun döndürmek zorundadır. Biri 5 sütun, diğeri 6 sütun döndürürse hata alırsınız.
- NULL karşılaştırma hatası:
WHERE kolon = NULLyerineWHERE kolon IS NULLyazmalısınız. MariaDB’deNULL = NULLher zamanFALSEdöner.
- Performans için filtreleri doğru yerleştirmek: Filtrelerinizi hem LEFT hem RIGHT JOIN sorgularına eklemeyi unutmayın. Sadece birine eklediğinizde yanlış sonuç alırsınız.
Sonuç
MariaDB’de FULL OUTER JOIN sözdizimini kullanamıyor olmak ilk bakışta kısıtlayıcı görünebilir, ama LEFT JOIN UNION ALL RIGHT JOIN kombinasyonu bu ihtiyacı fazlasıyla karşılar. Hatta bazı durumlarda bu yaklaşım daha esnektir: her iki sorguya bağımsız filtre uygulayabilir, COALESCE ve CASE WHEN ile sonuç kümesini zenginleştirebilirsiniz.
Özetlemek gerekirse:
- Temel yöntem:
LEFT JOIN+UNION ALL+RIGHT JOIN WHERE sol IS NULLşeklinde kullanın - Performans: JOIN sütunlarını mutlaka indeksleyin,
EXPLAINile sorgu planını kontrol edin - Bakım: Tekrar eden mantığı view veya stored procedure içine alın
- Hata kontrolü:
NULLkarşılaştırmalarındaIS NULLkullanın, sütun sayılarını eşleştirin - Büyük veri: Geçici tablo yöntemini değerlendirin
Bu teknikleri bir kez içselleştirdiğinizde, MariaDB veya MySQL üzerinde çalışırken FULL OUTER JOIN eksikliğini hiç hissetmeyeceksiniz. Üstelik sorgunuzun ne yaptığını adım adım takip edebildiğiniz için hata ayıklaması da çok daha kolay olacak.
