SQLite’ın da Rust tarzı edisyonlara ihtiyacı var
(mort.coffee)- SQLite, yerel depolama ve gömülü projelerde yaygın olarak kullanılan kendi kendine yeterli bir RDBMS olsa da, geriye dönük uyumluluk nedeniyle güvenlik ve performans açısından dezavantajlı varsayılan ayarları koruyor
- Varsayılan olarak yabancı anahtar kısıtlamalarını zorunlu kılmaz ve
ROWID’yi yeniden kullanabilir; bu da silinen bir kullanıcının gönderilerinin aynı kimliği alan başka bir kullanıcıya bağlanması gibi veri bütünlüğü sorunlarına yol açabilir - Normal tablolar, tanımlanan sütundan farklı veri tiplerini de saklar; STRICT tablolar bunu engeller ancak her tablo için ayrı ayrı
strictbelirtilmelidir ve kullanıcı tanımlı tip adlarını kullanan yaklaşımla çakışır - Eşzamanlı yazmada anında ortaya çıkan
SQLITE_BUSY, devre dışı bırakılmış WAL ve muhafazakâr senkronizasyon ayarları sırasıylabusy_timeout,journal_mode,synchronouspragma’larıyla iyileştirilebilir PRAGMA edition = 2026gibi yıl tabanlı edisyonlar, güvenli varsayılan ayar paketlerinin seçilmesini sağlayarak mevcut davranışı bozmadan SQLite’ın varsayılanlarını geliştirmeyi sürdürebilir
SQLite’ın güçlü yanları ve eskimiş varsayılanları
- SQLite, gömülü projelerin veritabanı ve yerel veri depolamanın fiilî endüstri standardı sayılabilir; bazı sunucu yazılımları da lobste.rs artık SQLite üzerinde çalışıyor
- Ayrı bir süreç olarak çalışan geleneksel RDBMS’lerden farklı olarak, kütüphane biçiminde bir RDBMS olduğu için yazılımı kendi kendine yeterli tutmayı sağlar
- Genel bir dosya biçiminde olduğu gibi kullanıcı tanımlı serileştirici ve ayrıştırıcı yazma gereği olmadığından, RDBMS ile dosya tabanlı depolamanın avantajlarını birlikte sunar
- Ancak veri bütünlüğü, eşzamanlılık ve performansı etkileyen çeşitli varsayılan ayarların uygunsuz olması sorunu devam ediyor
Yabancı anahtarların zorunlu olmamasından doğan bütünlük sorunları
- Yabancı anahtarlar, veritabanı tutarlılığını korumanın ve kopuk referansları önlemenin temel araçlarından biridir, ancak SQLite bunları varsayılan olarak zorunlu kılmaz
posts.user_idsütunununusers.iddeğerini referans alacağı tanımlansa bile, varsayılan ayarlar aşağıdaki davranışları engelleyemez- Geçerli bir kullanıcı kimliği olmayan gönderi oluşturulması
- O kullanıcıya ait gönderiler bırakılmışken kullanıcının silinmesi
INTEGER PRIMARY KEY, tablonunROWIDtakma adı olur ve SQLite’ınROWIDatama algoritması bazı durumlarda kimlikleri yeniden kullanır- Bob, ID
1ile bir gönderi yazdıktan sonra yalnızca hesabı silinse bile yabancı anahtar hatası oluşmaz - Sonrasında Alice yeniden kullanılan ID
1değerini alırsa, Bob’un eski gönderileri Alice’e bağlanır - Sorgu tarafı da normal biçimde başarılı olduğundan, sorun basit bir kopuk referanstan daha zor fark edilir
- Bob, ID
- Her bağlantıda aşağıdaki pragma etkinleştirilirse yabancı anahtarlar zorunlu hâle getirilebilir
PRAGMA foreign_keys = ON;
- Bu en baştan uygulanmış olsaydı, geride gönderi bırakan bir kullanıcının silinmesi
FOREIGN KEY constraint failedhatasıyla durdurulurdu
Tanımlanandan farklı veri tiplerine izin veren normal tablolar
- SQLite değerleri
NULL,INTEGER,REAL,TEXT,BLOBtiplerinden biri olsa da, normal tablolardaki sütun tanımları saklanabilecek veri tiplerini katı biçimde sınırlamaz; bunun yerine tip yakınlığını (type affinity) belirler INTEGERyakınlığı olan bir sütunda şu saklama kuralları uygulanır- Tamsayı olarak yorumlanabilen
TEXT,INTEGER’a dönüştürülür - Kayan noktalı sayı olarak yorumlanabilen
TEXT, çift duyarlıklı kayan nokta olanREAL’e dönüştürülür - Diğer değerler ise özgün veri tipiyle saklanır
- Tamsayı olarak yorumlanabilen
- Diğer yakınlık türlerinde de ayrı dönüştürme kuralları vardır
BLOByakınlığı, değeri olduğu gibi saklarTEXTyakınlığı,BLOB,TEXT,NULLdeğerlerini olduğu gibi bırakır ve sayılarıTEXT’e dönüştürürREALyakınlığı,INTEGERyakınlığı gibi çalışır ancak tamsayılarıREAL’e dönüştürür
- Bu yüzden
duration_sec INTEGERsütununda bile'Way too long, I mean come on'gibi sayısal olmayan bir dize saklanabilir - Gerçek projelerde, boolean için
1ve0yerine'1've'0'dizelerinin kaydedildiği; bunun da veri temizleme ve hata ayıklama gerektirdiği örnekler görülmüştür
STRICT tablolar ve kullanıcı tanımlı tiplerle çakışma
- STRICT tablolar, bir sütuna yanlış veri tipi konduğunda
cannot store TEXT value in INTEGER columngibi bir tip hatası üretir
CREATE TABLE music (
id INTEGER PRIMARY KEY,
name TEXT,
duration_sec INTEGER
) strict;
-
Tüm tabloları küresel olarak katı yapan bir pragma olmadığından, her
CREATE TABLEifadesine eksiksiz biçimdestricteklemek gerekir -
SQLite, esnek tiplerin faydalı olduğu görüşünü savunsa da, STRICT tablolarda bile
ANYveri tipi belirtilerek her tür değeri saklayan sütunlar oluşturulabilir- Böylece herhangi bir veri tipine izin veren sütunlarla belirli bir veri tipini zorunlu kılan sütunlar açıkça ayrılabilir
-
Daha gerçekçi uyumluluk sorunu ise STRICT tabloların yalnızca veri tiplerini zorunlu kılmakla kalmayıp tip belirteci yorumlama kurallarını da değiştirmesidir
-
Normal tabloların tip adlarını yorumlama biçimi
- Normal SQLite tabloları, bildirilen tip dizesine göre yakınlık belirler
"INT"içeriyorsaINTEGERolarak işlenir"CHAR","CLOB","TEXT"ifadelerinden birini içeriyorsaTEXTolur"BLOB"içeriyorsa veya tip belirtilmemişseBLOBolarak işlenir"REAL","FLOA","DOUB"ifadelerinden birini içeriyorsaREALolur- Hiçbir koşula uymuyorsa
NUMERIColarak işlenir - Gevşek tip sistemiyle bu kurallar birleştirildiğinde,
DATETIME,KEY_VALUE_SET,COLORgibi kullanıcı tanımlı tip adları sütunlara verilebilir - Veritabanı bağlayıcıları veya sarmalayıcıları, tip adına bakarak serileştirme ve serileştirmeyi çözme işlemlerini otomatik yapabilir
- Ayrı bir işleme özelliği olmasa bile, tip adı tek başına sütunun beklenen verisini belgeler
- STRICT doğrudan varsayılan hâle getirilirse bu kullanım biçiminden vazgeçmek gerekir; bu yüzden açık kullanıcı tanımlı tip takma adlarına ihtiyaç vardır
CREATE TYPE KEY_VALUE_SET = TEXT;- Katı tablolarda
KEY_VALUE_SETgibi bir takma ad kullanılabilse, uygulamanın ayrıştırması gerekenTEXTsütunlarının beklenen veri deseni de şemada kaydedilebilir - Kullanıcı tanımlı tiplere
CHECKkısıtı bağlama özelliği de faydalı olurdu - SQL:1999 standardında, kısıtları da destekleyen tip takma adı söz dizimi olan
CREATE DOMAINzaten bulunuyor; SQLite bunu bu standart ifadeyi destekleyerek uygulayabilir
Eşzamanlı yazma ve SQLITE_BUSY
- SQLite birden çok eşzamanlı okumaya izin verir, ancak yazmaları aynı anda yalnızca bir tane olacak şekilde işler
- Varsayılan ayarlarda iki süreç aynı anda yazma kilidi almaya çalıştığında, biri beklemeden hemen
SQLITE_BUSYhatası alır- Disk I/O yapan yazma işlemleri doğası gereği yavaşlayabileceğinden, kilidin bir süre serbest bırakılmasını beklemek daha doğaldır
- Gerçek sistemlerin aralıklı olarak durup elle yeniden deneme döngüsü yazmayı gerektirdiği örnekler de olmuştur
- Aşağıdaki ayar, kilidi almak için en fazla 5 saniye yeniden denedikten sonra ancak
SQLITE_BUSYdöndürür
PRAGMA busy_timeout = 5000;
- Normal işletimde tüm yazmaları tek bir süreçte, mümkünse tek bir iş parçacığında toplamak uygundur ve eşzamanlı yazma hızlı olamaz
- Yine de eşzamanlı yazmanın gerektiği meşru işletim örnekleri vardır
- Veritabanını etkileşimli olarak düzenlemek için
sqlite3komut satırı aracının kullanılması - Ayrı bir ön uç bulunmayan seyrek yönetim işleri için betiklerin çalıştırılması
- Veritabanını etkileşimli olarak düzenlemek için
- Bu tür işler, varsayılan ayarlar nedeniyle beklenmedik
SQLITE_BUSYhatalarına yol açarak çalışan yazılımı kesintiye uğratabilir
WAL ve performans varsayılanları
- SQLite doğru yapılandırıldığında PostgreSQL veya MySQL gibi büyük sunucuların üstlendiği bazı rolleri yerine getirebilecek kadar hızlı olabilir, ancak varsayılan performansı iyi değildir
- Sunucu ortamına yönelik ayrıntılı ayarlamalar için Optimizing SQLite for servers incelenebilir
- Varsayılan ayarlardaki en önemli performans sorunu, Write-Ahead Log, yani WAL’ın devre dışı olmasıdır
PRAGMA journal_mode = WAL;
- WAL, çoğu durumda yazma hızını ciddi biçimde artırır
- Aşağıdaki ayar da birlikte kullanıldığında veri bozulması riski olmadan disk senkronizasyonu sayısını önemli ölçüde azaltabilir
PRAGMA synchronous = NORMAL;
synchronousayarının ayrıntılı davranışı SQLite pragma belgelerinde özetlenmiştir
Yıl tabanlı SQLite edisyonları
- Mevcut varsayılanları bugün doğrudan değiştirmek, eski yazılımları bozabilir ve kullanıcıların gelecekteki SQLite yükseltmelerinden çekinmesine yol açabilir
- Geriye dönük uyumluluğu korurken iyileştirilmiş varsayılanları seçmeye yarayan tek bir üst pragma (super pragma) eklenebilir
PRAGMA edition = 2026;
2026edisyonu en azından aşağıdaki ayarların takma adı gibi çalışır
PRAGMA foreign_keys = ON;
PRAGMA busy_timeout = 5000;
PRAGMA journal_mode = WAL;
PRAGMA synchronous = NORMAL;
- Bu edisyonda yeni tablolar için
strictmodu da varsayılan olarak uygulanır - Rust editions gibi, mevcut kodun davranışını korurken yeni varsayılan davranışların açıkça seçilmesini sağlar
- JavaScript’teki
"use strict";gibi tek bir modun aksine, yıl tabanlı edisyonlar zamana göre makul varsayılan ayar paketlerini değiştirebilir - Örneğin Hctree’nin WAL2 özelliği 2034’te ana dala girerse, gelecekteki
PRAGMA edition = 2034ayarıjournal_mode = WAL2seçimini yapacak şekilde tasarlanabilir - Edisyon sistemi, mevcut programların varsayılan davranışını bozmadan SQLite’ın geçmiş ayarlara bağlı kalmadan güvenlik ve performans varsayılanlarını geliştirmesine imkân veren bir uzlaşma olabilir
2 yorum
Teoride iyi görünüyor ama uyumluluk çatışmaları ve ek yükü düşününce zararlı olacağını düşünüyorum.
SQLite, Aegis gibi gömülü ortamlarda çalışmak üzere doğmuş bir DBMS.
void**ile bodoslama ilerlemek ve__weak__bildiriminden sonra fonksiyonların üzerine yazmak gömülü dünyanın normali. Böyle bir dünyada buna gerçekten gerek var mı, diye düşündürüyor.Lobste.rs yorumları
Dün lobste.rs’in SQLite’a geçtiği haberini (https://lobste.rs/s/ko1ji1/lobste_rs_is_now_running_on_sqlite) görünce bu yazıyı yazdım.
O zamanki konuşmayı okurken SQLite varsayılanlarının küçük pürüzleri aklıma geldi ve bunları bir blog yazısı olarak derledim.
İmkân olursa bu yazıyı SQLite forumunda da paylaşmaya değer: https://sqlite.org/forum/forum
SQL’de
CREATE TYPE, bileşik tipler oluşturmak için kullanılır; Postgres burada aralık, enum, dizi ve oldukça karmaşık temel tipleri bile destekler.İstenen özellik daha çok
CREATE DOMAIN’e yakın. Temel somut tip üzerinden bir newtype oluşturup varsayılan değer ve kısıtlar da belirleyebildiğiniz için, birden çok tabloda aynıCHECKkısıtını tekrarlayan şemalar çok daha sadeleşir. Örneğincreate domain mything as text;veyacreate domain mything as integer not null check (VALUE < 5);şeklinde kullanılabilir.Yalnızca
busy_timeout, anında oluşanSQLITE_BUSYhatalarını engelleyemez; bu oldukça tehlikeli tuzağın da ele alınması gerekiyor.Salt okunur bir sorguyla transaction başlatıp ardından yazma sorgusu çalıştırarak eldeki kilidi yükseltmeye çalıştığınızda, başka bir yazma işlemi varsa
busy_timeoutayarından bağımsız olarak anında başarısız olur. Çözüm içinbegin immediatekullanmak gerekir.Önerilen edition’a bağlanan ayarlardan bazılarının SQLite modelinde bağlantı özelliği, diğerlerinin ise veritabanı dosyasının veya tek tek tabloların özelliği olduğunu dikkate almak gerekiyor.
PRAGMA journal_mode = WALveritabanı dosyasında kalıcı olurkenPRAGMA foreign_keys = ONher bağlantı için tekrar ayarlanmalıdır.STRICTtekil tablonun özelliğidir; bu yüzden aynı veritabanı içinde katı tablolarla katı olmayan tabloları karıştırmak da mümkündür. Dolayısıyla bu birleşik PRAGMA’nın veritabanı dosyasının mı yoksa bağlantının mı özelliği olarak tanımlanacağına karar vermek gerekir. Eski sürüm kütüphanelerin, bilmedikleri özellikler içeren bir dosyayı değiştirip bozmasını önlemek için yerleşik bir uyarı mekanizması olup olmadığını da merak ediyorum.Edition yaklaşımı, SQLite geliştiricilerinin geriye dönük uyumluluğu koruma hedefiyle, güncel önerilen ayarları isteyen kullanıcılar için daha az kafa karıştırıcı varsayılanlar arasında iyi bir uzlaşma gibi görünüyor.
Son zamanlarda RocksDB’ye fena sardım. SQLite gibi gömülü bir veritabanı kütüphanesi, ama SQL’in olmamasını aksine büyük bir avantaj olarak görüyorum.
Her işlemi SQL ile ifade etmek zorunda kalma kısıtının, veritabanıyla çalışırken yalnızca engel olduğunu hissediyorum.
lobste.rs gibi SQL kullanan bir sistemi RocksDB’ye ya da başka bir anahtar-değer veritabanına nasıl taşıyacağımı hayal etmek zor.
busy_timeoutayarlamanın çözüm olduğuna katılmıyorum. SQLite, tek bir yazma thread’i kullanıp birbirinden bağımsız mantıksal transaction’ları toplu işlediğinizde son derece iyi performans verir.Somut yöntem https://andersmurphy.com/2025/12/… adresinde anlatılıyor.
Ancak temizlik işleri için veritabanını doğrudan değiştirmek ya da ayrı bir yönetim ekranı yapmaya değmeyecek seyrek idari işleri script ile çalıştırmak da yaygın. Böyle bir durumda, sırf kötü şans eseri çalışma zamanı çakıştı diye sunucunun ya da uygulamanın rastgele durması iyi değil.