İçeriğe geç
Tüm yazılar

Room veritabanını 2026'da küçültmek: VACUUM, auto_vacuum ve asla gerçekten gitmeyen satırlar

Satırları sildikten sonra bile bir Room/SQLite veritabanı dosyasının neden büyümeye devam ettiği, ve bu alanı VACUUM, kademeli auto_vacuum ve gerçek bir temizlik işiyle nasıl güvenle geri kazanacağınız.

MFKAPPS 4 dk okuma

Bir Room veritabanından on bin satır silin, diskteki dosya küçülmez. Kullanıcılar bunu dürüst yoldan fark eder: eski işlemleri temizlerler, “temiz bir başlangıç” için uygulamayı kaldırıp yeniden kurarlar, ve Ayarlar’daki uygulamanın depolama girdisi neredeyse hiç kıpırdamaz. Silme işleminde yanlış bir şey yok — satırlar gitti, sorgu sonuçları doğru — ama SQLite bir tablo küçüldü diye sayfaları dosya sistemine geri vermez. Onları boş olarak işaretler ve aynı dosyada, bir sonraki yazmanın yeniden kullanması için saklar.

Bu bilinçli bir varsayılan, bir hata değil, ve neden bu şekilde çalıştığını anlamak, düzeltmek için ihtiyacınız olanın büyük kısmı.

Dosya kendiliğinden neden küçülmez

SQLite bir veritabanını sabit boyutlu sayfalar halinde düzenler. Bir DELETE, satırları bir sayfadan kaldırır ve o sayfayı dahili bir boş listeye (freelist) ekler — bir sonraki INSERT için kullanılabilir, ama hâlâ dosyaya ayrılmış durumda. SQLite’a açıkça istemediğiniz sürece dosyanın kendisi küçülmez, çünkü bu ayrı, daha maliyetli bir işlemdir: tüm veritabanını, boşaltılan sayfalar gerçekten kaldırılmış yeni bir dosyaya yeniden yazması ve sonra onu devreye sokması gerekir.

Varsayılan olarak, Room’un auto_vacuum modu NONE’dır. Bu, çoğu uygulama için çoğu zaman doğru seçimdir — her yazmanın bir dosya yeniden yazımı değil, sadece bir sayfa güncellemesi olduğu anlamına gelir — ama aynı zamanda bir yıllık yoğun kullanım boyunca 40 MB’a büyümüş ve sonra satırlarının %90’ı silinmiş bir veritabanının hâlâ 40 MB’lık bir dosya olduğu anlamına da gelir. Granyn gibi bir uygulamada, birisi bütçeleme yöntemini değiştirdikten sonra yıllarca birikmiş eski işlem geçmişini silebileceği için, “depolama şişmesi” hakkındaki destek e-postalarının tam olarak çıktığı yer burasıdır.

Alanı geri kazanmanın üç yolu, ve gerçek maliyetleri

PRAGMA vacuum tüm dosyayı yeniden inşa eder ve her boş sayfayı tek seferde geri kazanır. En kapsamlı seçenektir ve en maliyetlisidir: veritabanının şu anda kapladığı kadar boş disk alanına ihtiyaç duyar, süresince özel bir kilit tutar, ve orta seviye bir cihazda onlarca megabaytlık bir veritabanında saniyenin fark edilir bir kesrinden birkaç saniyeye kadar sürebilir. Asla ana iş parçacığında çağırmayın, ve asla her uygulama açılışında otomatik olarak çağırmayın — bu bir bakım işlemidir, bir başlangıç adımı değil.

auto_vacuum = INCREMENTAL, büyüyen bir yerel öncelikli uygulama için varsayılan olarak kullanmaya değer olanıdır. Herhangi bir tablo var olmadan önce bir kez ayarlanır, dosyayı otomatik olarak yeniden yazmaz — bunun yerine, tam bir VACUUM’un hep ya da hiç maliyeti olmadan bir programa göre çalıştırabileceğiniz PRAGMA incremental_vacuum(N) ile sayfaları azar azar geri kazanmanıza izin verir.

auto_vacuum = FULL, her işlemden sonra alanı otomatik olarak geri kazanır. Kulağa kullanışlı geliyor ama kaçınırım: rutin silmeleri yeniden yazma ağırlıklı işlemlere çevirir, nadir bir bakım maliyetini, her yazmada sonsuza kadar süren küçük bir vergiyle takas eder.

auto_vacuum, yalnızca boş bir veritabanında ayarlanabilir ya da sonradan tam bir VACUUM ile değiştirilebilir — daha sonra yeniden yazmadan değiştirebileceğiniz bir ayar değildir, bu yüzden göndermeden önce karar verin:

