SQLite'ta birden fazla değişikliği tek bir bütün olarak uygulamak için BEGIN ile bir işlem (transaction) başlatır, değişiklikleri yapar ve COMMIT ile kalıcı hâle getirirsiniz; vazgeçerseniz ROLLBACK her şeyi geri alır. İç içe geri dönüş noktalarına ihtiyacınız olduğunda SAVEPOINT, RELEASE ve ROLLBACK TO kullanılır. Hiç işlem başlatmasanız bile SQLite her ifadeyi kendi kendine bir işlem içinde çalıştırır.

Bu yazıda örtük ve açık işlemleri, işlem içindeki bir hatanın neden bütün işlemi geri almadığını, DEFERRED, IMMEDIATE ve EXCLUSIVE başlangıç türlerini, kilit durumlarını, yalıtım (isolation) kurallarını ve savepoint yığınını çalıştırılmış örneklerle anlatıyoruz. İki bağlantının aynı dosyaya eriştiği durumları komut satırı aracının .connection komutuyla canlandırıyoruz. WAL kipinin ayrıntıları WAL modu yazısında, "database is locked" hatasının teşhisi ise ayrı bir yazıda ele alınıyor.

Örtük işlemler: her ifade zaten bir işlemdir

Örtük işlemler: her ifade zaten bir işlemdir

Belgelere göre SQLite'ta hiçbir okuma veya yazma bir işlemin dışında gerçekleşmez. Veritabanına erişen her komut, ortada bir işlem yoksa kendiliğinden bir işlem başlatır ve son ifade bittiğinde bu işlemi otomatik olarak işler (commit). Bu yüzden tek başına çalıştırdığınız bir UPDATE ya tamamen uygulanır ya da hiç uygulanmaz; yarım kalmış bir güncelleme görmezsiniz.

Örtük işlemin bedeli, her ifadenin ayrı bir commit yapmasıdır. Varsayılan ayarlarda her commit verinin diske güvenle yazılmasını bekler. SQLite SSS sayfası, tek tek yapılan INSERT'lerin yavaşlığından şikâyet edenlere tam olarak bunu hatırlatır: asıl sınır ifade sayısı değil işlem sayısıdır ve birçok ifadeyi tek bir BEGIN ... COMMIT içine almak commit maliyetini hepsine paylaştırır. Bu etkinin ölçümü performans ayarları yazısında bulunuyor.

BEGIN, COMMIT ve ROLLBACK

Açık bir işlem BEGIN ile başlar ve genellikle bir sonraki COMMIT veya ROLLBACK komutuna kadar sürer. END TRANSACTION, COMMIT ile aynı anlama gelir; TRANSACTION kelimesi her iki komutta da isteğe bağlıdır. Klasik örnek bir hesaptan diğerine para aktarmaktır: iki güncellemenin ya ikisi birden olmalı ya da hiçbiri.

sql
CREATE TABLE hesap(
  id     INTEGER PRIMARY KEY,
  ad     TEXT,
  bakiye INTEGER CHECK (bakiye >= 0)
);
INSERT INTO hesap VALUES (1, 'Ayşe', 500), (2, 'Mehmet', 100);

BEGIN;
UPDATE hesap SET bakiye = bakiye - 200 WHERE id = 1;
UPDATE hesap SET bakiye = bakiye + 200 WHERE id = 2;
COMMIT;

SELECT id, bakiye FROM hesap;
-- çıktı:
-- 1|300
-- 2|300

Bağlantı kapanırsa veya ROLLBACK çakışma çözümü belirtilmiş bir kısıt ihlali olursa işlem de geri alınır. Programınız işlem ortasında çökerse, bir sonraki açılışta SQLite yarım kalmış değişiklikleri günlük dosyasından otomatik olarak geri sarar; bu konuyu aşağıda atomik commit bölümünde göreceksiniz.

İşlem içinde hata olunca ne olur?

Yeni başlayanların en sık şaşırdığı nokta budur. Bir kısıt ihlali (UNIQUE, NOT NULL, CHECK) olduğunda varsayılan çakışma çözümü ABORT olarak uygulanır. Belgelere göre ABORT yalnızca hatayı veren ifadenin yaptığı değişiklikleri geri alır; aynı işlemdeki önceki ifadelerin değişiklikleri korunur ve işlem açık kalır. Yani SQLite sizin yerinize bütün işlemi iptal etmez:

