SQLite'ta tetikleyici (trigger), bir tabloda INSERT, UPDATE veya DELETE gerçekleştiğinde kendiliğinden çalışan SQL ifadeleri grubudur ve CREATE TRIGGER ile tanımlanır. Tetikleyici değişiklikten önce (BEFORE) ya da sonra (AFTER) çalışabilir; görünümler üzerinde ise işlemin yerine geçen INSTEAD OF biçimi kullanılır. Gövdede değişen satırın eski değerlerine OLD, yeni değerlerine NEW ile erişilir, WHEN ile çalışma koşulu konur ve RAISE() ile işlem hata vererek durdurulabilir.
Bu yazıda değişiklik günlüğü, stok denetimi ve sürüm sayacı gibi gerçekçi örneklerle tetikleyicilerin nasıl çalıştığını, RAISE() seçenekleri arasındaki farkları, özyinelemeli tetikleyicileri, gövdede izin verilmeyen söz dizimlerini ve sık yapılan hataları anlatıyoruz. Serinin tamamı için SQLite öğrenme rehberine, görünümleri yazılabilir yapan INSTEAD OF tetikleyicileri için SQLite CREATE VIEW yazısına bakabilirsiniz.
Tetikleyicinin parçaları

Bir tetikleyici tanımı şu kararları içerir: ne zaman çalışacağı (BEFORE, AFTER veya INSTEAD OF), hangi olayda çalışacağı (INSERT, UPDATE, UPDATE OF sütun listesi veya DELETE), hangi tabloya bağlı olduğu, isteğe bağlı bir WHEN koşulu ve BEGIN ile END arasındaki ifadeler. Zaman belirtilmezse varsayılan BEFORE'dur. SQLite yalnızca satır düzeyinde tetikleyicileri destekler: değişen her satır için gövde bir kez çalışır. Standart SQL'deki FOR EACH STATEMENT biçimi yoktur; bu yüzden FOR EACH ROW yazmak isteğe bağlıdır ve yazsanız da yazmasanız da davranış aynıdır.
OLD ve NEW yalnızca anlamlı oldukları olaylarda kullanılabilir:
| Olay | NEW | OLD |
|---|---|---|
| INSERT | Eklenen satırın değerleri | Kullanılamaz |
| UPDATE | Güncelleme sonrası değerler | Güncelleme öncesi değerler |
| DELETE | Kullanılamaz | Silinen satırın değerleri |
Gövdede UPDATE, INSERT, DELETE ve SELECT ifadeleri yazılabilir. SELECT ifadesinin sonucu bir yere gönderilmez; bu biçim genellikle RAISE() fonksiyonunu çağırmak ya da yan etkisi olan bir fonksiyonu çalıştırmak için kullanılır. Bağlı olduğu tablo silindiğinde tetikleyici de kendiliğinden silinir; ancak gövdede adı geçen başka tablolar silinirse tetikleyici yerinde kalır ve bir sonraki çalışmasında hata verir.
AFTER tetikleyiciyle değişiklik günlüğü
Tetikleyicilerin en yaygın kullanımı, bir tablodaki değişiklikleri ayrı bir günlük tablosuna yazmaktır. Aşağıdaki örnekte hesap bakiyesi her değiştiğinde eski ve yeni değer kaydediliyor, silinen hesaplar da ayrıca işleniyor:
CREATE TABLE hesap(id INTEGER PRIMARY KEY, sahip TEXT NOT NULL, bakiye INTEGER NOT NULL DEFAULT 0);
CREATE TABLE hesap_log(
log_id INTEGER PRIMARY KEY,
hesap_id INTEGER, islem TEXT, eski INTEGER, yeni INTEGER
);
CREATE TRIGGER hesap_bakiye_log AFTER UPDATE OF bakiye ON hesap
WHEN OLD.bakiye <> NEW.bakiye
BEGIN
INSERT INTO hesap_log(hesap_id, islem, eski, yeni)
VALUES (NEW.id, 'guncelleme', OLD.bakiye, NEW.bakiye);
END;
CREATE TRIGGER hesap_sil_log AFTER DELETE ON hesap
BEGIN
INSERT INTO hesap_log(hesap_id, islem, eski, yeni)
VALUES (OLD.id, 'silme', OLD.bakiye, NULL);
END;
INSERT INTO hesap(sahip, bakiye) VALUES ('Ayşe', 500), ('Mehmet', 200);
UPDATE hesap SET bakiye = bakiye + 100; -- iki satır, iki kayıt
UPDATE hesap SET bakiye = bakiye WHERE id = 1; -- değer değişmedi, WHEN engeller
UPDATE hesap SET sahip = 'Ayşe Yılmaz' WHERE id = 1; -- bakiye SET içinde yok
DELETE FROM hesap WHERE id = 2;
SELECT * FROM hesap_log;
-- çıktı:
-- 1|1|guncelleme|500|600
-- 2|2|guncelleme|200|300
-- 3|2|silme|300|Çıktıda üç kayıt var ve her biri bir kuralı gösteriyor. İlk UPDATE iki satırı değiştirdiği için tetikleyici iki kez çalıştı. İkinci UPDATE bakiyeyi aynı değere yazdı; WHEN OLD.bakiye <> NEW.bakiye koşulu yanlış olduğu için gövde çalışmadı. Üçüncü UPDATE yalnızca sahip sütununu değiştirdi; tetikleyici UPDATE OF bakiye olarak tanımlandığından hiç tetiklenmedi. UPDATE OF yalnızca SET kısmının sol tarafında o sütunlardan biri geçtiğinde devreye girer.
Günlük için AFTER tercih edilmesinin bir nedeni var: AFTER tetikleyicisi çalıştığında değişiklik ve kısıt denetimleri tamamlanmıştır, yani günlüğe yalnızca gerçekten yazılan değerler düşer. Asıl ifade başarısız olursa, aynı ifadenin parçası olarak çalışan tetikleyicinin yaptığı değişiklikler de geri alınır.
BEFORE tetikleyici ve RAISE ile doğrulama
CHECK kısıtları yalnızca aynı satırdaki değerleri denetleyebilir. Başka bir tablodaki veriye bakan kurallar için tetikleyici gerekir. Aşağıdaki örnekte satış kaydı eklenmeden önce stok kontrol ediliyor, yetersizse işlem anlamlı bir mesajla durduruluyor; satış başarılıysa AFTER tetikleyicisi stoktan düşüyor:
CREATE TABLE urun(id INTEGER PRIMARY KEY, ad TEXT, stok INTEGER NOT NULL);
CREATE TABLE satis(id INTEGER PRIMARY KEY, urun_id INTEGER NOT NULL, adet INTEGER NOT NULL);
INSERT INTO urun VALUES (1, 'Kalem', 10), (2, 'Defter', 2);
CREATE TRIGGER satis_stok_kontrol BEFORE INSERT ON satis
WHEN NEW.adet > (SELECT stok FROM urun WHERE id = NEW.urun_id)
BEGIN
SELECT RAISE(ABORT, 'stok yetersiz, kalan: ' ||
(SELECT stok FROM urun WHERE id = NEW.urun_id));
END;
CREATE TRIGGER satis_stok_dus AFTER INSERT ON satis
BEGIN
UPDATE urun SET stok = stok - NEW.adet WHERE id = NEW.urun_id;
END;
INSERT INTO satis(urun_id, adet) VALUES (1, 4);
INSERT INTO satis(urun_id, adet) VALUES (2, 5);
-- hata: stok yetersiz, kalan: 2
SELECT id, stok FROM urun;
-- çıktı:
-- 1|6
-- 2|2
SELECT count(*) FROM satis;
-- çıktı: 1RAISE(ABORT, mesaj) çağrıldığında o anki ifade durdurulur, ifadenin yaptığı değişiklikler geri alınır ve uygulamaya SQLITE_CONSTRAINT hata koduyla birlikte verdiğiniz mesaj döner. Mesajın bir SQL ifadesi olabilmesi, yani örnekteki gibi birleştirme ve alt sorgu içerebilmesi SQLite 3.47.0 sürümüyle geldi; daha eski sürümlerde mesaj sabit bir metin olmak zorundadır. Uygulamanız eski bir kütüphaneyle çalışıyorsa mesajı düz metin olarak yazın.
RAISE() fonksiyonunun dört biçimi vardır ve ilk üçü çatışma çözüm algoritmalarıyla aynı anlama gelir: ROLLBACK işlemin tamamını geri alır, ABORT yalnızca o ifadeyi geri alır, FAIL ise ifadenin o ana kadar yaptığı değişiklikleri bırakıp durur. Dördüncü biçim olan IGNORE ise hata üretmez. ROLLBACK ile ABORT arasındaki farkı açık bir işlem içinde görmek kolaydır:
CREATE TABLE odeme(id INTEGER PRIMARY KEY, tutar INTEGER);
CREATE TRIGGER odeme_kontrol BEFORE INSERT ON odeme WHEN NEW.tutar <= 0
BEGIN
SELECT RAISE(ROLLBACK, 'tutar pozitif olmalı');
END;
BEGIN;
INSERT INTO odeme(tutar) VALUES (100);
INSERT INTO odeme(tutar) VALUES (-5);
-- hata: tutar pozitif olmalı
COMMIT;
-- hata: cannot commit - no transaction is active
SELECT count(*) FROM odeme;
-- çıktı: 0 (ilk INSERT de geri alındı)
-- Aynı tetikleyici RAISE(ABORT, ...) ile yazılsaydı:
-- yalnızca -5 satırı reddedilir, işlem açık kalır,
-- COMMIT sonrasında count(*) 1 olur.ROLLBACK, açık işlemi tamamen bitirdiği için sonraki COMMIT "işlem yok" hatası verdi ve ilk eklenen satır da kayboldu. Çoğu doğrulama senaryosunda ABORT daha öngörülebilirdir: uygulama hatayı yakalar ve işlemin geri kalanını kendisi yönetir. Çatışma algoritmalarının ayrıntıları için SQLite UPSERT ve ON CONFLICT yazısına bakın.
RAISE(IGNORE) ile satırı sessizce atlamak

