2026'da Room veritabanı index'leri: gerçekten yavaş olan sorguyu bulmak ve düzeltmek
Android'de Room/SQLite veritabanını index'lemeye pratik bir rehber — EXPLAIN QUERY PLAN okumak, tahmin etmeden @Index eklemek ve bir index'i sessizce etkisiz kılan hatalar.
Testte anında hissettiren, Room tabanlı bir uygulama aylar sonra takılmaya başlayabilir — kullanıcı gerçekte, sizin doldurduğunuz on iki yerine bin işlem biriktirdiğinde. Alışılmış içgüdü bir yere index eklemek ve umut etmektir. Bu, işe yaramadığı kadar işe yarar da, çünkü index’ler bir tabloyu hızlandırmaz — belirli bir erişim desenini hızlandırır, ve yanlış olanı hiçbir okuma faydası olmadan yazma yükü ekler. İşte gerçekten yavaş olan sorguyu nasıl bulacağınız, nasıl doğrulayacağınız ve doğru şekilde nasıl index’leyeceğiniz.
Tahmin etmeyin — ölçün
SQLite, sorgusunu bir planı sorarsanız size tam olarak nasıl çalıştıracağını söyler. Herhangi bir sorgunun önüne EXPLAIN QUERY PLAN ekleyin ve adb shell üzerinden ya da ham bir Room sorgusuyla çalıştırın:
@RawQuery
fun explain(query: SupportSQLiteQuery): List<ExplainRow>
EXPLAIN QUERY PLAN
SELECT * FROM transactions WHERE category_id = 7 ORDER BY date DESC;
Çıktı satırı hikayenin tamamıdır. SCAN transactions, SQLite’ın tablodaki her satırı okuduğu ve koşulu her birinde kontrol ettiği anlamına gelir — maliyet tablo boyutuyla doğrusal olarak büyür. SEARCH transactions USING INDEX idx_transactions_category (category_id=?) ise doğrudan eşleşen satırlara atladığı anlamına gelir. Index’lemeyle ilgili her şey, sık çalıştırdığınız sorgular için SCAN’i SEARCH’e çevirmeye ve geri kalan her şeyi olduğu gibi bırakmaya iniyor.
Burada kaçınılması gereken hata, hangi sütunun önemli hissettirdiğine göre index eklemektir. Bir notes sütunu nadiren filtrelenir; her liste ekranının WHERE cümlesinde kullanılan bir category_id tamamen farklı bir hikayedir. Herhangi bir şeye dokunmadan önce en sık çalışan beş altı DAO sorgunuzda EXPLAIN QUERY PLAN çalıştırın — ana liste ekranlarını oluşturan ve her uygulama açılışında çalışanlar.
Room’da index eklemek
Room, SQLite’ın CREATE INDEX’ini @Entity ek açıklamasının indices parametresi üzerinden açığa çıkarır:
@Entity(
tableName = "transactions",
indices = [Index(value = ["category_id"]), Index(value = ["date"])],
)
data class Transaction(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val categoryId: Long,
val date: Long,
val amountCents: Long,
)
Bu bir şema değişikliğidir, dolayısıyla bir sütun eklemekle aynı şekilde bir Migration gerektirir:
val MIGRATION_5_6 = object : Migration(5, 6) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("CREATE INDEX IF NOT EXISTS idx_transactions_category ON transactions(category_id)")
db.execSQL("CREATE INDEX IF NOT EXISTS idx_transactions_date ON transactions(date)")
}
}
Room’un şema dışa aktarımı (exportSchema = true artı room.schemaLocation derleyici argümanı), @Entity index’leriniz ile elle yazılmış bir migration birbirinden saparsa bunu işaretler — bu hatanın gönderilmeden önce yakalanmasının olağan yoludur.
Bileşik index tuzağı
category_id’ye göre filtreleyen ve date’e göre sıralayan bir sorgu — yukarıdaki tam desen — iki ayrı tek sütunlu index’ten tam fayda görmez. SQLite çoğu durumda sorgu başına tablo başına bir index kullanabilir, bu yüzden daha seçici olanı seçer ve yine de sonuçları bellekte sıralamak zorunda kalır. Her iki sütunu da, doğru sırayla kapsayan bileşik bir index, SQLite’ın hem filtre için index’i kullanmasına hem de satırları zaten sıralı halde döndürmesine izin verir:
indices = [Index(value = ["category_id", "date"])]
Burada sıra önemlidir. Bu index, WHERE category_id = ? ve WHERE category_id = ? ORDER BY date sorgularına hizmet eder, çünkü her iki koşul da index’i soldan sağa okur. Yalnızca date’e göre filtreleyen bir sorguya yardımcı olmaz — bunun için hâlâ date üzerinde tek sütunlu bir index’e, ya da önce date olan ikinci bir bileşik index’e ihtiyacınız olur. Bileşik bir index ekledikten sonra EXPLAIN QUERY PLAN’i tekrar kontrol edin; planda hâlâ USE TEMP B-TREE FOR ORDER BY görüyorsanız, sütun sırası sorgunun ihtiyacıyla eşleşmiyor demektir.
Index’ler bedava değildir
SQLite’ın koruduğu her index, index’lenmiş bir sütuna dokunan her INSERT, UPDATE ya da DELETE’te güncellenmek zorundadır. Bir bütçeleme uygulamasındaki transactions gibi bir tablo için, yazmalar okumalara göre görece nadirdir — günde bir avuç ekleme, düzinelerce liste render’ına karşı — bu yüzden takas kolaydır. Sürekli yazılan ve nadiren okunan bir tablo için (bir olay günlüğü, bir senkronizasyon kuyruğu) ise, transactions tablosuna yardımcı olan aynı üç index, kimsenin fark etmeyeceği bir okuma faydası için yazmaları ölçülebilir şekilde yavaşlatabilir. Şemadaki her tabloyu değil, her ekran açılışında sorgulanan tabloları index’leyin.
Birincil anahtar zaten örtük bir index alır — bunu indices içinde tekrar dahil etmek gereksizdir. Aynısı @PrimaryKey işaretli bir sütun ya da başka bir index’te zaten unique = true ilan ettiğiniz bir sütun için de geçerlidir; SQLite destekleyen index’i otomatik olarak oluşturur.
Bunun gerçekte önemli olduğu yer
Bunu sıkıcı yoldan, Granyn üzerinde buldum: birkaç yüz satırda anında hissettiren bir işlem listesi, gerçek kullanım birkaç bini geçtiğinde kategoriye göre filtrelerken görünür bir gecikme almaya başladı. DAO sorgusunda EXPLAIN QUERY PLAN, tam bir SCAN gösterdi — category_id filtresinin kullanabileceği bir index yoktu. Yukarıdaki bileşik index’i eklemek, filtrelenmiş sorguyu tam tablo taramasından index sorgusuna geri döndürdü ve takılma ortadan kalktı. Mimari değişikliği yok, yeni kütüphane yok, sadece bir migration’da doğru iki satır.
Ders bu tek tablonun ötesine genelleniyor: yerel öncelikli bir uygulama yavaşladığında sayfalamaya, önbelleğe ya da yeniden yazmaya uzanmadan önce, gerçekten yavaş olan sorguda EXPLAIN QUERY PLAN çalıştırın. Çoğu zaman çözüm bir index’tir, küçüktür ve yanlış yaparsanız geri alınabilir.
// İlgili okumalar
Günlükten dahası
2026'da Android'de Room TypeConverters: şemanı bozmadan enum, tarih ve liste saklamak
Android'de Room TypeConverters için pratik bir rehber — enum'lar, Instant/LocalDate ve listeler — ve bir converter'ı sessiz bir veri bozulması hatasına dönüştüren hatalar.
Room'un @Relation'ı: Android'de N+1 sorgu olmadan bire-çok veri sorgulamak
Room'un @Relation ek açıklamasına pratik bir rehber — kategoriler ve girişler gibi bire-çok veriyi N+1 sorgu ya da elle join olmadan modellemek.
Room'da tam metin arama: 2026'da yerel öncelikli bir Android uygulamasına anında arama eklemek
Room'un FTS4 desteğine pratik bir rehber — sanal bir arama tablosu kurmak, onu tetikleyicilerle senkron tutmak ve FTS5'in neden elle bir migration gerektirdiği.