SQLite'ta database is locked hatası, SQLITE_BUSY (5) sonuç kodunun mesajıdır ve başka bir veritabanı bağlantısının, çoğunlukla başka bir sürecin, dosya üzerinde sizin işleminizle çakışan bir kilit tuttuğunu söyler. SQLite aynı anda yalnızca bir yazara izin verir. Çözüm genellikle şu adımların birleşimidir: her bağlantıda PRAGMA busy_timeout ayarlamak, yazma işlemlerini kısa tutup BEGIN IMMEDIATE ile başlatmak, açık kalan okuma işlemlerini kapatmak, WAL kipine geçmek ve veritabanını ağ dosya sisteminde değil yerel diskte tutmak.
Bu yazıda kilit durumlarını resmî belgelere göre açıklıyor, hatayı sqlite3 komut satırının .connection komutuyla iki bağlantı açarak tek bir betikte yeniden üretiyor ve her senaryonun neden oluştuğunu gösteriyoruz. Benzer görünen SQLITE_LOCKED (6) kodunun farkını da ayrı bir bölümde ele alıyoruz.
SQLite kilitleri nasıl çalışır?

Varsayılan rollback journal kipinde, tek bir sürecin bakış açısından veritabanı dosyası beş kilit durumundan birinde bulunur. Okuma için SHARED kilit gerekir ve aynı anda çok sayıda süreç SHARED kilit tutabilir. Yazmak isteyen süreç önce RESERVED kilidi alır; bu kilit aynı anda yalnızca bir süreçte olabilir, ancak diğerleri okumaya devam edebilir. Değişiklikler diske yazılacağı zaman yazar PENDING ve ardından EXCLUSIVE kilide geçer. EXCLUSIVE kilit varken başka hiçbir kilit bulunamaz.
| Kilit | Anlamı | Başkaları ne yapabilir |
|---|---|---|
| UNLOCKED | Hiç kilit yok | Okuyabilir ve yazabilir |
| SHARED | Okuma yapılıyor | Okuyabilir, yazamaz |
| RESERVED | İleride yazma niyeti var, şimdilik okuyor | Okumaya başlayabilir, RESERVED alamaz |
| PENDING | Yazar EXCLUSIVE için mevcut okuyucuların çıkmasını bekliyor | Yeni SHARED kilit alamaz |
| EXCLUSIVE | Değişiklikler dosyaya yazılıyor | Hiçbir kilit alamaz |
PENDING durumunun varlık nedeni yazarın aç kalmasını önlemektir: sürekli yeni okuyucu gelirse yazar hiç fırsat bulamazdı. PENDING kilit mevcut okuyucuların işini bitirmesine izin verir ama yeni okuyucuları kapıda bekletir. Bir bağlantı RESERVED kilidi alamazsa, yani başka bir bağlantı zaten yazma niyetini bildirmişse, belge yazma girişiminin hemen SQLITE_BUSY ile döndüğünü belirtir.
SQL düzeyinde ise BEGIN komutu tek başına kilit almaz. İlk SELECT ile SHARED, ilk INSERT, UPDATE veya DELETE ile RESERVED kilit alınır; EXCLUSIVE ise bellek önbelleği taşana veya işlem kaydedilene kadar ertelenir. Ertelemenin amacı okuyucuları mümkün olan en son ana kadar engellememektir, ama aşağıda göreceğimiz kilitlenme durumlarının kaynağı da budur. İşlem türlerinin ayrıntısı SQLite transaction yazısında.
Hatayı yeniden üretmek
Komut satırı aracının .connection komutu aynı süreç içinde ek bağlantılar açmanızı sağlar; her biri dosyaya ayrı bir bağlantı olarak erişir. Aşağıdaki betikte bağlantı 0 bir yazma işlemi başlatıp açık bırakıyor, bağlantı 1 aynı satırı güncellemeye çalışıyor:
-- sqlite3 kilit.db < kilit1.sql
CREATE TABLE sayac(ad TEXT PRIMARY KEY, deger INTEGER);
INSERT INTO sayac VALUES ('ziyaret', 0);
-- Bağlantı 0: yazma işlemini başlatıp açık bırakıyoruz
BEGIN IMMEDIATE;
UPDATE sayac SET deger = deger + 1;
-- Bağlantı 1: aynı dosyaya ikinci bir bağlantı
.connection 1
.open kilit.db
PRAGMA busy_timeout;
-- çıktı: 0
UPDATE sayac SET deger = deger + 10;
-- hata: database is locked
SELECT deger FROM sayac;
-- çıktı: 0
-- Bağlantı 0 işini bitirince ikinci bağlantı yazabilir
.connection 0
COMMIT;
.connection 1
UPDATE sayac SET deger = deger + 10;
SELECT deger FROM sayac;
-- çıktı: 11Çıktıda üç ayrıntı dikkat çekiyor. Birincisi, yeni bağlantının bekleme süresi 0'dır; yani kilit görür görmez hata döner. İkincisi, rollback journal kipinde bile ikinci bağlantı okumaya devam edebildi, çünkü bağlantı 0 henüz yalnızca RESERVED kilit tutuyordu ve değişiklikler bellekteydi; okuyucu kaydedilmemiş değeri değil eski değeri gördü. Üçüncüsü, ilk bağlantı COMMIT ettikten sonra ikinci bağlantının aynı güncellemesi sorunsuz çalıştı.
busy_timeout: hemen vazgeçmek yerine beklemek
PRAGMA busy_timeout = N, kilitle karşılaşan bağlantının hata döndürmeden önce toplam N milisaniye boyunca tekrar denemesini sağlar. Bu pragma, C arayüzündeki sqlite3_busy_timeout() fonksiyonunun SQL karşılığıdır ve bu fonksiyona doğrudan erişim sunmayan dil bağlayıcıları için vardır. En büyük değer 2147483647 milisaniyedir, yaklaşık 24,9 gün; bundan büyük değerlerin sonucu tanımsızdır. Komut satırında aynı işi .timeout MS komutu görür.
Her bağlantının yalnızca bir meşgul işleyicisi olabilir ve busy_timeout ayarlamak daha önce kurulmuş özel bir işleyiciyi siler. Ayar dosyada saklanmaz; bağlantıya aittir. Bu yüzden uygulamanızın açtığı her bağlantıda, açılıştan hemen sonra ayarlanması gerekir. Bekleme süresi kilidi tutan tarafın işini bitireceği varsayımına dayanır. Aşağıdaki denemede kilidi tutan bağlantı aynı betikte bekletildiği için hiç bırakılmadı ve ikinci bağlantı süre dolunca yine hata aldı:
-- Bağlantı 0 yine BEGIN IMMEDIATE ile yazma kilidini tutuyor
.connection 1
.open kilit.db
PRAGMA busy_timeout = 300;
-- çıktı: 300
UPDATE sayac SET deger = deger + 10;
-- hata: database is locked (en az 300 ms bekledikten sonra)Bu sonuç önemli bir dersi gösterir: busy_timeout kısa kilitleri tolere eder, uzun süre açık bırakılan işlemleri çözmez. Bir işlem dakikalarca açık kalıyorsa, örneğin kullanıcıdan girdi beklerken veya ağ çağrısı yaparken işlemi açık tutuyorsanız, süreyi artırmak yalnızca hatanın daha geç gelmesine yol açar.
sqlite3 -cmd ".timeout 5000" uygulama.dbKilitlenme: bekleme süresinin işe yaramadığı durum

