3 puan yazan GN⁺ 3 시간 전 | 2 yorum | WhatsApp'ta paylaş
  • 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ı strict belirtilmelidir 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ıyla busy_timeout, journal_mode, synchronous pragma’larıyla iyileştirilebilir
  • PRAGMA edition = 2026 gibi 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_id sütununun users.id değ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, tablonun ROWID takma adı olur ve SQLite’ın ROWID atama algoritması bazı durumlarda kimlikleri yeniden kullanır
    • Bob, ID 1 ile bir gönderi yazdıktan sonra yalnızca hesabı silinse bile yabancı anahtar hatası oluşmaz
    • Sonrasında Alice yeniden kullanılan ID 1 değ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
  • 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 failed hatasıyla durdurulurdu

Tanımlanandan farklı veri tiplerine izin veren normal tablolar

  • SQLite değerleri NULL, INTEGER, REAL, TEXT, BLOB tiplerinden 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
  • INTEGER yakı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 olan REAL’e dönüştürülür
    • Diğer değerler ise özgün veri tipiyle saklanır
  • Diğer yakınlık türlerinde de ayrı dönüştürme kuralları vardır
    • BLOB yakınlığı, değeri olduğu gibi saklar
    • TEXT yakınlığı, BLOB, TEXT, NULL değerlerini olduğu gibi bırakır ve sayıları TEXT’e dönüştürür
    • REAL yakınlığı, INTEGER yakınlığı gibi çalışır ancak tamsayıları REAL’e dönüştürür
  • Bu yüzden duration_sec INTEGER sütununda bile 'Way too long, I mean come on' gibi sayısal olmayan bir dize saklanabilir
  • Gerçek projelerde, boolean için 1 ve 0 yerine '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 column gibi 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 TABLE ifadesine eksiksiz biçimde strict eklemek gerekir

  • SQLite, esnek tiplerin faydalı olduğu görüşünü savunsa da, STRICT tablolarda bile ANY veri 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çeriyorsa INTEGER olarak işlenir
    • "CHAR", "CLOB", "TEXT" ifadelerinden birini içeriyorsa TEXT olur
    • "BLOB" içeriyorsa veya tip belirtilmemişse BLOB olarak işlenir
    • "REAL", "FLOA", "DOUB" ifadelerinden birini içeriyorsa REAL olur
    • Hiçbir koşula uymuyorsa NUMERIC olarak işlenir
    • Gevşek tip sistemiyle bu kurallar birleştirildiğinde, DATETIME, KEY_VALUE_SET, COLOR gibi 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_SET gibi bir takma ad kullanılabilse, uygulamanın ayrıştırması gereken TEXT sütunlarının beklenen veri deseni de şemada kaydedilebilir
    • Kullanıcı tanımlı tiplere CHECK kı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 DOMAIN zaten 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_BUSY hatası 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_BUSY dö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 sqlite3 komut satırı aracının kullanılması
    • Ayrı bir ön uç bulunmayan seyrek yönetim işleri için betiklerin çalıştırılması
  • Bu tür işler, varsayılan ayarlar nedeniyle beklenmedik SQLITE_BUSY hataları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;

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;
  • 2026 edisyonu 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 strict modu 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 = 2034 ayarı journal_mode = WAL2 seç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

 
tomskang 2 시간 전

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.

 
GN⁺ 3 시간 전
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.

  • 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ı CHECK kısıtını tekrarlayan şemalar çok daha sadeleşir. Örneğin create domain mything as text; veya create domain mything as integer not null check (VALUE < 5); şeklinde kullanılabilir.

    • Önce mevcut tip takma adı özelliği olup olmadığını kontrol etmeliydim. Yazıya bununla ilgili kısmı ekleyeceğim.
  • Yalnızca busy_timeout, anında oluşan SQLITE_BUSY hataları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_timeout ayarından bağımsız olarak anında başarısız olur. Çözüm için begin immediate kullanmak gerekir.

    • Bunun gerçek bir bug olmadığını biliyorum ama açıkçası neredeyse bug gibi hissettiren bir davranış.
  • Ö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 = WAL veritabanı dosyasında kalıcı olurken PRAGMA foreign_keys = ON her bağlantı için tekrar ayarlanmalıdır. STRICT tekil 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.

    • RocksDB bir anahtar-değer veritabanı, SQLite ise ilişkisel veritabanı; yani farklı amaçlar için oluşturulmuş ayrı kavramlar gibi.
      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_timeout ayarlamanı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.

    • Tipik iş yüklerinde yazma işlemini tekille sınırlamak ve birden çok yazma işleminin sürekli rekabet ettiği bir yapı kurmamak daha doğru.
      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.