UNION ALL ile Tekrar Eden Satırları Dahil Etme
Veritabanı sorgularında birden fazla tablodan ya da aynı tablodan farklı koşullarla veri çekmek gerektiğinde, UNION ailesinin operatörleri imdadımıza yetişir. Ancak burada kritik bir ayrım var: UNION tekrar eden satırları filtreler, UNION ALL ise tüm satırları olduğu gibi getirir, hiçbirini atmaz. Bu fark kulağa küçük gibi gelse de performans, veri bütünlüğü ve iş mantığı açısından devasa sonuçlar doğurabilir. Bu yazıda MariaDB ve MySQL ortamlarında UNION ALL operatörünü derinlemesine inceleyeceğiz, gerçek dünya senaryolarıyla nasıl kullandığımızı paylaşacağız.
UNION ve UNION ALL Arasındaki Temel Fark
UNION operatörü, iki veya daha fazla SELECT sorgusunun sonuçlarını birleştirir ve otomatik olarak yinelenen satırları kaldırır. Bu işlem arka planda bir DISTINCT operasyonu gibi çalışır ve ek işlemci gücü tüketir. UNION ALL ise böyle bir temizleme yapmaz; tüm sonuçları doğrudan döner.
Basit bir örnekle başlayalım:
-- UNION: Tekrar edenleri kaldırır
SELECT city FROM customers
UNION
SELECT city FROM suppliers;
-- UNION ALL: Tekrar edenleri dahil eder
SELECT city FROM customers
UNION ALL
SELECT city FROM suppliers;
İlk sorguda aynı şehir adı hem customers hem de suppliers tablosunda olsa bile sonuçta bir kez görünür. İkinci sorguda ise her tablodaki her satır ayrı ayrı listelenir. Eğer İstanbul her iki tabloda da varsa, UNION ALL ile sonuçta iki kez “İstanbul” görürsünüz.
Bu basit fark, pek çok sysadmin’in ve geliştiricinin gözden kaçırdığı ama production ortamlarında ciddi sorunlara yol açan bir detaydır.
Neden UNION ALL Kullanmalısınız?
Performans Açısından
UNION kullandığınızda MariaDB ve MySQL motoru, tüm sonuç setini geçici bir tabloya yazar ve ardından bu tablodaki tekrarları temizler. Bu ekstra bir sorting ya da hashing işlemi demektir. Özellikle milyonlarca satır içeren tablolarda bu fark çok belirgin olur.
UNION ALL ise bu adımı tamamen atlar. Sonuçları direkt döner. Bu yüzden veri tekrarına ihtiyaç duyduğunuzda ya da tekrarın zaten mümkün olmadığını bildiğinizde kesinlikle UNION ALL tercih etmelisiniz.
Veri Doğruluğu Açısından
Finansal raporlamada, log analizinde ya da stok takibinde aynı değere sahip iki farklı kaydı “tekrar” olarak silmek felaket olabilir. Örneğin iki farklı müşterinin aynı tutarda sipariş verdiği bir durumda UNION biri siler, UNION ALL ikisini de saklar.
Temel Sözdizimi ve Kurallar
UNION ALL kullanırken uymanız gereken birkaç kural var:
- Her
SELECTsorgusunda aynı sayıda sütun olmalıdır - Karşılık gelen sütunlar uyumlu veri tiplerine sahip olmalıdır
- Sonuç setinin sütun adları ilk SELECT sorgusundaki sütun adlarından gelir
ORDER BYyalnızca en sona, tüm sorgunun sonuna eklenir- Her bir
SELECTkendiWHERE,GROUP BY,HAVINGkoşullarını taşıyabilir
SELECT kolon1, kolon2, kolon3
FROM tablo1
WHERE kosul1
UNION ALL
SELECT kolon1, kolon2, kolon3
FROM tablo2
WHERE kosul2
ORDER BY kolon1;
Gerçek Dünya Senaryosu 1: Çoklu Log Tablolarını Birleştirme
Büyük sistemlerde logları yönetmek için partition ya da tablo bölümleme stratejileri kullanılır. Diyelim ki aylık log tablolarınız var: logs_2024_01, logs_2024_02, logs_2024_03. Belirli bir kullanıcının tüm bu aylardaki hata loglarını çekmek istiyorsunuz.
SELECT log_id, user_id, log_message, log_date, 'Ocak' AS ay
FROM logs_2024_01
WHERE user_id = 1042 AND log_level = 'ERROR'
UNION ALL
SELECT log_id, user_id, log_message, log_date, 'Subat' AS ay
FROM logs_2024_02
WHERE user_id = 1042 AND log_level = 'ERROR'
UNION ALL
SELECT log_id, user_id, log_message, log_date, 'Mart' AS ay
FROM logs_2024_03
WHERE user_id = 1042 AND log_level = 'ERROR'
ORDER BY log_date DESC;
Burada dikkat çeken nokta şu: 'Ocak', 'Subat', 'Mart' gibi sabit string değerleri ekleyerek hangi tablodan geldiğini takip edebiliyoruz. Eğer UNION kullansaydık ve iki farklı ayda tamamen aynı içerikli iki hata kaydı olsaydı, birini kaybedebilirdik. UNION ALL ile her log kaydı korunur.
Gerçek Dünya Senaryosu 2: Satış ve İade Raporlaması
E-ticaret sistemlerinde satışları ve iadeleri ayrı tablolarda tutmak yaygın bir pratiktir. Dönemsel raporlama için iki tabloyu birleştirmeniz gerekebilir:
SELECT
siparis_no,
musteri_id,
urun_id,
miktar,
tutar,
'SATIS' AS islem_tipi,
islem_tarihi
FROM satislar
WHERE islem_tarihi BETWEEN '2024-01-01' AND '2024-03-31'
UNION ALL
SELECT
iade_no AS siparis_no,
musteri_id,
urun_id,
miktar,
tutar * -1 AS tutar,
'IADE' AS islem_tipi,
islem_tarihi
FROM iadeler
WHERE islem_tarihi BETWEEN '2024-01-01' AND '2024-03-31'
ORDER BY musteri_id, islem_tarihi;
Bu sorguda iade tutarını negatif yaparak finansal raporlamada doğrudan toplanabilir bir veri seti oluşturduk. Aynı müşterinin birden fazla satış ya da iadesi varsa hepsi ayrı satırlar olarak gelir; bu tam istediğimiz davranış. UNION kullansaydık, aynı tutarda iki farklı iade kaybolabilirdi.
Gerçek Dünya Senaryosu 3: Arşiv ve Aktif Veri Tabloları
Performans için eski verileri arşiv tablolarına taşımak yaygın bir yöntemdir. Hem aktif hem arşiv tablodan veri çekerken UNION ALL idealdir:
SELECT
siparis_id,
musteri_adi,
toplam_tutar,
siparis_tarihi,
durum,
'AKTIF' AS kaynak
FROM siparisler
WHERE musteri_id = 5678
UNION ALL
SELECT
siparis_id,
musteri_adi,
toplam_tutar,
siparis_tarihi,
durum,
'ARSIV' AS kaynak
FROM siparisler_arsiv
WHERE musteri_id = 5678
ORDER BY siparis_tarihi DESC
LIMIT 50;
Bu yapı, müşteri hizmetleri ekibinin hem eski hem yeni siparişlere tek sorguda erişmesini sağlar. LIMIT tüm sorgunun sonuna eklenir ve birleşik sonuç üzerinde çalışır.
UNION ALL ile GROUP BY Kombinasyonu
UNION ALL sorgusunu bir alt sorgu olarak kullanıp üzerine GROUP BY uygulayabilirsiniz. Bu teknik özellikle raporlama sorgularında çok kullanışlıdır:
SELECT
urun_kategorisi,
SUM(toplam_satis) AS genel_toplam,
COUNT(*) AS islem_sayisi
FROM (
SELECT urun_kategorisi, satis_tutari AS toplam_satis
FROM satis_q1_2024
UNION ALL
SELECT urun_kategorisi, satis_tutari AS toplam_satis
FROM satis_q2_2024
UNION ALL
SELECT urun_kategorisi, satis_tutari AS toplam_satis
FROM satis_q3_2024
) AS yillik_satislar
GROUP BY urun_kategorisi
ORDER BY genel_toplam DESC;
Buradaki mantık şu: Her çeyrek tablosundan tüm satırları alıyoruz (UNION ALL ile tekrar edenleri de dahil ediyoruz), ardından dışarıdaki sorgu kategori bazında toplayarak raporumuzu oluşturuyor. Eğer UNION kullansaydık, aynı kategoride aynı tutarda iki farklı satış birleşirdi ve yanlış toplam elde ederdik.
UNION ALL ile Veri Kalitesi Kontrolü
UNION ALL ilginç bir kullanım alanı daha sunar: iki tablo arasındaki farkları bulmak. Bu teknik özellikle veri migration sonrası doğrulama için harika çalışır:
SELECT urun_id, COUNT(*) AS adet
FROM (
SELECT urun_id FROM envanter_yeni
UNION ALL
SELECT urun_id FROM envanter_eski
) AS birlesik
GROUP BY urun_id
HAVING COUNT(*) = 1;
Bu sorgu, yalnızca bir tabloda bulunan urun_id değerlerini döner. COUNT(*) = 1 olan kayıtlar ya yalnızca yeni tabloda ya da yalnızca eski tabloda var demektir. Bu şekilde iki tablo arasındaki tutarsızlıkları kolayca tespit edebilirsiniz.
Hangi tabloda olduğunu da görmek isterseniz:
SELECT urun_id, COUNT(*) AS adet, kaynak
FROM (
SELECT urun_id, 'YENI' AS kaynak FROM envanter_yeni
UNION ALL
SELECT urun_id, 'ESKI' AS kaynak FROM envanter_eski
) AS birlesik
GROUP BY urun_id, kaynak
ORDER BY urun_id;
Performans İpuçları ve Dikkat Edilmesi Gerekenler
Index Kullanımı
UNION ALL sorguları her bir SELECT için bağımsız index kullanabilir. Bu büyük avantajdır. Ancak dış sorgunun ORDER BY veya GROUP BY için index kullanamayacağını aklınızda bulundurun.
-- Her bir alt sorgu kendi indexini kullanır
EXPLAIN
SELECT siparis_id, tutar FROM siparisler WHERE musteri_id = 100
UNION ALL
SELECT siparis_id, tutar FROM siparisler_arsiv WHERE musteri_id = 100;
EXPLAIN çıktısını inceleyerek her alt sorgunun index kullanıp kullanmadığını kontrol edin. Eğer ALL ya da index tipi görüyorsanız, ilgili sütunlar üzerinde index oluşturmayı düşünün.
Geçici Tablo Kullanımı
Çok fazla sayıda alt sorgu içeren UNION ALL sorgularında MariaDB geçici tablo oluşturabilir. Bu durumu EXPLAIN çıktısındaki Using temporary ifadesiyle anlayabilirsiniz. Gerekirse SQL_BIG_RESULT hint’ini kullanarak optimizasyona yardımcı olabilirsiniz:
SELECT SQL_BIG_RESULT urun_id, SUM(miktar)
FROM (
SELECT urun_id, miktar FROM depo_a
UNION ALL
SELECT urun_id, miktar FROM depo_b
UNION ALL
SELECT urun_id, miktar FROM depo_c
) AS toplam_stok
GROUP BY urun_id;
Sütun Tipi Uyumsuzlukları
MariaDB ve MySQL, UNION ALL sorgularında sütun tiplerini otomatik olarak cast eder. Ama bu her zaman beklediğiniz sonucu vermeyebilir:
-- Potansiyel tip uyumsuzlugu
SELECT siparis_id, '2024' AS yil FROM siparisler_2024
UNION ALL
SELECT siparis_id, yil FROM siparisler_arsiv; -- yil burada INT olabilir
-- Daha guvenli yaklasim: Explicit cast
SELECT siparis_id, CAST('2024' AS CHAR) AS yil FROM siparisler_2024
UNION ALL
SELECT siparis_id, CAST(yil AS CHAR) AS yil FROM siparisler_arsiv;
Stored Procedure ile UNION ALL Dinamik Kullanım
Tablo sayısı dinamik olduğunda stored procedure içinde UNION ALL sorgusunu string olarak oluşturup execute etmek gerekebilir:
DELIMITER //
CREATE PROCEDURE GetAllLogs(IN p_user_id INT, IN p_start_date DATE, IN p_end_date DATE)
BEGIN
DECLARE v_sql TEXT DEFAULT '';
DECLARE v_table VARCHAR(50);
DECLARE v_first BOOLEAN DEFAULT TRUE;
DECLARE cur CURSOR FOR
SELECT table_name
FROM information_schema.tables
WHERE table_schema = DATABASE()
AND table_name LIKE 'logs_%'
ORDER BY table_name;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET @done = 1;
SET @done = 0;
OPEN cur;
read_loop: LOOP
FETCH cur INTO v_table;
IF @done = 1 THEN
LEAVE read_loop;
END IF;
IF v_first THEN
SET v_sql = CONCAT(v_sql,
'SELECT log_id, user_id, log_message, log_date FROM ', v_table,
' WHERE user_id = ', p_user_id,
' AND log_date BETWEEN ''', p_start_date, ''' AND ''', p_end_date, '''');
SET v_first = FALSE;
ELSE
SET v_sql = CONCAT(v_sql,
' UNION ALL SELECT log_id, user_id, log_message, log_date FROM ', v_table,
' WHERE user_id = ', p_user_id,
' AND log_date BETWEEN ''', p_start_date, ''' AND ''', p_end_date, '''');
END IF;
END LOOP;
CLOSE cur;
SET v_sql = CONCAT(v_sql, ' ORDER BY log_date DESC');
SET @dynamic_sql = v_sql;
PREPARE stmt FROM @dynamic_sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
END //
DELIMITER ;
Bu stored procedure, veritabanındaki logs_ ile başlayan tüm tabloları otomatik olarak bulur ve UNION ALL ile birleştirir. Yeni ay tabloları eklendiğinde prosedürü güncellemenize gerek kalmaz.
Sık Yapılan Hatalar
- ORDER BY’ı alt sorgulara koymak: Her alt sorguda
ORDER BYkullanmak hataya ya da anlamsız sonuçlara yol açar, sadece en sona ekleyin - Sütun sayısı uyuşmazlığı: Her
SELECTaynı sayıda sütun döndürmek zorundadır, eksik sütunlar içinNULLya da sabit değer kullanın - NULL sütunları görmezden gelmek: Bir tabloda olmayan ama diğerinde olan bir sütun için
NULL AS kolon_adikullanın - UNION ALL yerine UNION kullanmak: Tekrarların anlam taşıdığı finansal ve log sorgularında
UNIONveri kaybına neden olur
-- Yanlis: Farkli sayida sutun
SELECT id, ad, soyad FROM tablo1
UNION ALL
SELECT id, ad FROM tablo2; -- HATA!
-- Dogru
SELECT id, ad, soyad FROM tablo1
UNION ALL
SELECT id, ad, NULL AS soyad FROM tablo2;
Sonuç
UNION ALL, doğru kullanıldığında hem performans hem de veri bütünlüğü açısından UNION‘dan çok daha güçlü bir araçtır. Özellikle log yönetimi, finansal raporlama, arşiv-aktif veri birleştirme ve veri migration doğrulama gibi senaryolarda vazgeçilmez bir sorgu aracı haline gelir.
Özetlemek gerekirse:
- Tekrarların önemli olduğu her durumda
UNION ALLkullanın - İki tablonun kesişimini bulmak için değil, tüm veriyi toplamak için kullanın
- Performans kritikse
UNIONyerineUNION ALLtercih edin veEXPLAINile doğrulayın - Çok sayıda tabloyu birleştiriyorsanız stored procedure ile dinamik SQL yazmayı düşünün
- Her alt sorgunun aynı sayıda ve uyumlu tipte sütun döndürdüğünden emin olun
Bu operatörü yerinde ve doğru kullanmak, sorgu performansınızı artırır ve beklenmedik veri kayıplarının önüne geçer. Production ortamlarında bir değişiklik yapmadan önce her zaman test ortamında EXPLAIN ile sorgu planını incelemeyi alışkanlık haline getirin.