RAISE(IGNORE) çağrıldığında tetikleyicinin geri kalanı ve o satır için yapılacak işlem bırakılır; hata dönmez ve o ana kadar yapılan değişiklikler geri alınmaz. Çok satırlı bir INSERT'te bu, kurala uymayan satırların atlanıp diğerlerinin eklenmesi anlamına gelir. RAISE() yalnızca tetikleyici gövdesinde kullanılabilir:
CREATE TRIGGER sifir_adet BEFORE INSERT ON satis WHEN NEW.adet <= 0
BEGIN
SELECT RAISE(IGNORE);
END;
INSERT INTO satis(urun_id, adet) VALUES (1, 0), (1, 1), (2, -3);
SELECT urun_id, adet FROM satis;
-- çıktı:
-- 1|4
-- 1|1
SELECT RAISE(ABORT, 'deneme');
-- hata: RAISE() may only be used within a trigger-programÜç satırlık INSERT'ten yalnızca adedi pozitif olan satır eklendi; ilk satır önceki örnekten kalan kayıttır. Bu davranış kullanışlı olsa da sessizdir: uygulama hangi satırların atlandığını öğrenemez. Kullanıcıya geri bildirim vermeniz gerekiyorsa ABORT ile anlamlı bir mesaj döndürmek genellikle daha iyidir.
BEFORE yerine neden çoğunlukla AFTER önerilir
Belgeler BEFORE tetikleyicileri için iki önemli uyarı yapar. Birincisi, BEFORE UPDATE veya BEFORE DELETE tetikleyicisi güncellenecek ya da silinecek satırı kendisi değiştirir veya silerse, ardından gelen asıl işlemin sonucu tanımsızdır; o satırlar için AFTER tetikleyicilerinin çalışıp çalışmayacağı da belirsizdir. İkincisi, rowid değeri açıkça verilmemiş bir eklemede BEFORE INSERT tetikleyicisi içindeki NEW.rowid değeri tanımsızdır; otomatik atanacak kimlik o anda henüz belli değildir. Bu nedenle belgeler mümkün olduğunda AFTER tetikleyicilerini tercih etmenizi önerir.
Pratik bir kural olarak BEFORE tetikleyicisini yalnızca doğrulama ve RAISE için kullanın, veri değiştiren işleri AFTER tetikleyicisine bırakın. Yukarıdaki stok örneği bu ayrımı izliyor: denetim BEFORE'da, stok düşme AFTER'da. Rowid ve INTEGER PRIMARY KEY ilişkisi için SQLite PRIMARY KEY ve rowid yazısına bakın.
UPDATE OF tuzağı: var olmayan sütun adı
Tarihsel bir gözden kaçırma nedeniyle SQLite, UPDATE OF listesinde yazılan sütunların tabloda gerçekten var olup olmadığını denetlemez; tanınmayan adlar sessizce yok sayılır. Belgeler bunun düzeltilmesinin eski uygulamaları bozacağı için değiştirilmediğini belirtir. Sonuç olarak yazım hatası olan bir tetikleyici hiç hata vermeden oluşturulur ve hiçbir zaman çalışmaz:
CREATE TABLE belge(id INTEGER PRIMARY KEY, baslik TEXT, surum INTEGER NOT NULL DEFAULT 1);
-- Yazım hatası: baslikk diye bir sütun yok, ama hata da yok
CREATE TRIGGER yanlis_sutun AFTER UPDATE OF baslikk ON belge BEGIN SELECT 1; END;
CREATE TRIGGER belge_surum AFTER UPDATE OF baslik ON belge
BEGIN
UPDATE belge SET surum = OLD.surum + 1 WHERE id = NEW.id;
END;
INSERT INTO belge(baslik) VALUES ('Taslak');
UPDATE belge SET baslik = 'Sürüm iki' WHERE id = 1;
SELECT * FROM belge;
-- çıktı: 1|Sürüm iki|2İkinci tetikleyici, aynı tabloyu güncelleyen bir AFTER tetikleyicisinin tipik örneğidir: başlık her değiştiğinde surum sütunu bir artırılır. Tetikleyicinin kendi UPDATE ifadesi yalnızca surum sütununu değiştirdiği için UPDATE OF baslik tetikleyicisini yeniden çalıştırmaz. Yeni bir tetikleyici yazdıktan sonra onu tetikleyecek bir değişiklik yapıp sonucu kontrol etmek, bu tür sessiz hataları yakalamanın en kolay yoludur.
Kendi tablosunu güncelleyen tetikleyiciler ve recursive_triggers
Bir tetikleyicinin gövdesi, aynı tetikleyiciyi yeniden ateşleyecek bir değişiklik yapabilir. Bunun olup olmayacağını PRAGMA recursive_triggers belirler. Özyinelemeli tetikleyici desteği 3.6.18 sürümünde eklendi ve uyumluluk için varsayılan olarak kapalı geldi; belgeler ileride varsayılanın açık olabileceğini de not eder.
CREATE TABLE sayac(n INTEGER);
INSERT INTO sayac VALUES (0);
CREATE TRIGGER kendini_cagir AFTER UPDATE ON sayac WHEN NEW.n < 5
BEGIN
UPDATE sayac SET n = NEW.n + 1;
END;
PRAGMA recursive_triggers;
-- çıktı: 0
UPDATE sayac SET n = 1;
SELECT n FROM sayac;
-- çıktı: 2 (tetikleyici bir kez çalıştı, kendi UPDATE'i onu yeniden tetiklemedi)
PRAGMA recursive_triggers = ON;
UPDATE sayac SET n = 1;
SELECT n FROM sayac;
-- çıktı: 5 (WHEN koşulu yanlış olana kadar zincirleme çalıştı)
CREATE TRIGGER sonsuz AFTER UPDATE ON sayac BEGIN UPDATE sayac SET n = n + 1; END;
UPDATE sayac SET n = 0;
-- hata: too many levels of trigger recursionAyar kapalıyken tetikleyici kendi yaptığı UPDATE ile yeniden çalışmadı ve değer 2'de kaldı. Ayar açıldığında zincir, WHEN koşulu yanlış olana kadar sürdü. Koşulu olmayan bir tetikleyici ise derinlik sınırına çarptı ve ifade hatayla durdu. Bu sınır derleme zamanında SQLITE_MAX_TRIGGER_DEPTH ile, çalışma zamanında C arayüzündeki sqlite3_limit() ile belirlenir. Ayar bağlantıya özeldir; kalıcı değildir ve her bağlantıda yeniden verilmesi gerekir.
Bu ayarın göz ardı edilen bir etkisi daha vardır: REPLACE çatışma çözümü bir satırı silip yerine yenisini koyduğunda, silme tetikleyicileri yalnızca recursive_triggers açıkken çalışır. Silme tetikleyicisiyle günlük tutuyorsanız ve tabloya REPLACE ile yazıyorsanız, ayar kapalıyken bu silmeler günlüğe düşmez. Ayrıntılı örnek UPSERT yazısında.
Tetikleyici gövdesindeki söz dizimi kısıtları

Tetikleyici içindeki INSERT, UPDATE ve DELETE ifadeleri, üst düzeydeki karşılıklarının tüm söz dizimini desteklemez. Belgelerdeki kısıtlar şunlardır:
- Değiştirilen tablonun adı şema adıyla nitelenemez;
main.tablodeğil yalnızcatabloyazılır. - TEMP olmayan tetikleyicilerde değiştirilen veya sorgulanan tablo, tetikleyicinin bağlı olduğu tabloyla aynı veritabanında olmalıdır.
INSERT INTO tablo DEFAULT VALUESbiçimi desteklenmez.- UPDATE ve DELETE içinde INDEXED BY ve NOT INDEXED kullanılamaz.
- UPDATE ve DELETE içinde ORDER BY ve LIMIT kullanılamaz; bu, SQLITE_ENABLE_UPDATE_DELETE_LIMIT ile derlenmiş sürümlerde de geçerlidir.
- Ortak tablo ifadeleri (WITH) doğrudan kullanılamaz, ancak gövdedeki ifadelerin alt sorguları içinde kullanılabilir.
CREATE TRIGGER nitelikli AFTER INSERT ON belge
BEGIN INSERT INTO main.sayac VALUES (1); END;
-- hata: qualified table names are not allowed on INSERT, UPDATE, and DELETE statements within triggers
CREATE TRIGGER varsayilan AFTER INSERT ON belge
BEGIN INSERT INTO sayac DEFAULT VALUES; END;
-- hata: near "DEFAULT": syntax errorGövdedeki bir INSERT veya UPDATE kendi ON CONFLICT seçeneğini taşıyabilir. Ancak tetikleyiciyi başlatan dıştaki ifade bir ON CONFLICT seçeneği belirtmişse, iç ifadelerinki yerine dıştakinin çatışma politikası uygulanır.
TEMP tetikleyiciler
CREATE TEMP TRIGGER ile oluşturulan tetikleyici yalnızca onu tanımlayan bağlantıda çalışır; aynı veritabanını değiştiren başka uygulamalar bu tetikleyiciyi görmez. TEMP tetikleyiciler aynı veritabanı kuralına tabi değildir ve bağlı başka veritabanlarındaki tabloları sorgulayıp değiştirebilir. 3.53.0 sürüm notları da TEMP tetikleyici gövdelerinin artık main şemasındaki tabloları değiştirip sorgulayabildiğini belirtir. Kalıcı bir tabloya TEMP tetikleyici eklerken tablo adını şemasıyla birlikte yazın; örneğin ON main.siparis. Belgelere göre şema adı verilmezse, herhangi bir şema değişikliğinde tetikleyici başka bir veritabanındaki aynı adlı tabloya yeniden bağlanabilir. ATTACH ile çalışırken TEMP tetikleyicileri tablo bağlandıktan sonra oluşturun. Bağlı veritabanları için SQLite ATTACH DATABASE yazısına bakın.
Tetikleyicileri listeleme ve silme
Tetikleyiciler sqlite_schema tablosunda type = 'trigger' satırları olarak durur; tbl_name sütunu tetikleyicinin bağlı olduğu tabloyu gösterir. DROP TRIGGER bir tetikleyiciyi kaldırır, tablo silindiğinde ise bağlı tetikleyiciler kendiliğinden silinir:
SELECT type, name, tbl_name FROM sqlite_schema WHERE type = 'trigger';
-- çıktı:
-- trigger|hesap_bakiye_log|hesap
-- trigger|hesap_sil_log|hesap
DROP TABLE hesap;
SELECT count(*) FROM sqlite_schema WHERE type = 'trigger';
-- çıktı: 0
DROP TRIGGER yok;
-- hata: no such trigger: yok
DROP TRIGGER IF EXISTS yok; -- hata yokSQLite'ta tetikleyiciyi geçici olarak devre dışı bırakan bir komut yoktur. Bir tetikleyicinin çalışmasını istemediğiniz bir bakım işlemi yapacaksanız tetikleyiciyi silip işlem sonunda yeniden oluşturabilirsiniz; tanımın metnini önce sqlite_schema tablosundan alın.
Sık yapılan hatalar
- UPDATE OF listesinde sütun adını yanlış yazıp tetikleyicinin çalıştığını sanmak.
- BEFORE tetikleyicisinde aynı satırı değiştirmek; sonuç tanımsızdır.
- BEFORE INSERT içinde otomatik atanacak
NEW.rowidveya INTEGER PRIMARY KEY değerine güvenmek. - recursive_triggers ayarını kalıcı sanmak; her bağlantıda yeniden verilmelidir.
- REPLACE ile yazılan tabloda silme tetikleyicilerinin her zaman çalışacağını varsaymak.
- Tetikleyici gövdesinde
main.tablogibi nitelenmiş ad veya DEFAULT VALUES kullanmak. - RAISE(ROLLBACK) ile işlemin tamamını geri alıp uygulamanın açık işlem varsaymasına yol açmak.
Sık Sorulan Sorular
Bir tabloda aynı olay için birden fazla tetikleyici olabilir mi?
Evet, aynı tabloya aynı olay için birden fazla tetikleyici tanımlayabilirsiniz. Birbirine bağımlı adımları tek tetikleyicinin gövdesinde sırayla yazmak, çalışma sırasıyla ilgili belirsizlikleri ortadan kaldırır.
Tetikleyici yabancı anahtarların yerine kullanılabilir mi?
Kullanılabilir ama gerekmez. SQLite, PRAGMA foreign_keys = ON ile yabancı anahtarları ve ON DELETE CASCADE gibi eylemleri yerleşik olarak uygular. Tetikleyicileri, yabancı anahtarların ifade edemediği kurallar için ayırın. Ayrıntılar için SQLite foreign key yazısına bakın.
Tetikleyici içinde hata olursa uygulamaya hangi kod döner?
RAISE(ABORT), RAISE(FAIL) ve RAISE(ROLLBACK) uygulamaya SQLITE_CONSTRAINT kodu ve verdiğiniz mesajı döndürür. Hata kodlarının tam listesi için SQLite hata kodları yazısına bakın.
Tetikleyiciler UPSERT ile birlikte nasıl çalışır?
Denememizde her dört tetikleyiciye de günlük satırı yazdırdık. Çatışma olmayan UPSERT'te BEFORE INSERT ve AFTER INSERT çalıştı. Çatışma olup DO UPDATE devreye girdiğinde sıra "before insert, before update, after update" oldu; yani BEFORE INSERT tetikleyicisi, satır sonunda eklenmese bile çalıştı. DO NOTHING ile sonuçlanan çatışmada da yalnızca BEFORE INSERT çalıştı. BEFORE INSERT tetikleyicinizde yan etkili işler varsa bu davranışı hesaba katın.
Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.
İlgili içerikler: SQLite öğrenme rehberi · SQLite CREATE VIEW · SQLite UPSERT · SQLite transaction · SQLite foreign key · SQLite INSERT, UPDATE, DELETE