Room.databaseBuilder(context, AppDatabase::class.java, "app.db")
    .addCallback(object : RoomDatabase.Callback() {
        override fun onOpen(db: SupportSQLiteDatabase) {
            db.query("PRAGMA auto_vacuum").use { if (it.moveToFirst() && it.getInt(0) == 0) {
                db.execSQL("PRAGMA auto_vacuum = INCREMENTAL")
            } }
        }
    })
    .build()

Bu pragma’yı mevcut bir NONE veritabanında ayarlamanın yalnızca bir sonraki tam VACUUM’dan sonra etkili olacağını unutmayın — bu callback yeni bir kurulumu doğru şekilde hazırlar; zaten NONE ile gönderilmiş bir uygulamanın, modları değiştirmek için bir kez VACUUM çalıştıran tek seferlik bir geçişe ihtiyacı vardır.

Bunların hepsinden daha çok önem taşıyan temizlik işi

VACUUM ve kademeli vakumlama, SQLite’ın zaten boş olduğunu bildiği alanı geri kazanır. Kendi şemanızın bilinçli olarak tuttuğu alan için hiçbir şey yapmazlar — bu, yerel öncelikli bir uygulamada şişmenin çok daha yaygın nedenidir. Geri alma ya da senkronizasyon için tutulan, hiçbir zaman kalıcı olarak silinmeyen bir yumuşak silme sütunu (isDeleted = true), ne kadar agresif vakumlarsanız vakumlayın, satırı ve alanını sonsuza kadar tutar.

Çözüm, yumuşak silme süre tanıma dönemini geçmiş satırları kalıcı olarak silen, sonra da bunun serbest bıraktığı sayfaları geri kazanan periyodik bir WorkManager işidir:

class DatabaseMaintenanceWorker(
    context: Context,
    params: WorkerParameters,
) : CoroutineWorker(context, params) {

    override suspend fun doWork(): Result {
        val cutoff = System.currentTimeMillis() - THIRTY_DAYS_MS
        val db = AppDatabase.getInstance(applicationContext)

        db.transactionDao().hardDeleteSoftDeletedBefore(cutoff)
        db.openHelper.writableDatabase.execSQL("PRAGMA incremental_vacuum(500)")

        return Result.success()
    }

    companion object {
        private const val THIRTY_DAYS_MS = 30L * 24 * 60 * 60 * 1000
    }
}

Bunu haftada bir, pil düşük değilken çalışacak bir kısıtlamayla, benzersiz periyodik iş olarak zamanlayın — bu bir temizlik, cihazı uyandırmaya değecek bir şey değil:

val request = PeriodicWorkRequestBuilder<DatabaseMaintenanceWorker>(7, TimeUnit.DAYS)
    .setConstraints(Constraints.Builder().setRequiresBatteryNotLow(true).build())
    .build()

WorkManager.getInstance(context).enqueueUniquePeriodicWork(
    "db_maintenance",
    ExistingPeriodicWorkPolicy.KEEP,
    request,
)

incremental_vacuum(500), hepsini tek seferde yapmaya çalışmak yerine, çalıştırma başına en fazla 500 sayfayı geri kazanır — bir kullanıcı hiç fark etmeden arka plan iş parçacığında haftalık çalıştırılacak kadar ucuz.

Çalışmanızı kontrol etmek

Bunlardan herhangi birinin işe yarayıp yaramadığını tahmin etmeyin — ölçün. PRAGMA page_count ve PRAGMA page_size çarpıldığında SQLite’ın kullandığı gerçek alanı verir, ve PRAGMA freelist_count size kaç sayfanın boş ama henüz dosya sistemine geri verilmemiş olduğunu söyler:

fun databaseStats(db: SupportSQLiteDatabase): Pair<Long, Long> {
    val pageCount = db.query("PRAGMA page_count").use { it.moveToFirst(); it.getLong(0) }
    val pageSize = db.query("PRAGMA page_size").use { it.moveToFirst(); it.getLong(0) }
    val freePages = db.query("PRAGMA freelist_count").use { it.moveToFirst(); it.getLong(0) }
    return (pageCount * pageSize) to (freePages * pageSize)
}

freelist_count sürüm sonra sürüm yüksek kalıyorsa, kademeli vakumlama yeterince sık çalışmıyordur. Boş liste düşük olmasına rağmen dosya boyutunun kendisi tırmanmaya devam ediyorsa, sorun hiç vakumlama değildir — kimsenin kalıcı olarak silmediği bir yumuşak silme sütunudur. Gerçekten bozuk olanı düzeltin; VACUUM’u daha sık çalıştırmak, kendi şemanızın hâlâ tuttuğu satırları temizlemeyecektir.