DELETE ile Kayıt Silme ve Güvenli Kullanım

Veritabanı yönetiminde en riskli işlemlerden biri şüphesiz kayıt silmektir. Yanlış yazılmış bir DELETE sorgusu, production ortamında geri dönüşü olmayan veri kayıplarına yol açabilir. Bu yüzden DELETE komutunu doğru anlamak ve güvenli kullanım alışkanlıkları edinmek, her DBA ve backend geliştirici için kritik öneme sahiptir.

DELETE Komutunun Temel Söz Dizimi

MySQL ve MariaDB’de DELETE komutu oldukça basit görünse de ince noktaları olan güçlü bir araçtır. Temel söz dizimi şu şekildedir:

DELETE FROM tablo_adi WHERE koşul;

Buradaki en önemli unsur WHERE koşuludur. WHERE olmadan yazılan bir DELETE, tablodaki tüm kayıtları siler. Bu durum production’da felakete yol açabilir.

Basit bir örnekle başlayalım. Kullanıcılar tablosundan belirli bir kullanıcıyı silmek istiyoruz:

-- Önce kaydı görüntüleyelim
SELECT * FROM kullanicilar WHERE kullanici_id = 42;

-- Sonra silelim
DELETE FROM kullanicilar WHERE kullanici_id = 42;

Bu ikili yaklaşım, yani önce SELECT ile kontrol edip sonra DELETE çalıştırmak, temel güvenlik alışkanlıklarının başında gelir. Neyi sildiğinizi her zaman önce gözlerinizle görün.

WHERE Koşulunun Önemi ve Tehlikeli Senaryolar

WHERE koşulsuz DELETE, tablonuzu tamamen boşaltır. Peki bu ne anlama gelir?

-- TEHLİKELİ: Tüm tabloyu siler!
DELETE FROM siparisler;

-- DOĞRU: Sadece iptal edilmiş siparişleri siler
DELETE FROM siparisler WHERE durum = 'iptal';

Gerçek dünyada yaşanan bir senaryo düşünelim: Bir e-ticaret sitesinin sipariş tablosunda binlerce kayıt var. Geliştirici eski siparişleri temizlemek istiyor ama WHERE koşulunu unutuyor. İşte bu yüzden MySQL’in safe update modu hayat kurtarabilir:

-- Safe update modunu etkinleştir
SET sql_safe_updates = 1;

-- Bu sorgu artık hata verecek (anahtar kolon kullanılmıyor)
DELETE FROM siparisler WHERE durum = 'iptal';

-- ERROR 1175: You are using safe update mode...

Safe update modu etkinleştirildiğinde, PRIMARY KEY veya benzersiz indeks içermeyen DELETE sorgularını MySQL engeller. Bu modu kalıcı olarak my.cnf dosyasına ekleyebilirsiniz:

# /etc/mysql/my.cnf veya /etc/my.cnf dosyasına ekle
[mysqld]
sql_safe_updates = 1

LIMIT Kullanarak Toplu Silme İşlemlerini Kontrol Altına Alma

Milyonlarca kayıt içeren bir tabloda tek seferde tüm kayıtları silmeye çalışmak, sunucuyu kasabilir ve uzun süreli tablo kilitlenmelerine neden olabilir. Bu gibi durumlarda LIMIT kullanarak silme işlemini parçalara bölmek çok daha akıllıca bir yaklaşımdır:

-- Günlük 10.000 kayıtı sil, sunucuyu yorma
DELETE FROM log_kayitlari 
WHERE olusturma_tarihi < DATE_SUB(NOW(), INTERVAL 90 DAY) 
LIMIT 10000;

Bu sorguyu bir shell script ile döngüye alarak toplu silme işlemini kontrollü şekilde yapabilirsiniz:

#!/bin/bash
# Eski log kayıtlarını parça parça sil

DB_USER="admin"
DB_PASS="sifre123"
DB_NAME="uygulama_db"
BATCH_SIZE=10000
SLEEP_TIME=2