sql
BEGIN;
UPDATE hesap SET bakiye = bakiye - 1000 WHERE id = 2;
-- hata: CHECK constraint failed: bakiye >= 0
UPDATE hesap SET bakiye = bakiye + 1000 WHERE id = 1;
SELECT id, bakiye FROM hesap;
-- çıktı:
-- 1|1300
-- 2|300
ROLLBACK;

SELECT id, bakiye FROM hesap;
-- çıktı:
-- 1|300
-- 2|300

Birinci güncelleme CHECK kısıtına takıldı ve geri alındı, ama ikinci güncelleme sorunsuz çalıştı; işlem hâlâ açıktı ve COMMIT yazsaydık Ayşe'nin hesabına karşılığı olmayan 1000 birim eklenmiş olurdu. Doğru davranış, uygulama kodunda her ifadenin sonucunu kontrol edip hata durumunda açıkça ROLLBACK çalıştırmaktır. Komut satırında çalışan betiklerde .bail on ilk hatada aracın durmasını sağlar; durmadan önce açık işlem bağlantı kapanırken geri alınır. Kısıt ihlalinde bütün işlemin geri alınmasını istiyorsanız ilgili kısıta ON CONFLICT ROLLBACK yazabilirsiniz; ayrıntılar UPSERT ve ON CONFLICT yazısında.

Bazı hatalar ise SQLite'ın işlemi kendisinin geri almasına yol açabilir. Belgeler bunları disk veya veritabanı dolu (SQLITE_FULL), disk giriş/çıkış hatası (SQLITE_IOERR), kesme (SQLITE_INTERRUPT) ve bellek yetersizliği (SQLITE_NOMEM) olarak sayar. Bu durumlarda SQLite önce yalnızca o ifadeyi geri almaya çalışır, ama bazen bütün işlemi iptal etmek zorunda kalır. Durumu her zaman aynı noktaya getirmek için belgeler bu hatalardan sonra açıkça ROLLBACK çalıştırmayı önerir; işlem zaten geri alınmışsa ROLLBACK bir hata döndürür ama zarar vermez.

İşlemler iç içe geçmez

BEGIN ... COMMIT ile başlatılan işlemler iç içe kullanılamaz. Bir işlem açıkken ikinci bir BEGIN, işlem SAVEPOINT ile başlamış olsa bile hata verir. İç içe yapı gerekiyorsa savepoint kullanılır.

sql
BEGIN;
BEGIN;
-- hata: cannot start a transaction within a transaction
COMMIT;
COMMIT;
-- hata: cannot commit - no transaction is active

Bu davranış, bir fonksiyonun kendi işlemini açtığı ve başka bir fonksiyonun içinden çağrıldığı kodlarda sık karşılaşılan bir sorundur. Böyle katmanlı kodda iç fonksiyonların BEGIN yerine SAVEPOINT kullanması, hem tek başına hem de dış bir işlemin içinde doğru çalışmalarını sağlar.

DEFERRED, IMMEDIATE ve EXCLUSIVE

DEFERRED, IMMEDIATE ve EXCLUSIVE: DEFERRED (varsayılan), IMMEDIATE, EXCLUSIVE

SQLite aynı anda birçok okuma işlemine izin verir ama aynı anda yalnızca tek bir yazma işlemi olabilir. BEGIN'in hangi türde açıldığı, yazma hakkının ne zaman istendiğini belirler. Varsayılan tür DEFERRED'dır ve aslında hiçbir şeyi hemen başlatmaz: bağlantıya yalnızca "son ifade bitince otomatik commit yapma" diye bir işaret koyar. İlk ifade SELECT ise bir okuma işlemi başlar; sonradan gelen bir yazma ifadesi işlemi yazma işlemine yükseltmeye çalışır. Başka bir bağlantı o arada veritabanını değiştirmişse veya değiştirmekteyse bu yükseltme mümkün olmaz ve ifade SQLITE_BUSY ile başarısız olur. İlk ifade yazma ise doğrudan yazma işlemi başlar.

IMMEDIATE, ilk yazma ifadesini beklemeden yazma işlemini hemen başlatır. Başka bir bağlantı zaten yazıyorsa BEGIN IMMEDIATE komutunun kendisi SQLITE_BUSY ile başarısız olabilir; ama komut başarılı olursa, belgelerin WAL için vurguladığı gibi, o işlem içindeki sonraki ifadeler artık meşgul hatası almaz. EXCLUSIVE de yazma işlemini hemen başlatır; WAL kipinde IMMEDIATE ile aynıdır, diğer günlük kiplerinde ise işlem sürerken diğer bağlantıların okumasını da engeller.