Belgedeki meşgul işleyicisi açıklaması, bir işleyici tanımlı olsa bile her zaman çağrılmayacağını belirtir. İki süreç düşünün: birincisi SHARED kilit tutuyor ve onu RESERVED kilide yükseltmek istiyor; ikincisi RESERVED kilit tutuyor ve EXCLUSIVE kilide geçmek için birincinin okumayı bitirmesini bekliyor. İkisi de beklerse hiçbiri ilerleyemez. SQLite bu durumu fark ettiğinde işleyiciyi çağırmadan birinci sürece hemen SQLITE_BUSY döndürür ve onun okuma kilidini bırakmasını bekler. Aşağıdaki betik bu senaryoyu adım adım üretiyor:
-- sqlite3 kilit.db < kilit6.sql (yeni bir dosya, rollback journal kipi)
CREATE TABLE sayac(ad TEXT PRIMARY KEY, deger INTEGER);
INSERT INTO sayac VALUES ('ziyaret', 0);
.connection 1
.open kilit.db
BEGIN;
SELECT deger FROM sayac; -- bağlantı 1 SHARED kilit tutuyor
-- çıktı: 0
.connection 0
BEGIN;
UPDATE sayac SET deger = 1; -- bağlantı 0 RESERVED kilidi aldı
COMMIT; -- EXCLUSIVE için okuyucunun çıkmasını bekliyor
-- hata: database is locked
.connection 1
UPDATE sayac SET deger = 2; -- RESERVED kilit başkasında: kilitlenme
-- hata: database is locked
ROLLBACK; -- bağlantı 1 geri çekiliyor
.connection 0
COMMIT; -- işlem hâlâ açıktı, yeniden deneme başarılı
SELECT deger FROM sayac;
-- çıktı: 1Burada iki belge kuralını birlikte görüyoruz. COMMIT bir okuyucu yüzünden SQLITE_BUSY ile başarısız olduğunda işlem açık kalır ve okuyucu çekildikten sonra yeniden denenebilir. Kilitlenmede geri çekilmesi gereken taraf ise okuma işlemini yazmaya yükseltmeye çalışan bağlantıdır: ROLLBACK ile işlemi bırakıp baştan başlamalıdır. Uygulama kodunda bu, hata alındığında aynı ifadeyi tekrar denemek yerine tüm işlemi geri alıp yeniden başlatmak anlamına gelir.
Bu durumun kalıcı çözümü BEGIN IMMEDIATE kullanmaktır. Sonuç kodu belgesine göre BEGIN IMMEDIATE kendisi SQLITE_BUSY döndürebilir, ancak başarılı olursa aynı işlem içindeki sonraki işlemlerin COMMIT'e kadar SQLITE_BUSY almayacağı garanti edilir. Yani bekleme yazmanın başında, busy_timeout işleyicisinin güvenle çalışabildiği bir noktada olur; işlemin ortasında kilitlenme yaşanmaz.
WAL kipi neyi değiştirir?
WAL kipinde değişiklikler ana dosya yerine -wal dosyasının sonuna eklenir. Belgeye göre yazarlar okuyucuları, okuyucular da yazarları engellemez: her okuma işlemi başladığı andaki son kaydı işaret noktası olarak alır ve işlem boyunca o anki veriyi görür. Ancak WAL dosyası tek olduğu için yine aynı anda yalnızca bir yazar olabilir. Aşağıdaki örnekte okuyucu, kaydedilmemiş bir yazma işlemi sürerken engellenmeden eski değeri okudu:
-- sqlite3 kilit.db < kilit3.sql (yeni bir dosya)
PRAGMA journal_mode = WAL;
-- çıktı: wal
CREATE TABLE sayac(ad TEXT PRIMARY KEY, deger INTEGER);
INSERT INTO sayac VALUES ('ziyaret', 0);
BEGIN IMMEDIATE;
UPDATE sayac SET deger = deger + 1;
.connection 1
.open kilit.db
SELECT deger FROM sayac; -- yazar işini bitirmedi, okuyucu engellenmez
-- çıktı: 0
.connection 0
COMMIT;
.connection 1
SELECT deger FROM sayac; -- yeni okuma işlemi kaydedilmiş değeri görür
-- çıktı: 1WAL kipinde tipik bir tuzak daha vardır. Bir bağlantı BEGIN ile okuma işlemi açar, bu sırada başka bir bağlantı veriyi değiştirirse, ilk bağlantının görüntüsü eskimiş olur. Belge bu durumda işlemi yazmaya yükseltme girişiminin SQLITE_BUSY_SNAPSHOT ile başarısız olduğunu anlatır, çünkü izin verilseydi veritabanının geçmişi çatallanırdı. Bekleme süresi bunu çözmez; işlemi bırakıp yeniden başlatmak gerekir. Komut satırı aracı birincil kodu gösterdiği için mesaj yine database is locked olarak görünür:
-- sqlite3 kilit.db < kilit4.sql (yeni bir dosya)
PRAGMA journal_mode = WAL;
-- çıktı: wal
CREATE TABLE sayac(ad TEXT PRIMARY KEY, deger INTEGER);
INSERT INTO sayac VALUES ('ziyaret', 0);
BEGIN; -- ertelenmiş işlem: önce okuyor
SELECT deger FROM sayac;
-- çıktı: 0
.connection 1
.open kilit.db
UPDATE sayac SET deger = 5; -- başka bağlantı araya girip yazıyor
.connection 0
UPDATE sayac SET deger = deger + 1;
-- hata: database is locked (genişletilmiş kod: SQLITE_BUSY_SNAPSHOT)
ROLLBACK;
BEGIN IMMEDIATE; -- doğru yol: yazacağınızı baştan bildirin
UPDATE sayac SET deger = deger + 1;
COMMIT;
SELECT deger FROM sayac;
-- çıktı: 6WAL kipinde de SQLITE_BUSY alınabilecek birkaç durum belgelenmiştir: başka bir bağlantı veritabanını EXCLUSIVE kilitleme kipinde açmışsa, son bağlantı kapanırken WAL ve paylaşılan bellek dosyalarını temizlediği kısa süre içinde ve bir çökmeden sonra ilk bağlantı kurtarma yaparken. Kurtarma sırasında alınan hata genişletilmiş olarak SQLITE_BUSY_RECOVERY koduyla gelir. Belgede ilginç bir örnek de vardır: bazı tarayıcılar kendi veritabanlarını EXCLUSIVE kipte açtığı için, tarayıcı çalışırken bu dosyaları okumaya çalışmak bu hatayla sonuçlanır.
Bu senaryolardan çıkan pratik bir başlangıç noktası şudur: veritabanını bir kez WAL kipine alın (bu ayar dosyada kalıcıdır) ve her bağlantıda busy_timeout değerini ayarlayın. WAL kipinin checkpoint, dosya boyutu ve paylaşılan bellek ayrıntıları SQLite WAL modu yazısında anlatılıyor.
PRAGMA journal_mode = WAL;
-- çıktı: wal
PRAGMA busy_timeout = 5000;
-- çıktı: 5000SQLITE_LOCKED ile SQLITE_BUSY farkı
İki kodun adı benzediği için sık karıştırılır. SQLITE_BUSY başka bir veritabanı bağlantısıyla, büyük olasılıkla başka bir süreçle yaşanan çakışmadır. SQLITE_LOCKED (6) ise aynı bağlantı içindeki veya paylaşılan önbellek kullanan bağlantılar arasındaki bir çakışmayı bildirir. Belgedeki örnek, aynı bağlantıda bir tablo okunurken o tabloyu DROP TABLE ile silmeye çalışmaktır: tabloyu silmek, devam eden okumanın altından veriyi çekmek anlamına gelir. Paylaşılan önbellek kipinde bir bağlantı bir kaydı değiştirirken diğerinin aynı kayda erişimi engellendiğinde ise SQLITE_LOCKED_SHAREDCACHE döner.
Bu yüzden SQLITE_LOCKED görüyorsanız sorunu başka süreçlerde değil, kendi kodunuzda arayın: sonuçlarını sonuna kadar okumadığınız veya kapatmadığınız bir sorgu, aynı bağlantıda şema değişikliği yapan bir ifadeyle çakışıyor olabilir. Bekleme süresini artırmak bu durumda bir şey değiştirmez.
Ağ dosya sistemleri ve sunucu ortamı
Belgeye göre bazı ağ dosya sistemlerinde yazma kilitleri hatalı çalışmış ve bu durum bozulmaya yol açmıştır. WAL kipi ise paylaşılan bellek kullandığı için tüm okuyucuların aynı makinede olmasını gerektirir ve ağ dosya sisteminde çalışmaz. Belge, verinin uygulamadan ağla ayrıldığı durumlarda üç seçenek sunar: istemci-sunucu bir veritabanı kullanmak, SQLite'ı WAL kipinde dosyanın bulunduğu makinede çalıştırıp uzak isteklere bir vekil süreç üzerinden hizmet vermek ya da rollback kipinde aynı anda ya çok okuyucu ya da tek yazarla yetinmek. Birden çok sunucunun aynı dosyaya yazması gerekiyorsa PostgreSQL veya MySQL gibi bir sunucu veritabanını değerlendirin.
Tek bir sunucuda bile kilit sorunlarını artıran alışkanlıklar vardır. Yönetim aracıyla veritabanını açıp bir işlem başlatmak ve pencereyi öylece bırakmak, uzun süren bir rapor sorgusunu yazma işlemiyle aynı anda çalıştırmak ya da her istek için yeni bağlantı açıp kapatmak bunlara örnektir. Komut satırında elle bir BEGIN yazıp unuttuysanız uygulamanız siz COMMIT veya ROLLBACK yazana kadar yazamaz.
Kontrol listesi

| Belirti | Olası neden | Çözüm |
|---|---|---|
| Hata hemen geliyor | busy_timeout ayarlı değil | Her bağlantıda PRAGMA busy_timeout |
| Hata bekleme süresinden sonra geliyor | Başka bir işlem uzun süre açık | İşlemleri kısaltın, açık kalan BEGIN arayın |
| İşlemin ortasında hata, bekleme etkisiz | Okumadan yazmaya yükseltme kilitlenmesi | BEGIN IMMEDIATE ile başlatın, hatada ROLLBACK edip yeniden deneyin |
| WAL kipinde yazarken hata | Eskimiş okuma görüntüsü (BUSY_SNAPSHOT) | Yazacak işlemleri BEGIN IMMEDIATE ile açın |
| COMMIT sırasında hata | Okuyucu EXCLUSIVE kilidi engelliyor | Okuyucuyu kapatın, COMMIT'i yeniden deneyin veya WAL kullanın |
| Okumalar yazarlar yüzünden takılıyor | Rollback journal kipi | PRAGMA journal_mode=WAL |
| SQLITE_LOCKED (6) | Aynı bağlantıda çakışan ifade | Açık sorguları sonlandırın, şema değişikliğini sonraya bırakın |
| Ara sıra kilit ve bozulma | Ağ dosya sistemi | Dosyayı yerel diske taşıyın |
Sık yapılan hatalar
- busy_timeout ayarını bir kez yapıp kalıcı sanmak: Ayar bağlantıya özeldir; havuzdaki her yeni bağlantıda tekrar ayarlanmalıdır.
- Aynı ifadeyi döngüde tekrar denemek: Kilitlenme ve BUSY_SNAPSHOT durumlarında tek ifadeyi tekrarlamak işe yaramaz; işlemi geri alıp baştan başlatın.
- Okuma sonuçlarını sonuna kadar tüketmemek: Bitmemiş bir sorgu okuma işlemini açık tutar ve diğer bağlantıların COMMIT etmesini engelleyebilir.
- Her şeyi tek dev işleme koymak: Toplu yükleme için işlem gruplamak hızlıdır, ama kilit süresini uzatır. Büyük işleri makul parçalara bölün; ayrıntı için performans ayarları yazısına bakın.
- Kilit dosyalarını silmek:
-walveya-journaldosyası bir kilit dosyası değildir; silmek veri kaybına ve bozulmaya yol açabilir. Ayrıntılar bozuk veritabanı yazısında.
Sık Sorulan Sorular
WAL kipine geçince database is locked hatası tamamen biter mi?
Hayır. Okuyucu ile yazar arasındaki engellemeler büyük ölçüde ortadan kalkar, ama aynı anda tek yazar sınırı devam eder ve belgede listelenen istisnai durumlar yine SQLITE_BUSY üretebilir. Bekleme süresi ve kısa işlemler WAL kipinde de gereklidir.
busy_timeout için ne kadar süre vermeliyim?
Belge bir değer önermez; doğru süre işlemlerinizin ne kadar sürdüğüne bağlıdır. Süre, en uzun normal yazma işleminizden biraz uzun olmalı, ama kullanıcının isteği zaman aşımına uğramadan önce bitmelidir. Hata sürekli süre dolduktan sonra geliyorsa sorun süre değil, uzun açık kalan bir işlemdir.
Hangi bağlantının kilidi tuttuğunu nasıl bulurum?
SQLite kilit sahibini SQL ile raporlamaz. Uzun süre açık kalan işlemleri, komut satırında unutulmuş oturumları ve dosyaya erişen diğer süreçleri kontrol etmeniz gerekir. Uygulama tarafında işlem süresini günlüğe yazmak, sorunlu kod yolunu bulmanın en pratik yoludur.
SQLITE_PROTOCOL hatası da kilitle mi ilgili?
Evet, ama çok nadirdir. Belgeye göre WAL kipinde iki bağlantı aynı anda işlem başlatmaya çalıştığında yarışı kaybeden kısa bir süre bekleyip tekrar dener; aynı bağlantı saniyeler boyunca onlarca kez kaybederse SQLITE_PROTOCOL döner. Bu, yalnızca çok sayıda sürecin aynı veritabanına yoğun biçimde yazdığı durumlarda görülür.
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 WAL modu · SQLite transaction · SQLite hata kodları · SQLite performans ayarları · SQLite bozuk veritabanı kurtarma