while true; do
    SILINEN=$(mysql -u$DB_USER -p$DB_PASS $DB_NAME -se "
        DELETE FROM log_kayitlari 
        WHERE olusturma_tarihi < DATE_SUB(NOW(), INTERVAL 90 DAY) 
        LIMIT $BATCH_SIZE;
        SELECT ROW_COUNT();
    ")
    
    echo "$(date): $SILINEN kayıt silindi"
    
    if [ "$SILINEN" -eq 0 ]; then
        echo "Tüm eski kayıtlar temizlendi, işlem tamamlandı."
        break
    fi
    
    sleep $SLEEP_TIME
done

Bu yaklaşım sayesinde silme işlemi sırasında diğer sorgular tablo kilidinden etkilenmez ve sunucu yükü kontrol altında kalır.

Transaction Kullanarak Güvenli Silme

DELETE işlemlerinde en iyi güvenlik ağı transaction kullanmaktır. Transaction içinde yapılan silme işlemini, sonucu beğenmediğinizde geri alabilirsiniz:

-- Transaction başlat
START TRANSACTION;

-- Silinecek kayıtları önce kontrol et
SELECT COUNT(*) FROM musteriler WHERE son_giris < '2020-01-01';

-- Silme işlemini gerçekleştir
DELETE FROM musteriler WHERE son_giris < '2020-01-01';

-- Etkilenen satır sayısını kontrol et
SELECT ROW_COUNT() AS silinen_kayit_sayisi;

-- Her şey beklendiği gibiyse onayla
COMMIT;

-- Yanlış bir şeyler varsa geri al
-- ROLLBACK;

Transaction kullanımı özellikle şu durumlarda hayat kurtarır:

  • Büyük toplu silme işlemlerinde
  • Birden fazla tablodan eş zamanlı silme yapıldığında
  • Silme öncesi son bir kontrol yapılmak istendiğinde
  • Ekip ortamında kod incelemesi yapılırken

Önemli bir uyarı: InnoDB tabloları transaction destekler ancak MyISAM tabloları desteklemez. Dolayısıyla bu güvenlik ağı yalnızca InnoDB’de çalışır.

JOIN ile Çoklu Tablo Silme

Gerçek dünya uygulamalarında zaman zaman birden fazla tablodan eş zamanlı kayıt silmeniz gerekir. MySQL ve MariaDB bu işlem için JOIN syntax’ını destekler:

-- Pasif kullanıcıların hem hesabını hem de sipariş kayıtlarını sil
DELETE k, s 
FROM kullanicilar k
INNER JOIN siparisler s ON k.kullanici_id = s.kullanici_id
WHERE k.hesap_durumu = 'pasif' 
AND k.son_giris < DATE_SUB(NOW(), INTERVAL 2 YEAR);

Burada k ve s takma adları ile hangi tablolardan silme yapılacağını belirtiyoruz. Bu sorgu tek seferde iki tabloyu etkiler.

Sadece ilgili tablodan silmek istiyorsanız alias belirtmeniz yeterlidir:

-- Sadece siparişleri sil, müşteri kaydı kalsın
DELETE s 
FROM siparisler s
INNER JOIN kullanicilar k ON s.kullanici_id = k.kullanici_id
WHERE k.hesap_durumu = 'pasif';

Subquery Kullanarak Koşullu Silme

Zaman zaman silme koşulunu başka bir tablodan çekmeniz gerekebilir. Bu durumlarda subquery (alt sorgu) kullanılır:

-- Ürünü stokta olmayan siparişlerin detaylarını sil
DELETE FROM siparis_detaylari
WHERE urun_id IN (
    SELECT urun_id 
    FROM urunler 
    WHERE stok_miktari = 0 AND aktif = 0
);

Burada dikkat edilmesi gereken önemli bir nokta var. MySQL’de aynı tabloyu hem silme hem de subquery’de kullanmak doğrudan mümkün değildir. Bu durumda geçici bir türetilmiş tablo kullanmanız gerekir:

-- HATA verecek: Aynı tabloyu hem sil hem subquery olarak kullanma
-- DELETE FROM urunler WHERE urun_id IN (SELECT urun_id FROM urunler WHERE stok = 0);

-- DOĞRU YAKLAŞIM: Türetilmiş tablo kullan
DELETE FROM urunler 
WHERE urun_id IN (
    SELECT urun_id FROM (
        SELECT urun_id FROM urunler WHERE stok_miktari = 0 AND aktif = 0
    ) AS gecici_tablo
);

Soft Delete: Fiziksel Silmeden Kaçınmanın Akıllı Yolu

Gerçek üretim ortamlarında fiziksel silme yapmak çoğu zaman önerilmez. Bunun yerine soft delete yani mantıksal silme yaklaşımı tercih edilir. Bu yöntemde kayıt gerçekten silinmez, sadece “silinmiş” olarak işaretlenir:

-- Tabloya silinme takip kolonları ekleme
ALTER TABLE musteriler 
ADD COLUMN silindi TINYINT(1) DEFAULT 0,
ADD COLUMN silinme_tarihi DATETIME NULL,
ADD COLUMN silen_kullanici INT NULL;

-- Soft delete işlemi
UPDATE musteriler 
SET silindi = 1, 
    silinme_tarihi = NOW(),
    silen_kullanici = 5
WHERE musteri_id = 123;

-- Aktif kayıtları getirme
SELECT * FROM musteriler WHERE silindi = 0;

-- Silinmiş kayıtları listeleme
SELECT * FROM musteriler WHERE silindi = 1;

Soft delete’in avantajları:

  • Yanlışlıkla silinen veriler kolayca geri alınabilir
  • Veri geçmişi korunur, audit trail oluşturulabilir
  • GDPR ve KVKK gibi yasal gerekliliklere uyum kolaylaşır
  • Müşteri şikayetlerinde “Neden sildim?” sorusunun cevabı bulunabilir

Dezavantajları ise tablonun zamanla şişmesi ve sorguların WHERE silindi = 0 ekleyen filtreler gerektirmesidir. Bu sorunu view ile çözebilirsiniz:

-- Sadece aktif kayıtları gösteren view oluştur
CREATE VIEW aktif_musteriler AS
SELECT * FROM musteriler WHERE silindi = 0;

-- Artık her sorguda koşul yazmak gerekmez
SELECT * FROM aktif_musteriler WHERE sehir = 'Istanbul';

TRUNCATE ile DELETE Arasındaki Farklar

Tüm tablo içeriğini silmeniz gerektiğinde TRUNCATE veya DELETE kullanabilirsiniz. Ancak aralarında kritik farklar vardır:

-- DELETE ile tümünü sil (yavaş, transaction destekler, WHERE eklenebilir)
DELETE FROM gecici_veriler;

-- TRUNCATE ile tümünü sil (çok daha hızlı, transaction desteklemez)
TRUNCATE TABLE gecici_veriler;

Farkları madde madde inceleyelim:

  • DELETE: Her satırı tek tek siler, binary log’a yazar, WHERE koşulu kullanılabilir, ROLLBACK mümkündür, AUTO_INCREMENT değerini sıfırlamaz
  • TRUNCATE: Tablo sayfalarını komple temizler, çok daha hızlıdır, WHERE koşulu kullanılamaz, ROLLBACK desteklenmez (InnoDB’de kısmi destek var), AUTO_INCREMENT değerini sıfırlar
  • DROP: Tabloyu tamamen siler, tüm yapı ve veriler gider

Büyük geçici tablolar için TRUNCATE, filtrelenmiş silme için DELETE kullanmak doğru yaklaşımdır.

Silme Öncesi Backup Alma Alışkanlığı

Production ortamında önemli bir silme işlemi öncesinde mutlaka yedek alın. Bu bir refleks haline gelmeli:

#!/bin/bash
# Silme öncesi etkilenecek kayıtları yedeğe al

DB_USER="admin"
DB_PASS="sifre123"  
DB_NAME="uygulama_db"
BACKUP_DIR="/var/backups/pre-delete"
TARIH=$(date +%Y%m%d_%H%M%S)

mkdir -p $BACKUP_DIR

# Silinecek kayıtları CSV olarak yedekle
mysql -u$DB_USER -p$DB_PASS $DB_NAME -e "
    SELECT * FROM musteriler 
    WHERE son_giris < '2021-01-01' 
    INTO OUTFILE '/tmp/silinecek_musteriler_$TARIH.csv'
    FIELDS TERMINATED BY ','
    ENCLOSED BY '"'
    LINES TERMINATED BY 'n';
"

# CSV'yi backup dizinine taşı
mv /tmp/silinecek_musteriler_$TARIH.csv $BACKUP_DIR/

echo "Yedek alındı: $BACKUP_DIR/silinecek_musteriler_$TARIH.csv"
echo "Dosya boyutu: $(du -sh $BACKUP_DIR/silinecek_musteriler_$TARIH.csv)"

# Onay iste
read -p "Silme işlemine devam edilsin mi? (evet/hayir): " ONAY

if [ "$ONAY" = "evet" ]; then
    mysql -u$DB_USER -p$DB_PASS $DB_NAME -e "
        DELETE FROM musteriler WHERE son_giris < '2021-01-01';
        SELECT ROW_COUNT() AS silinen_kayit;
    "
    echo "Silme işlemi tamamlandı."
else
    echo "Silme işlemi iptal edildi."
fi

Cascade ile İlişkili Tabloların Otomatik Temizlenmesi

Foreign key constraint ve CASCADE ON DELETE kullanarak ilişkili kayıtları otomatik olarak silebilirsiniz:

-- İlişkili tabloları CASCADE ile tanımla
CREATE TABLE siparisler (
    siparis_id INT PRIMARY KEY AUTO_INCREMENT,
    musteri_id INT NOT NULL,
    siparis_tarihi DATETIME,
    FOREIGN KEY (musteri_id) 
        REFERENCES musteriler(musteri_id) 
        ON DELETE CASCADE
);

-- Artık müşteriyi sildiğinizde siparişler de otomatik silinir
DELETE FROM musteriler WHERE musteri_id = 55;
-- Bu işlem musteriler ve siparisler tablosunu etkiler

CASCADE kullanırken dikkatli olmak gerekir çünkü zincirleme silmeler kontrol dışına çıkabilir. Üretim ortamında CASCADE yerine RESTRICT veya SET NULL tercih etmek daha güvenlidir:

-- Silme koruması: Siparişi olan müşteriyi silmeye izin verme
CREATE TABLE siparisler (
    siparis_id INT PRIMARY KEY AUTO_INCREMENT,
    musteri_id INT NOT NULL,
    FOREIGN KEY (musteri_id) 
        REFERENCES musteriler(musteri_id) 
        ON DELETE RESTRICT
);

-- Bu sorgu hata verecek: Siparişi olan müşteri silinemez
DELETE FROM musteriler WHERE musteri_id = 55;
-- ERROR 1451: Cannot delete or update a parent row

Audit Log ile Silme İşlemlerini İzleme

Kim, ne zaman, hangi kaydı sildi? Bu soruların cevabını tutmak için trigger kullanabilirsiniz:

-- Silme olaylarını kayıt altına alacak audit tablosu
CREATE TABLE audit_log (
    log_id INT PRIMARY KEY AUTO_INCREMENT,
    tablo_adi VARCHAR(100),
    islenen_kayit_id INT,
    islem_tipi VARCHAR(20),
    islem_tarihi DATETIME DEFAULT NOW(),
    kullanici VARCHAR(100) DEFAULT CURRENT_USER(),
    eski_veri TEXT
);

-- Silme trigger'ı oluştur
DELIMITER //
CREATE TRIGGER musteriler_silme_audit
BEFORE DELETE ON musteriler
FOR EACH ROW
BEGIN
    INSERT INTO audit_log (tablo_adi, islenen_kayit_id, islem_tipi, eski_veri)
    VALUES (
        'musteriler',
        OLD.musteri_id,
        'DELETE',
        CONCAT('Ad:', OLD.ad, ' Soyad:', OLD.soyad, ' Email:', OLD.email)
    );
END//
DELIMITER ;

-- Test et
DELETE FROM musteriler WHERE musteri_id = 99;

-- Audit logunu kontrol et
SELECT * FROM audit_log ORDER BY islem_tarihi DESC LIMIT 10;

Bu yaklaşım sayesinde herhangi bir kayıt yanlışlıkla silindiğinde audit_log tablosundan veriye ulaşabilirsiniz.

Pratik Güvenlik Kuralları Özeti

Yıllar içinde edindiğim deneyimlerden çıkardığım temel güvenlik kuralları:

  • Her DELETE öncesi SELECT yaz: Aynı WHERE koşuluyla önce SELECT çalıştır, kaç kayıt etkileneceğini gör
  • Transaction kullan: Büyük silme işlemlerini mutlaka transaction içine al
  • LIMIT ile başla: Büyük tablolarda önce LIMIT 1 ile dene, sonra artır
  • Production’da dikkatli ol: Local veya test ortamında deneme yap, sonra production’a taşı
  • Safe update modu aktif tut: sql_safe_updates = 1 ayarını my.cnf’e ekle
  • Yedek al: Kritik silme işlemleri öncesi etkilenecek kayıtları yedekle
  • Soft delete değerlendir: Gerçek silme yerine mantıksal silmeyi tercih et
  • İki gözle kontrol: WHERE koşulunu yazdıktan sonra bir daha oku, sondaki noktalı virgülden önce dur

Sonuç

DELETE komutu, doğru kullanıldığında veritabanı yönetiminin vazgeçilmez bir parçasıdır. Ancak dikkatsiz kullanıldığında geri dönüşü olmayan veri kayıplarına neden olabilir. WHERE koşulunu kontrol etmek, transaction kullanmak, soft delete yaklaşımını benimsemek ve audit log tutmak gibi alışkanlıklar edinmek, uzun vadede sizi büyük felaketlerden koruyacaktır.

Sysadmin ve DBA olarak en değerli becerimiz teknik bilgiden çok dikkat ve disiplindir. “Acaba emin miyim?” diye sorduğunuz her an, bir felaket senaryosunu daha önlemiş olursunuz. Production veritabanında DELETE çalıştırmadan önce bir nefes alın, sorguyu tekrar okuyun ve sonra ENTER’a basın.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir