MySQL ve MariaDB’de NOT IN ile Belirli Değerleri Dışlama
Veritabanı sorgularında bazen “şunu bul” demek yerine “şunu hariç tut” demek çok daha pratik olur. Özellikle belirli kategorileri, durumları veya değerleri dışlamak istediğinizde NOT IN operatörü tam ihtiyacınız olan araçtır. Bu yazıda MariaDB ve MySQL ortamlarında NOT IN kullanımını gerçek dünya senaryolarıyla detaylıca ele alacağız.
NOT IN Nedir ve Ne Zaman Kullanılır?
NOT IN, SQL sorgularında bir sütundaki değerin belirtilen liste içinde yer almadığını kontrol eden bir operatördür. Mantıksal olarak IN operatörünün tam tersidir. Bir kayıt kümesini filtrelerken “şu değerler dışındaki her şeyi getir” demek istediğinizde bu operatör devreye girer.
Günlük sysadmin hayatında şu senaryolarla sıkça karşılaşırsınız:
- Belirli durumda olan siparişleri dışlamak (iptal edilmiş, beklemedeki)
- Test veya sistem kullanıcılarını raporlardan hariç tutmak
- Belirli sunucu gruplarını izleme sorgularından çıkarmak
- Bakım modundaki hizmetleri aktif listeden göstermemek
- Arşivlenmiş ya da silinmiş işaretli kayıtları analiz dışı bırakmak
Temel sözdizimi oldukça sade:
SELECT sutun1, sutun2
FROM tablo
WHERE sutun NOT IN (deger1, deger2, deger3);
Şimdi bunu pratikte nasıl kullandığımıza bakalım.
Temel NOT IN Kullanımı
Önce basit bir örnekle başlayalım. Diyelim ki bir e-ticaret platformunu yönetiyorsunuz ve sipariş tablonuzda farklı durumlar var: pending, processing, shipped, delivered, cancelled, refunded. Sadece aktif süreçteki siparişleri görmek istiyorsunuz:
SELECT order_id, customer_id, total_amount, status, created_at
FROM orders
WHERE status NOT IN ('cancelled', 'refunded', 'returned')
ORDER BY created_at DESC;
Bu sorgu iptal edilmiş, iade edilmiş ve geri gönderilmiş siparişleri bir kenara bırakarak sadece işlem görmekte olan kayıtları döndürür. IN ile yazmak isteseniz tüm geçerli durumları tek tek yazmanız gerekirdi; bu yüzden dışlamak daha kısa ve okunabilir bir yoldur.
Sayısal değerlerle de aynı şekilde çalışır:
SELECT user_id, username, email, role_id
FROM users
WHERE role_id NOT IN (1, 2, 99)
ORDER BY username;
Burada 1 numaralı süper admin, 2 numaralı sistem hesabı ve 99 numaralı test rolüne sahip kullanıcılar hariç tutulmuştur.
Alt Sorgu (Subquery) ile NOT IN
NOT IN operatörünün gerçek gücü alt sorgularla birlikte kullanıldığında ortaya çıkar. Başka bir tablodaki değerlere göre filtreleme yapabilirsiniz.
Örnek senaryo: Bir sunucu yönetim sisteminde hangi sunucuların hiç bakım planına dahil edilmediğini bulmak istiyorsunuz:
SELECT server_id, hostname, ip_address, datacenter
FROM servers
WHERE server_id NOT IN (
SELECT DISTINCT server_id
FROM maintenance_schedules
WHERE schedule_date >= CURDATE()
)
AND status = 'active'
ORDER BY hostname;
Bu sorgu, gelecekte herhangi bir bakım planı olmayan aktif sunucuları listeler. Böyle bir listeyi haftalık raporlarda kullanarak hangi sunucuların bakım planlamasından kaçtığını kolayca tespit edebilirsiniz.
Bir başka senaryo: Hangi müşterilerin son 6 ayda hiç sipariş vermediğini bulmak:
SELECT c.customer_id, c.full_name, c.email, c.registration_date
FROM customers c
WHERE c.customer_id NOT IN (
SELECT DISTINCT customer_id
FROM orders
WHERE order_date >= DATE_SUB(NOW(), INTERVAL 6 MONTH)
AND status NOT IN ('cancelled', 'refunded')
)
AND c.is_active = 1
ORDER BY c.registration_date ASC;
Bu sorguyu bir cron job ile aylık çalıştırıp sonuçları pazarlama ekibine gönderebilirsiniz. Aktif ama alışveriş yapmayan müşterileri tespit etmek için biçilmiş kaftandır.
NULL Değerleriyle NOT IN Kullanırken Dikkat
Bu konu birçok deneyimli DBA’nın bile zaman zaman hata yaptığı bir noktadır. NOT IN listesinde ya da alt sorguda NULL değeri varsa beklenmedik sonuçlar alırsınız.
Şu örneği inceleyin:
-- Bu sorgu hiç sonuç döndürmeyebilir!
SELECT product_id, product_name
FROM products
WHERE category_id NOT IN (
SELECT category_id
FROM categories
WHERE is_archived = 1
);
Eğer categories tablosundaki category_id sütununda herhangi bir NULL değer varsa, bu sorgu boş sonuç döndürür. Bunun nedeni SQL’in NULL ile karşılaştırma yapma biçimidir; NULL ile yapılan her karşılaştırma UNKNOWN döner ve bu da NOT IN mantığını tamamen çökertir.
Çözüm olarak IS NOT NULL filtresini alt sorguya ekleyin:
SELECT product_id, product_name, price, stock_quantity
FROM products
WHERE category_id NOT IN (
SELECT category_id
FROM categories
WHERE is_archived = 1
AND category_id IS NOT NULL
)
ORDER BY product_name;
Bu küçük ekleme büyük farklılık yaratır. Üretim ortamında bu tuzağa düşmek saatlerinizi alabilir, bu yüzden alt sorgu içeren NOT IN kullanımlarında her zaman NULL kontrolü yapma alışkanlığı edinin.
Performans Konuları ve NOT IN vs NOT EXISTS
NOT IN operatörü kullanışlı olsa da büyük veri setlerinde performans sorunlarına yol açabilir. Özellikle alt sorguyla birlikte kullanıldığında bu durum belirginleşir.
Kural olarak şunu aklınızda tutun: Alt sorgu küçükse NOT IN gayet iyi çalışır. Ama alt sorgu büyük bir tabloyu tarıyorsa NOT EXISTS genellikle daha iyi performans gösterir.
NOT IN ile yazılmış sorgu:
SELECT e.employee_id, e.full_name, e.department, e.salary
FROM employees e
WHERE e.employee_id NOT IN (
SELECT employee_id
FROM performance_reviews
WHERE review_year = YEAR(CURDATE())
);
Aynı sonucu NOT EXISTS ile daha verimli şekilde yazabilirsiniz:
SELECT e.employee_id, e.full_name, e.department, e.salary
FROM employees e
WHERE NOT EXISTS (
SELECT 1
FROM performance_reviews pr
WHERE pr.employee_id = e.employee_id
AND pr.review_year = YEAR(CURDATE())
);
NOT EXISTS büyük tablolarda genellikle daha hızlıdır çünkü eşleşen ilk kaydı bulduğunda durur. NOT IN ise tüm listeyi işlemek zorunda kalabilir.
Bununla birlikte hangi yöntemin daha iyi performans gösterdiğini anlamak için EXPLAIN kullanmak her zaman en doğru yaklaşımdır:
EXPLAIN SELECT e.employee_id, e.full_name, e.department
FROM employees e
WHERE e.employee_id NOT IN (
SELECT employee_id
FROM performance_reviews
WHERE review_year = 2024
);
EXPLAIN çıktısındaki type, rows ve Extra kolonlarına bakarak hangi sorgunun daha verimli çalıştığını görebilirsiniz.
Çoklu Koşullarla NOT IN Kullanımı
Gerçek dünya senaryolarında NOT IN genellikle diğer filtrelerle birlikte kullanılır. Bir log yönetim sisteminde belirli sunucuları ve belirli log seviyelerini dışlamak istediğinizi düşünün:
SELECT log_id, server_name, log_level, message, created_at
FROM system_logs
WHERE log_level NOT IN ('DEBUG', 'TRACE', 'VERBOSE')
AND server_name NOT IN ('dev-server-01', 'test-server-02', 'staging-db-01')
AND created_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR)
ORDER BY created_at DESC
LIMIT 1000;
Bu sorgu geliştirme ve test sunucularını, düşük öncelikli log seviyelerini dışarıda bırakarak son 24 saatin kritik loglarını getirir. Bir monitoring dashboard için mükemmel bir temel sorgudur.
Bir başka senaryo: Bir hosting panelinde belirli paket türleri dışındaki aktif hesapları listelemek:
SELECT a.account_id, a.domain_name, a.username,
a.disk_usage_mb, a.bandwidth_gb, a.created_date
FROM hosting_accounts a
WHERE a.package_type NOT IN ('trial', 'suspended_trial', 'legacy_basic', 'discontinued')
AND a.account_status NOT IN ('suspended', 'terminated', 'fraud')
AND a.reseller_id NOT IN (
SELECT reseller_id
FROM resellers
WHERE reseller_status = 'inactive'
)
ORDER BY a.disk_usage_mb DESC;
Bu tür sorgular panel başlangıç sayfasındaki özet istatistikler için kullanılabilir.
Güncelleme ve Silme İşlemlerinde NOT IN
NOT IN sadece SELECT sorgularıyla sınırlı değildir. UPDATE ve DELETE işlemlerinde de son derece faydalıdır.
Belirli roller dışındaki tüm kullanıcıların şifre sıfırlama işaretini kaldırmak:
UPDATE users
SET force_password_reset = 0, updated_at = NOW()
WHERE force_password_reset = 1
AND role NOT IN ('super_admin', 'system', 'api_user')
AND last_login < DATE_SUB(NOW(), INTERVAL 90 DAY);
Eski log kayıtlarını temizlerken bazı kritik log türlerini korumak:
DELETE FROM application_logs
WHERE created_at < DATE_SUB(NOW(), INTERVAL 30 DAY)
AND log_type NOT IN ('security_breach', 'data_corruption', 'critical_error', 'audit')
AND server_env NOT IN ('production', 'dr-production');
Bu DELETE sorgusunu çalıştırmadan önce mutlaka önce SELECT ile kontrol edin:
-- Önce kaç kayıt silineceğini kontrol et
SELECT COUNT(*) as silinecek_kayit_sayisi
FROM application_logs
WHERE created_at < DATE_SUB(NOW(), INTERVAL 30 DAY)
AND log_type NOT IN ('security_breach', 'data_corruption', 'critical_error', 'audit')
AND server_env NOT IN ('production', 'dr-production');
Sayıyı onayladıktan sonra silme işlemine geçin. Üretim veritabanında geri dönülemez bir DELETE işlemi yapmak için bu önlem şarttır.
Dinamik Değer Listeleriyle NOT IN
Bazen dışlamak istediğiniz değerler statik değil, başka bir tablodan dinamik olarak gelir. Aşağıdaki örnek, bir IP kara listesi tablosunu kullanan bir sorguyu gösteriyor:
SELECT r.request_id, r.ip_address, r.endpoint, r.response_code, r.request_time
FROM api_requests r
WHERE r.ip_address NOT IN (
SELECT ip_address
FROM ip_blacklist
WHERE is_active = 1
AND (expires_at IS NULL OR expires_at > NOW())
AND ip_address IS NOT NULL
)
AND r.request_time >= DATE_SUB(NOW(), INTERVAL 1 HOUR)
ORDER BY r.request_time DESC;
Bu sorgu anlık API isteklerini izlerken kara listedeki IP adreslerini otomatik olarak dışarıda bırakır. expires_at kontrolü ile süresi dolmuş kara liste kayıtlarının sorguyu etkilememesi de sağlanmış olur.
Uygulama Katmanından NOT IN Parametrelerini Güvenli Geçirme
Bir PHP veya Python uygulamasından dinamik değerler geliyorsa NOT IN listesini güvenli şekilde oluşturmanız gerekir. SQL injection açığına davetiye çıkarmamak için hazırlıklı sorgular (prepared statements) kullanın.
MySQL komut satırından test edebileceğiniz örnek bir sorgu yapısı:
-- Uygulama katmanında dinamik olarak oluşturulan örnek
-- PHP PDO ile kullanım simülasyonu olarak MySQL CLI'da test:
SET @excluded_statuses = 'cancelled,refunded,fraud';
SELECT order_id, customer_id, total_amount, status
FROM orders
WHERE FIND_IN_SET(status, @excluded_statuses) = 0
AND order_date >= DATE_SUB(NOW(), INTERVAL 7 DAY);
Ancak FIND_IN_SET yaklaşımı index kullanımını zorlaştırır. Uygulama katmanında doğru yol, parametreli sorgular oluşturmaktır. Bunu MySQL prosedürü olarak da yazabilirsiniz:
DELIMITER $$
CREATE PROCEDURE GetActiveOrdersByExclusion(
IN p_excluded_statuses TEXT
)
BEGIN
-- Dinamik sorgu oluştur
SET @query = CONCAT(
'SELECT order_id, customer_id, total_amount, status, created_at ',
'FROM orders ',
'WHERE status NOT IN (', p_excluded_statuses, ') ',
'AND created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY) ',
'ORDER BY created_at DESC'
);
PREPARE stmt FROM @query;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
END$$
DELIMITER ;
-- Kullanımı:
CALL GetActiveOrdersByExclusion("'cancelled', 'refunded', 'pending'");
İndeks Kullanımı ve NOT IN Optimizasyonu
NOT IN sorgularının performanslı çalışması için ilgili sütunlarda indeks olması kritik önem taşır. Bir sorguyu optimize etmek için şu adımları izleyin:
-- Önce mevcut indeksleri kontrol et
SHOW INDEX FROM orders;
-- NOT IN ile kullanılan sütuna indeks ekle
ALTER TABLE orders ADD INDEX idx_status (status);
-- Bileşik sorgu için bileşik indeks daha iyi olabilir
ALTER TABLE orders ADD INDEX idx_status_date (status, created_at);
-- Sorgunun indeksi kullanıp kullanmadığını doğrula
EXPLAIN SELECT order_id, total_amount
FROM orders
WHERE status NOT IN ('cancelled', 'refunded')
AND created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY);
EXPLAIN çıktısında type kolonunun ALL (tam tablo taraması) yerine range veya ref göstermesi beklenir. ALL görüyorsanız indeks eklemeyi ya da sorguyu yeniden yazmayı düşünün.
Büyük tablolarda NOT IN performansını artırmak için bazı pratik ipuçları:
- Alt sorgu sonuç seti çok büyükse
NOT EXISTSveyaLEFT JOIN ... IS NULLkullanın NOT INlistesindeki sabit değerlerin sayısını makul tutun (100’den fazla değer için farklı yaklaşım düşünün)- Filtreleme sütunlarında uygun indeksler olduğundan emin olun
EXPLAIN ANALYZE(MariaDB 10.9+ ve MySQL 8.0+) ile gerçek çalışma sürelerini ölçün
LEFT JOIN ile NOT IN Alternatifi
Bazı durumlarda LEFT JOIN ... IS NULL kombinasyonu NOT IN‘den daha iyi performans verebilir ve aynı sonucu üretir:
-- NOT IN ile yazılmış versiyon
SELECT s.server_id, s.hostname, s.ip_address
FROM servers s
WHERE s.server_id NOT IN (
SELECT server_id FROM monitoring_agents WHERE is_active = 1
);
-- LEFT JOIN ile eşdeğer ve genellikle daha hızlı versiyon
SELECT s.server_id, s.hostname, s.ip_address
FROM servers s
LEFT JOIN monitoring_agents ma
ON s.server_id = ma.server_id AND ma.is_active = 1
WHERE ma.server_id IS NULL
ORDER BY s.hostname;
Bu iki sorgu aynı sonucu üretir; monitoring agent’ı olmayan sunucuları listeler. LEFT JOIN versiyonu büyük tablolarda genellikle daha iyi EXPLAIN planı üretir.
Sonuç
NOT IN operatörü, MariaDB ve MySQL sorgularında dışlama mantığı kurmanın en okunabilir ve doğrudan yollarından biridir. Basit değer listelerinden karmaşık alt sorgu kombinasyonlarına kadar geniş bir kullanım alanı sunar.
Günlük sysadmin ve DBA çalışmalarında aklınızda bulundurmanız gereken temel noktalar şunlardır:
- NULL tuzağına dikkat edin: Alt sorgularda
IS NOT NULLkontrolü yapmayı unutmayın, aksi halde boş sonuç almanız kaçınılmazdır. - Performansı ölçün:
EXPLAINveEXPLAIN ANALYZEkomutlarını alışkanlık haline getirin. - Büyük setlerde alternatif değerlendirin: Alt sorgu çok satır dönüyorsa
NOT EXISTSveyaLEFT JOIN ... IS NULLdaha iyi performans verebilir. - İndeksleri kontrol edin: Filtreleme yaptığınız sütunlarda uygun indeksler olduğundan emin olun.
- Üretimde test edin: Silme ve güncelleme sorgularından önce mutlaka
SELECT COUNT(*)ile kaç kaydın etkileneceğini doğrulayın.
NOT IN doğru kullanıldığında sorgularınızı hem daha kısa hem de daha anlaşılır kılar. Özellikle iş mantığının “şu değerleri dışla” şeklinde ifade edildiği durumlarda, bu operatörü doğrudan kullanmak kodun okunabilirliğini ciddi ölçüde artırır.