TürYazma kilidi ne zaman istenirTipik kullanım
DEFERRED (varsayılan)İlk yazma ifadesindeYalnızca okuyan veya nadiren yazan işlemler
IMMEDIATEBEGIN anındaÖnce okuyup sonra yazacağı belli olan işlemler
EXCLUSIVEBEGIN anında; rollback kipinde okuyucuları da dışarıda tutarÖzel durumlar

Komut satırı aracı 3.37.0 sürümünden beri aynı oturumda en fazla on bağlantı tutabilir ve .connection N ile aralarında geçiş yapılır. Aşağıdaki oturumda iki bağlantı aynı dosyayı kullanıyor; veritabanı varsayılan rollback (DELETE) günlük kipinde:

sql
-- sqlite3 iki.db ile açılan oturum; 0 ve 1 numaralı iki ayrı bağlantı
CREATE TABLE sayac(ad TEXT PRIMARY KEY, deger INTEGER);
INSERT INTO sayac VALUES ('ziyaret', 0);

BEGIN IMMEDIATE;                       -- bağlantı 0 yazma işlemini hemen başlatır
UPDATE sayac SET deger = deger + 1;

.connection 1
.open iki.db
SELECT deger FROM sayac;               -- okuma serbest, işlenmemiş değişiklik görünmez
-- çıktı: 0
BEGIN IMMEDIATE;
-- hata: database is locked
UPDATE sayac SET deger = deger + 10;
-- hata: database is locked

.connection 0
COMMIT;

.connection 1
SELECT deger FROM sayac;
-- çıktı: 1
BEGIN IMMEDIATE;
UPDATE sayac SET deger = deger + 10;
COMMIT;
SELECT deger FROM sayac;
-- çıktı: 11

Bağlantı 0 yazma işlemini tutarken bağlantı 1 okuyabildi ve işlenmemiş değişikliği görmedi. Kendi yazma işlemini başlatma girişimi ise "database is locked" hatası aldı. Gerçek uygulamalarda bu hatayı hemen almak yerine bir süre beklemek için PRAGMA busy_timeout ayarlanır; komut satırı tek iş parçacığında çalıştığı için bu örnekte bekleme fark yaratmazdı. Bağlantı 0 işlediği anda bağlantı 1 hem yeni değeri gördü hem de kendi işlemini tamamlayabildi.

Kilit durumları

Rollback günlüğü kullanan kiplerde eşzamanlılık dosya kilitleriyle yönetilir. Kilitleme belgesi bir bağlantının veritabanı dosyası üzerinde bulunabileceği beş durumu tanımlar ve atomik commit belgesi bir yazma işleminin bu durumlardan sırayla nasıl geçtiğini anlatır:

KilitAnlamı
UNLOCKEDKilit yok; varsayılan durum
SHAREDOkuma; birden çok bağlantı aynı anda tutabilir, kimse yazamaz
RESERVEDYazmaya niyetli; yalnızca bir bağlantı tutabilir, yeni okuyucular gelebilir
PENDINGYazmak üzere; mevcut okuyucular bitirir, yeni okuyucu kabul edilmez
EXCLUSIVEDosyaya yazılıyor; başka hiçbir kilit bulunamaz

PENDING aşaması, sürekli yeni okuyucuların geldiği bir sistemde yazarın hiç sıra bulamamasını önlemek için vardır. Bu yapı yüzünden rollback kipinde, bir okuma işlemi açıkken başka bir bağlantı değişikliğini diske yazamaz; okuyucu bitene kadar yazar bekler veya meşgul hatası alır. WAL kipinde ise değişiklikler ayrı bir dosyaya eklendiği için okuyucular ve yazar birbirini engellemez.

Yalıtım: kim neyi görür?

Belgelere göre paylaşılan önbellek ve read_uncommitted birlikte kullanılmadıkça SQLite'taki işlemler "serializable" yalıtım düzeyindedir ve bu, yazmaların gerçekten sıraya konmasıyla sağlanır. Bir bağlantının işlenmemiş değişiklikleri diğer bağlantılara görünmez. Bir okuma işlemi açık tutulduğu sürece o bağlantı, işlemin başındaki veritabanı görüntüsünü görmeye devam eder; başka bağlantıların sonradan işlediği değişiklikleri görmek için işlemini bitirip yenisini başlatması gerekir.

sql
-- sqlite3 iki.db; veritabanı WAL kipinde
PRAGMA journal_mode = WAL;
-- çıktı: wal
CREATE TABLE sayac(ad TEXT PRIMARY KEY, deger INTEGER);
INSERT INTO sayac VALUES ('ziyaret', 0);

.connection 1
.open iki.db
BEGIN;                                 -- DEFERRED: henüz kilit yok
SELECT deger FROM sayac;               -- okuma işlemi başladı, anlık görüntü alındı
-- çıktı: 0

.connection 0
UPDATE sayac SET deger = 5;            -- WAL'da okuyucu yazarı engellemez
SELECT deger FROM sayac;
-- çıktı: 5

.connection 1
SELECT deger FROM sayac;               -- hâlâ eski anlık görüntü
-- çıktı: 0
UPDATE sayac SET deger = deger + 1;    -- eski görüntüden yazmaya geçilemez
-- hata: database is locked
COMMIT;
SELECT deger FROM sayac;
-- çıktı: 5

WAL kipinde bağlantı 1'in okuma işlemi bir anlık görüntüye (snapshot) bağlıdır. Bağlantı 0 değeri 5 yaptıktan sonra bile bağlantı 1 eski değer olan 0'ı görmeye devam etti. Ardından aynı işlem içinde yazmaya çalışınca hata aldı: belgelere göre bu, genişletilmiş kodu SQLITE_BUSY_SNAPSHOT olan bir meşgul hatasıdır, çünkü eski bir görüntüden yazmaya izin vermek veritabanı geçmişini çatallandırırdı. Komut satırı yalnızca birincil hata metnini yazdırır. Çözüm, işlemi bitirip yeniden başlamak ya da yazacağınızı baştan biliyorsanız işlemi BEGIN IMMEDIATE ile açmaktır.

Aynı bağlantı içinde ise yalıtım yoktur: bir bağlantının henüz işlemediği değişiklikler, o bağlantının sonraki SELECT ifadelerinde görünür. Belgeler ayrıca, bir SELECT satır satır okunurken aynı bağlantıda yapılan değişikliklerin o SELECT'in sonraki satırlarına yansıyıp yansımayacağının tanımsız olduğunu, bu yüzden böyle bir varsayıma dayanan kod yazılmaması gerektiğini belirtir.

SAVEPOINT, RELEASE ve ROLLBACK TO

Savepoint'ler adlandırılmış ve iç içe geçebilen işlemlerdir. SAVEPOINT ad yeni bir işaret koyar; bir işlemin dışında verilirse BEGIN DEFERRED TRANSACTION gibi davranır, bir işlemin içinde verilirse mevcut işlemin içine bir geri dönüş noktası ekler. Adların benzersiz olması gerekmez.

sql
SAVEPOINT siparis;
INSERT INTO hesap VALUES (3, 'Zeynep', 50);

SAVEPOINT kalem;
UPDATE hesap SET bakiye = 0 WHERE id = 3;
ROLLBACK TO kalem;           -- yalnızca 'kalem' sonrası geri alınır

SELECT id, bakiye FROM hesap WHERE id = 3;
-- çıktı: 3|50

RELEASE siparis;             -- en dıştaki savepoint: COMMIT gibi davranır
SELECT count(*) FROM hesap;
-- çıktı: 3

RELEASE yok;
-- hata: no such savepoint: yok

ROLLBACK TO ad, veritabanını o savepoint'in hemen sonrasındaki hâline döndürür ama düz ROLLBACK'ten farklı olarak işlemi iptal etmez: savepoint yığında kalır ve aradaki savepoint'ler silinir. RELEASE ad, en son eklenenden başlayarak o ada sahip en yakın savepoint dahil tüm işaretleri yığından kaldırır; en dıştaki savepoint'i bırakırsa işlem commit edilir. Yığında olmayan bir adla RELEASE veya ROLLBACK TO hata döndürür ve hiçbir şeyi değiştirmez.

Belgeler RELEASE'i düşünmek için üç bakış açısı önerir: savepoint için bir COMMIT, adlandırılmış işlemi üst işlemle birleştirmek ya da zaman çizelgesindeki işaretleri silmek. Pratikte en kullanışlı kalıp şudur: bir toplu işlemde her kayıt için savepoint açın, kayıt hatalıysa yalnızca onu ROLLBACK TO ile geri alıp devam edin, sonunda dış işlemi commit edin.

Atomik commit perde arkasında nasıl çalışır?

Varsayılan DELETE günlük kipinde SQLite bir sayfayı değiştirmeden önce o sayfanın orijinal içeriğini veritabanıyla aynı dizindeki bir geri alma günlüğüne (rollback journal) yazar ve bu günlüğü diske kalıcı olarak işler. Sonra değişiklikleri veritabanı dosyasına yazar, onları da diske işler ve en sonunda günlük dosyasını siler. Belgelere göre commit tam olarak bu silme anında gerçekleşir: işletim sistemine "günlük var mı?" diye sorulduğunda cevap ya evet ya hayırdır, bu da işlemin dışarıdan atomik görünmesini sağlar.

Commit sırasında elektrik kesilirse, bir sonraki açılışta SQLite geride kalan günlüğün "sıcak" (hot) olup olmadığına bakar; sıcaksa orijinal sayfaları geri yazar ve veritabanı işlem hiç başlamamış gibi olur. Bu yüzden -journal uzantılı dosyayı veya WAL kipindeki -wal dosyasını elle silmek veri kaybına ve bozulmaya yol açabilir. ATTACH ile bağlanmış birden çok dosyayı değiştiren bir işlem rollback kiplerinde bir "süper günlük" (super-journal) yardımıyla bütün dosyalarda atomik olarak işlenir; WAL kipinde ise belgelere göre her dosya kendi içinde atomiktir ama dosyalar birlikte bir küme olarak atomik değildir. Ayrıntılar ATTACH DATABASE ve bozuk veritabanı kurtarma yazılarında.

Sık yapılan hatalar

Sık yapılan hatalar: Hata sonrası COMMIT etmek, Önce okuyup sonra yazan işlemleri DEFERRED açmak, Okuma işlemini açık unutmak, İç içe BEGIN yazmak
  • Hata sonrası COMMIT etmek: ABORT yalnızca hatalı ifadeyi geri alır; işlem açık kalır. Hata gördüğünüzde ROLLBACK çalıştırın.
  • Önce okuyup sonra yazan işlemleri DEFERRED açmak: Yazmaya geçerken meşgul hatası alabilirsiniz; bu tür işlemler için BEGIN IMMEDIATE kullanın.
  • Okuma işlemini açık unutmak: Uzun süre açık kalan bir okuma işlemi rollback kipinde yazarları, WAL kipinde ise checkpoint'i bekletir.
  • İç içe BEGIN yazmak: İkinci BEGIN hata verir; katmanlı kodda SAVEPOINT kullanın.
  • journal_mode=OFF ile ROLLBACK beklemek: Belgelere göre günlük kapalıyken ROLLBACK'in davranışı tanımsızdır.

Sık Sorulan Sorular

Tek bir INSERT için BEGIN yazmam gerekir mi?

Hayır. Tek ifade zaten kendi örtük işlemi içinde atomik olarak çalışır. BEGIN, birden çok ifadeyi bir bütün yapmak veya toplu yazmaları hızlandırmak için gereklidir.

İki farklı bağlantı aynı anda yazabilir mi?

Hayır, aynı veritabanına aynı anda yalnızca bir yazma işlemi olabilir. Diğer bağlantı sırasını bekler veya meşgul hatası alır; bekleme süresini PRAGMA busy_timeout belirler.

COMMIT neden "database is locked" hatası verebilir?

Rollback kipinde, başka bir bağlantı okuma işlemi tutarken yazar EXCLUSIVE kilidi alamaz. Belgelere göre COMMIT bu şekilde başarısız olursa işlem açık kalır ve okuyucu bitirdikten sonra COMMIT yeniden denenebilir.

Programlama dilimin kütüphanesi işlemleri kendisi mi yönetiyor?

Bazı bağlayıcılar varsayılan olarak örtük BEGIN ekler veya kendi işlem modunu sunar. Kullandığınız kütüphanenin davranışı için Python sqlite3, Node.js node:sqlite veya PHP PDO yazılarımıza bakı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 WAL modu · SQLite database is locked hatası · SQLite UPSERT ve ON CONFLICT · SQLite performans ayarları