Veritabanı Yönetimi

    Veritabanı Trigger'ları: Faydaları ve Tuzakları

    Trigger mantığı, gerçek kullanım senaryoları ve sizi zora sokacak klasik tuzaklar.

    10 dk okuma Güncellendi: 25 Ağustos 2026

    Veritabanı trigger'ı, bir tabloya veri yazıldığında, güncellendiğinde ya da silindiğinde otomatik olarak çalışan koddur. Uygulamanız hiçbir şey bilmeden, hiçbir ek satır yazmadan bir denetim kaydı düşer, bir toplam güncellenir ya da geçersiz bir satır reddedilir. Kulağa mükemmel geliyor ve bu yüzden çok sık kötüye kullanılıyor: trigger'ın en tehlikeli özelliği, görünmez olmasıdır.

    Bir sistemde yavaşlığın kaynağını ararken EXPLAIN ANALYZE çıktısında basit bir INSERT'in 40 milisaniye sürdüğünü görüp de sebebini bulamadıysanız, sorumlusu genellikle bir trigger'dır. Bu rehberde trigger'ın nasıl çalıştığını, PostgreSQL ile MySQL arasındaki söz dizimi farklarını, gerçekten değer kattığı senaryoları ve yıllar içinde defalarca gördüğüm klasik tuzakları anlatacağım. Amaç trigger'dan kaçınmanız değil, ne zaman kullanacağınızı bilerek kullanmanız.

    Trigger Nasıl Çalışır#

    Trigger üç bileşenden oluşur: hangi tabloya bağlı olduğu, hangi olayda tetiklendiği (INSERT, UPDATE, DELETE, PostgreSQL'de ayrıca TRUNCATE) ve olayın öncesinde mi sonrasında mı çalışacağı. Bu üçlü kombinasyon davranışı tamamen değiştirir.

    BEFORE trigger'ı, satır diske yazılmadan önce çalışır ve gelen satırı değiştirebilir. NEW kaydını düzenleyip döndürürseniz, tabloya yazılan sizin döndürdüğünüz sürüm olur. NULL döndürürseniz işlem tamamen iptal edilir. AFTER trigger'ı ise satır yazıldıktan sonra çalışır; artık satırı değiştiremez ama başka tablolara yazabilir, bildirim gönderebilir.

    Bir de kapsam ayrımı var: FOR EACH ROW etkilenen her satır için ayrı ayrı çalışır, FOR EACH STATEMENT ise ifadenin tamamı için bir kez. On bin satırı güncelleyen bir UPDATE, satır bazlı trigger'da on bin kez, ifade bazlı trigger'da bir kez tetiklenir. Bu fark performans açısından hayatidir.

    TürNe zaman çalışırSatırı değiştirebilir miTipik kullanım
    BEFORE ... FOR EACH ROWYazımdan önce, satır başınaEvetNormalizasyon, doğrulama, otomatik alan doldurma
    AFTER ... FOR EACH ROWYazımdan sonra, satır başınaHayırDenetim kaydı, türetilmiş tablo güncelleme
    BEFORE ... FOR EACH STATEMENTİfadeden önce, bir kezHayırToplu işlem öncesi kontrol
    AFTER ... FOR EACH STATEMENTİfadeden sonra, bir kezHayırToplu özet yenileme, bildirim
    INSTEAD OF (yalnız view)İfade yerineEvetYazılabilir view kurgusu

    PostgreSQL'de Trigger Yazmak#

    PostgreSQL'de trigger iki parçadır: önce bir trigger fonksiyonu yazarsınız (RETURNS trigger döndüren bir fonksiyon), sonra bu fonksiyonu bir tabloya bağlarsınız. Bu ayrım aynı fonksiyonu birden fazla tabloya bağlayabilmenizi sağlar. En yaygın ve en zararsız kullanım, güncelleme zaman damgasını otomatik tutmaktır:

    CREATE OR REPLACE FUNCTION guncelleme_zamani_yaz()
    RETURNS trigger
    LANGUAGE plpgsql
    AS $$
    BEGIN
        -- NEW, tabloya yazılmak üzere olan satırı temsil eder
        NEW.guncellendi := now();
        RETURN NEW;   -- BEFORE trigger'da NEW döndürmek zorunludur
    END;
    $$;
    
    CREATE TRIGGER trg_urunler_guncelleme
    BEFORE UPDATE ON urunler
    FOR EACH ROW
    EXECUTE FUNCTION guncelleme_zamani_yaz();
    

    Aynı fonksiyonu on farklı tabloya bağlayabilirsiniz; her tablonun guncellendi sütunu olması yeterlidir. Şimdi biraz daha ciddi bir örneğe bakalım: fiyat değişikliklerini denetim tablosuna yazan bir AFTER trigger'ı.

    CREATE TABLE fiyat_gecmisi (
        id           bigserial PRIMARY KEY,
        urun_id      bigint      NOT NULL,
        eski_fiyat   numeric(12,2),
        yeni_fiyat   numeric(12,2),
        degistiren   text        NOT NULL DEFAULT current_user,
        degisim_ani  timestamptz NOT NULL DEFAULT now()
    );
    
    CREATE OR REPLACE FUNCTION fiyat_degisimini_kaydet()
    RETURNS trigger
    LANGUAGE plpgsql
    AS $$
    BEGIN
        -- Yalnızca fiyat gerçekten değiştiyse kayıt at
        IF NEW.fiyat IS DISTINCT FROM OLD.fiyat THEN
            INSERT INTO fiyat_gecmisi (urun_id, eski_fiyat, yeni_fiyat)
            VALUES (OLD.id, OLD.fiyat, NEW.fiyat);
        END IF;
        RETURN NULL;  -- AFTER trigger'da dönüş değeri yok sayılır
    END;
    $$;
    
    CREATE TRIGGER trg_fiyat_gecmisi
    AFTER UPDATE OF fiyat ON urunler
    FOR EACH ROW
    EXECUTE FUNCTION fiyat_degisimini_kaydet();
    

    İki ayrıntıya dikkat edin. AFTER UPDATE OF fiyat yazarak trigger'ı yalnızca fiyat sütunu SET listesinde geçtiğinde tetikliyoruz; başka bir sütun güncellendiğinde fonksiyon hiç çağrılmıyor. İkincisi, IS DISTINCT FROM kullanıyoruz çünkü NEW.fiyat != OLD.fiyat karşılaştırması iki taraftan biri NULL olduğunda NULL döner ve IF bloğu çalışmaz — bu, NULL içeren tablolarda sessizce kayıt kaybına yol açan klasik bir hatadır.

    PostgreSQL'de bir de WHEN koşulu vardır ve fonksiyona hiç girmeden filtreleme yapar, yani daha ucuzdur:

    CREATE TRIGGER trg_fiyat_gecmisi
    AFTER UPDATE ON urunler
    FOR EACH ROW
    WHEN (OLD.fiyat IS DISTINCT FROM NEW.fiyat)
    EXECUTE FUNCTION fiyat_degisimini_kaydet();
    

    MySQL'de Trigger Yazmak#

    MySQL'de ayrı bir fonksiyon tanımlamazsınız; trigger gövdesi doğrudan CREATE TRIGGER içinde durur. Bu daha kısa görünür ama aynı mantığı beş tabloya uygulamak istediğinizde beş kez kopyalamanız gerekir.

    DELIMITER //
    
    CREATE TRIGGER trg_fiyat_gecmisi
    AFTER UPDATE ON urunler
    FOR EACH ROW
    BEGIN
        IF NOT (NEW.fiyat <=> OLD.fiyat) THEN
            INSERT INTO fiyat_gecmisi (urun_id, eski_fiyat, yeni_fiyat, degistiren)
            VALUES (OLD.id, OLD.fiyat, NEW.fiyat, CURRENT_USER());
        END IF;
    END //
    
    DELIMITER ;
    

    Buradaki <=> operatörü MySQL'in "NULL-güvenli eşitlik" operatörüdür ve PostgreSQL'deki IS NOT DISTINCT FROM ile aynı işi yapar. MySQL'de bilmeniz gereken önemli kısıtlar var:

    KısıtMySQL davranışı
    Aynı tabloya yazmaTrigger, bağlı olduğu tabloya INSERT/UPDATE yapamaz
    Trigger içinde COMMITKullanılamaz, işlem çağıranın kontrolündedir
    Aynı olay için birden fazla triggerMySQL 5.7 ve sonrasında mümkün, sıralama FOLLOWS/PRECEDES ile
    Hata fırlatmaSIGNAL SQLSTATE '45000' ile

    Doğrulama amaçlı bir BEFORE trigger'ı MySQL'de şöyle yazılır:

    DELIMITER //
    CREATE TRIGGER trg_fiyat_dogrula
    BEFORE INSERT ON urunler
    FOR EACH ROW
    BEGIN
        IF NEW.fiyat IS NULL OR NEW.fiyat < 0 THEN
            SIGNAL SQLSTATE '45000'
            SET MESSAGE_TEXT = 'Fiyat negatif veya bos olamaz';
        END IF;
        SET NEW.ad = TRIM(NEW.ad);
    END //
    DELIMITER ;
    

    MySQL yönetiminde daha temel konularda takılıyorsanız MySQL kullanıcı yetkileri yazısı trigger oluşturmak için gereken TRIGGER ayrıcalığına da değiniyor.

    Trigger'ın Gerçekten İyi Olduğu Yerler#

    Trigger'ı savunulabilir kılan tek bir ölçüt var: uygulama katmanı bu işi güvenilir biçimde yapamıyor mu? Aşağıdaki senaryolarda cevap genellikle evettir.

    1. Denetim kaydı (audit log). Veriye kim, ne zaman, hangi değeri yazdı sorusunun cevabı yasal veya operasyonel olarak gerekiyorsa, bunu uygulamaya bırakmak risklidir. Bir betikle, bir yönetim panelinden ya da doğrudan psql ile yapılan değişiklik uygulama loguna düşmez ama trigger'a düşer.
    2. Türetilmiş sayaçların tutarlılığı. Yorum sayısı, sepet toplamı gibi alanları uygulamada güncellerseniz, bir istisna anında ana kayıt ile sayaç arasında sapma oluşur. Trigger aynı işlemin içinde çalıştığı için sapma imkânsızdır.
    3. Karmaşık bütünlük kuralları. CHECK kısıtı tek satıra bakar. "Bir kullanıcının aynı anda en fazla üç aktif aboneliği olabilir" gibi tablolar arası bir kuralı ancak trigger uygulayabilir.
    4. Yazılabilir view'ler. Bir view üzerinden INSERT almak istiyorsanız INSTEAD OF trigger'ı bunun tek yoludur. View mantığı hakkında ayrıntı için view ve materialized view farkı yazısına bakabilirsiniz.

    Denetim kaydı senaryosunda to_jsonb ile satırın tamamını saklamak, sütun ekledikçe trigger'ı değiştirmek zorunda kalmamanızı sağlar:

    CREATE OR REPLACE FUNCTION genel_denetim()
    RETURNS trigger
    LANGUAGE plpgsql
    AS $$
    BEGIN
        INSERT INTO denetim_kaydi (tablo_adi, islem, eski_satir, yeni_satir)
        VALUES (
            TG_TABLE_NAME,
            TG_OP,
            CASE WHEN TG_OP = 'INSERT' THEN NULL ELSE to_jsonb(OLD) END,
            CASE WHEN TG_OP = 'DELETE' THEN NULL ELSE to_jsonb(NEW) END
        );
        RETURN NULL;
    END;
    $$;
    

    TG_TABLE_NAME ve TG_OP, PostgreSQL'in trigger fonksiyonlarına otomatik verdiği değişkenlerdir; böylece tek fonksiyonu tüm tablolarınıza bağlayabilirsiniz. JSONB tarafını daha iyi anlamak isterseniz PostgreSQL JSONB kullanımı yazısı sorgulama ve indeksleme tarafını açıklıyor.

    Klasik Tuzaklar#

    Görünmezlik. Bu, trigger'ın en büyük problemidir ve teknik değil insani bir sorundur. Yeni bir geliştirici tabloya INSERT atar, arka planda üç tablo daha değişir ve bunu hiçbir yerde okumaz. En az iki savunma kurun: trigger isimlerini trg_ ile başlatın ve şema dosyalarınızı depoda tutun. Bir tablodaki trigger'ları listelemek de bir alışkanlık hâline gelmeli:

    -- PostgreSQL: bir tablonun trigger'ları
    SELECT tgname, pg_get_triggerdef(oid)
    FROM pg_trigger
    WHERE tgrelid = 'urunler'::regclass AND NOT tgisinternal;
    
    -- MySQL: veritabanındaki tüm trigger'lar
    SELECT TRIGGER_NAME, EVENT_MANIPULATION, ACTION_TIMING, EVENT_OBJECT_TABLE
    FROM information_schema.TRIGGERS
    WHERE TRIGGER_SCHEMA = DATABASE();
    

    Zincirleme tetiklenme. A tablosundaki trigger B'ye yazar, B'deki trigger C'ye yazar, C'deki trigger tekrar A'ya dokunur. Sonsuz döngüye girmeseniz bile bir INSERT'in maliyeti öngörülemez hâle gelir. PostgreSQL yinelemeli trigger'lara izin verir; kendinizi korumak isterseniz pg_trigger_depth() ile derinlik kontrolü koyun:

    CREATE TRIGGER trg_ozet_guncelle
    AFTER INSERT ON siparisler
    FOR EACH ROW
    WHEN (pg_trigger_depth() < 2)
    EXECUTE FUNCTION ozet_guncelle();
    

    Toplu işlerde satır bazlı trigger. Bir milyon satır yükleyeceğiniz gece işinde satır bazlı trigger, işi saatlerce uzatır. Böyle durumlarda trigger'ı geçici olarak devre dışı bırakıp yüklemenin sonunda özeti tek sorguyla hesaplamak çok daha hızlıdır:

    ALTER TABLE siparisler DISABLE TRIGGER trg_ozet_guncelle;
    -- toplu yükleme burada
    ALTER TABLE siparisler ENABLE TRIGGER trg_ozet_guncelle;
    

    Bunu yaparken devre dışı olduğu sürede kaçırdığınız güncellemeleri elle telafi etmeyi unutmayın; aksi halde özet tablonuz sessizce yanlış kalır.

    Trigger içinde harici çağrı yapmak. Trigger'ın içinden e-posta göndermek, HTTP isteği atmak ya da uzak bir sunucuya yazmak, işlemin süresini dış dünyanın hızına bağlar. Karşı taraf yanıt vermezse müşterinizin siparişi askıda kalır. Doğru desen, trigger'ın yalnızca bir kuyruk tablosuna satır yazması ve asıl işi ayrı bir işçinin yapmasıdır. PostgreSQL'de pg_notify de bu amaçla kullanılabilir.

    Sıralamayı varsaymak. Aynı olaya bağlı birden fazla trigger varsa çalışma sırası PostgreSQL'de isim alfabetik sırasına göredir, MySQL'de ise FOLLOWS/PRECEDES ile belirtilmezse tanımlanma sırasıdır. Sıraya bağımlı mantık yazmak kırılgan bir tasarımdır; mümkünse tek trigger'da birleştirin.

    Yedek ve geri yüklemede sıra sorunu. pg_dump ile alınan bir yedeği geri yüklerken veriler tabloya yazılırken trigger'lar da tetiklenebilir ve denetim tablonuz gerçek olmayan binlerce kayıtla dolabilir. pg_restore --disable-triggers seçeneği bunun içindir; yedekleme akışınızı kurarken pg_dump ile yedekleme yazısındaki geri yükleme adımlarını gözden geçirin.

    Performans Etkisini Ölçmek#

    Trigger'ın maliyetini tahmin etmeyin, ölçün. PostgreSQL'de EXPLAIN ANALYZE, trigger sürelerini ayrı bir blokta raporlar:

    EXPLAIN ANALYZE
    UPDATE urunler SET fiyat = fiyat * 1.10 WHERE kategori_id = 7;
    

    Çıktının sonunda şuna benzer satırlar görürsünüz:

    Update on urunler  (cost=0.29..812.44 rows=0 width=0) (actual time=48.113..48.114 rows=0 loops=1)
    Trigger trg_fiyat_gecmisi: time=41.902 calls=1180
    Planning Time: 0.214 ms
    Execution Time: 49.006 ms
    

    Bu çıktı her şeyi anlatıyor: 49 milisaniyelik işlemin 42 milisaniyesi trigger'da geçmiş ve trigger 1180 kez çağrılmış. Denetim tablosuna indeks eklemek, WHEN koşuluyla gereksiz çağrıları elemek ya da satır bazlı yerine ifade bazlı bir yaklaşım kurmak burada ölçülebilir kazanç sağlar. MySQL EXPLAIN çıktısında trigger süresi ayrı gösterilmez; ölçüm için performance schema'ya bakmanız ya da trigger'ı geçici kapatıp süreyi karşılaştırmanız gerekir. Genel MySQL performans ayarları için MySQL/MariaDB performans optimizasyonu yazısı iyi bir başlangıç noktası.

    Sıkça Sorulan Sorular#

    Trigger mı stored procedure mı kullanmalıyım#

    İkisi rakip değil, farklı sorulara cevap verir. Procedure siz çağırdığınızda çalışır ve akış sizin kontrolünüzdedir; trigger ise veri değiştiğinde otomatik çalışır ve çağıran taraf haberdar olmayabilir. "Her koşulda, kim yaparsa yapsın çalışmalı" diyorsanız trigger, "belli bir iş akışında ben başlatacağım" diyorsanız procedure doğru araçtır. Ayrıntı için stored procedure rehberi yazısına bakın.

    Trigger performansı ne kadar düşürür#

    Basit bir alan güncellemesi yapan trigger'ın maliyeti mikrosaniyelerle ölçülür ve çoğu iş yükünde fark edilmez. Ancak trigger başka tabloya yazıyorsa, o tablonun indeksleri ve kilitleri de hesaba katılır; satır bazlı bir trigger on bin satırlık bir güncellemede on bin kez çalışır. Gerçek maliyeti EXPLAIN ANALYZE çıktısındaki Trigger satırlarından okuyun, tahmin yürütmeyin.

    Trigger'ı geçici olarak nasıl devre dışı bırakırım#

    PostgreSQL'de ALTER TABLE tablo DISABLE TRIGGER trigger_adi komutu tek bir trigger'ı, DISABLE TRIGGER ALL ise tümünü kapatır; bunun için tablo sahibi olmanız gerekir. MySQL'de tek tek devre dışı bırakma komutu yoktur, trigger'ı silip sonra yeniden oluşturmanız gerekir; bu yüzden trigger tanımlarını mutlaka depoda saklayın. Kapalı kaldığı sürede kaçan güncellemeleri sonradan telafi etmeyi unutmayın.

    Trigger içinde hata fırlatırsam ne olur#

    Hata, trigger'ı tetikleyen ifadeyi ve varsa içinde bulunduğu işlemi geri alır. PostgreSQL'de RAISE EXCEPTION, MySQL'de SIGNAL SQLSTATE '45000' ile hata fırlatırsınız ve INSERT veya UPDATE başarısız olur. Bu, doğrulama trigger'larının çalışma prensibidir; ancak hata mesajının uygulama tarafında anlaşılır biçimde ele alındığından emin olun, aksi halde kullanıcı anlamsız bir veritabanı hatası görür.

    Trigger denetim kaydı için iyi bir çözüm mü#

    Evet, hatta çoğu senaryoda en güvenilir çözümdür; çünkü uygulama dışından yapılan değişiklikleri de yakalar. Denetim tablosunu ayrı tutun, üzerine yalnızca gerekli indeksleri koyun ve büyümesini bölümlere ayırarak (partition) yönetin. Kritik nokta, denetim tablosunun yazma maliyetinin ana işlemin süresine eklendiğini unutmamaktır; bu tabloyu gereksiz indekslerle yavaşlatmayın.

    Trigger'ları nasıl listeleyip kontrol ederim#

    PostgreSQL'de \dS+ tablo_adi komutu psql içinde tablonun trigger'larını gösterir; daha ayrıntısı için pg_trigger katalog tablosunu pg_get_triggerdef() ile sorgulayabilirsiniz. MySQL'de SHOW TRIGGERS; veya information_schema.TRIGGERS görünümü aynı bilgiyi verir. Yeni devraldığınız bir veritabanında ilk yapılacak işlerden biri, tüm trigger'ları listeleyip ne yaptıklarını okumaktır.

    Kapanış#

    Trigger, veri bütünlüğünü uygulamadan bağımsız biçimde garanti eden en güçlü araçtır; aynı zamanda sistemin en kolay unutulan parçasıdır. Aklınızda tutmanız gereken dört alışkanlık şunlar: trigger'ları isimlendirme kuralıyla görünür kılın ve tanımlarını depoda saklayın, WHEN koşuluyla gereksiz çağrıları daha fonksiyona girmeden eleyin, EXPLAIN ANALYZE çıktısındaki Trigger satırlarını düzenli okuyun ve trigger içinden asla harici bir servise çağrı yapmayın.

    Trigger yükü altında bir veritabanının davranışı, disk hızına ve tutarlı CPU erişimine doğrudan bağlıdır; denetim tabloları büyüdükçe bu daha da belirginleşir. Kendi PostgreSQL veya MySQL kurulumunuzu tam kontrolle işletmek isterseniz VDS ve bulut sunucu paketlerimiz uygun bir zemin sunar; ayar, izleme ve bakım işini devretmek isterseniz sunucu yönetimi hizmetimiz devreye girer. Denetim kayıtlarınızın da düzenli yedeklenmesi için yedekleme çözümlerimize göz atabilirsiniz.

    PostgreSQLMySQLTrigger

    Uygulamaya geçmeye hazır mısınız?

    NVMe SSD, ücretsiz SSL ve %99.9 uptime garantisiyle Clou.TR hosting ve sunucu çözümleriyle projenizi hayata geçirin.