SQLite performansıyla ilgili en iyi uygulamalar (Görünümler)

Kavramlar ve Jetpack Compose uygulaması

Android, verimli bir SQL veritabanı olan SQLite için yerleşik destek sunar. Uygulamanızın performansını optimize etmek için bu en iyi uygulamaları takip edin. Böylece, verileriniz arttıkça uygulamanızın hızlı ve öngörülebilir şekilde hızlı kalmasını sağlayabilirsiniz. Bu en iyi uygulamaları kullanarak, yeniden oluşturulması ve sorunlarının giderilmesi zor olan performans sorunlarıyla karşılaşma olasılığını da azaltırsınız.

Daha hızlı performans elde etmek için aşağıdaki performans ilkelerini uygulayın:

  • Daha az satır ve sütun okuma: Sorgularınızı yalnızca gerekli verileri alacak şekilde optimize edin. Veritabanından okunan veri miktarını en aza indirin. Aşırı veri alma işlemi performansı etkileyebilir.

  • İşleri SQLite motoruna aktarın: SQL sorgularında hesaplama, filtreleme ve sıralama işlemleri yapın. SQLite'ın sorgu motorunu kullanmak performansı önemli ölçüde artırabilir.

  • Veritabanı şemasını değiştirme: Veritabanı şemanızı, SQLite'ın verimli sorgu planları ve veri temsilleri oluşturmasına yardımcı olacak şekilde tasarlayın. Performansı artırmak için tabloları doğru şekilde dizine ekleyin ve tablo yapılarını optimize edin.

Ayrıca, optimizasyon gerektiren alanları belirlemek için mevcut sorun giderme araçlarını kullanarak SQLite veritabanınızın performansını ölçebilirsiniz.

Jetpack Room kitaplığını kullanmanızı öneririz.

Veritabanını performans için yapılandırma

SQLite'ta optimum performans için veritabanınızı yapılandırmak üzere bu bölümdeki adımları uygulayın.

Senkronizasyon modunu gevşetme

SQLite'ta eşzamanlı modu kontrol etme seçeneği vardır. FULL modunda, her kaydetme işlemi, verilerin diske ulaşmasını sağlamak için bir fsync yayınlar. Bu, veri dayanıklılığını artırır ancak kaydetme işlemlerinizi yavaşlatır. WAL ile NORMAL modu her kayıtta senkronize edilmez ve veritabanını bozulmaya karşı korur.

WAL'yi çerçeve üzerinden etkinleştirdiğinizde Android, cihazın varsayılan WAL eşzamanlı modunu uygular. Bu mod, Android 9 ve sonraki sürümler için AOSP'de NORMAL şeklindedir. Cihaz üreticileri bu varsayılanı değiştirebilir ve WAL kullanmayan veritabanları varsayılan olarak FULL değerini kullanır. Veritabanınızın NORMAL kullandığından emin olmak için veritabanını açarken bunu ayarlayın:

Kotlin

// When opening the database
val params = SQLiteDatabase.OpenParams.Builder()
    .addOpenFlags(SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING)
    // SYNC_MODE_NORMAL is available in API level 33 and higher.
    // On API levels 28 to 32, pass "NORMAL" instead.
    .setSynchronousMode(SQLiteDatabase.SYNC_MODE_NORMAL)
    .build()
// With SQLiteOpenHelper, call setOpenParams(params) in your constructor.

// Or: after having opened the database
db.execSQL("PRAGMA synchronous = NORMAL")

Java

// When opening the database
SQLiteDatabase.OpenParams params = new SQLiteDatabase.OpenParams.Builder()
    .addOpenFlags(SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING)
    // SYNC_MODE_NORMAL is available in API level 33 and higher.
    // On API levels 28 to 32, pass "NORMAL" instead.
    .setSynchronousMode(SQLiteDatabase.SYNC_MODE_NORMAL)
    .build();
// With SQLiteOpenHelper, call setOpenParams(params) in your constructor.

// Or: after having opened the database
db.execSQL("PRAGMA synchronous = NORMAL");

Bu ayarda, veriler bir diske depolanmadan önce bir commit döndürülebilir. Güç kaybı veya kernel panic gibi bir cihaz kapanması meydana gelirse kaydedilen veriler kaybolabilir. Ancak günlük kaydı sayesinde veritabanınız bozulmaz.

Yalnızca uygulamanız kilitlenirse verileriniz diske ulaşmaya devam eder. Çoğu uygulamada bu ayar, önemli bir maliyet olmadan performans iyileştirmeleri sağlar.

Sorgu performansını iyileştirme

Yanıt sürelerini en aza indirip işleme verimliliğini en üst düzeye çıkararak SQLite'ta sorgu performansını iyileştirmek için aşağıdaki en iyi uygulamaları izleyin.

Yalnızca ihtiyacınız olan satırları okuma

Filtreler, tarih aralığı, konum veya ad gibi belirli ölçütleri belirterek sonuçlarınızı daraltmanıza olanak tanır. Sınırlar, gördüğünüz sonuç sayısını kontrol etmenizi sağlar:

Kotlin

db.rawQuery("""
    SELECT name
    FROM Customers
    LIMIT 10;
    """.trimIndent(),
    null
).use { cursor ->
    while (cursor.moveToNext()) {
        // Process cursor data
    }
}

Java

try (Cursor cursor = db.rawQuery("""
    SELECT name
    FROM Customers
    LIMIT 10;
    """, null)) {
  while (cursor.moveToNext()) {
    // Process cursor data
  }
}

Yalnızca ihtiyacınız olan sütunları okuma

Sorgularınızı yavaşlatabileceği ve kaynakları boşa harcayabileceği için gereksiz sütunları seçmekten kaçının. Bunun yerine, yalnızca kullanılan sütunları seçin.

Aşağıdaki örnekte id, name ve phone'yi seçersiniz:

Kotlin

// This is not the most efficient way of doing this.
// See the following example for a better approach.

db.rawQuery(
    """
    SELECT id, name, phone
    FROM customers;
    """.trimIndent(),
    null
).use { cursor ->
    while (cursor.moveToNext()) {
        val name = cursor.getString(1)
        // Further processing
    }
}

Java

// This is not the most efficient way of doing this.
// See the following example for a better approach.

try (Cursor cursor = db.rawQuery("""
    SELECT id, name, phone
    FROM customers;
    """, null)) {
  while (cursor.moveToNext()) {
    String name = cursor.getString(1);
    // Further processing
  }
}

Ancak yalnızca name sütununa ihtiyacınız vardır:

Kotlin

db.rawQuery("""
    SELECT name
    FROM Customers;
    """.trimIndent(),
    null
).use { cursor ->
    while (cursor.moveToNext()) {
        val name = cursor.getString(0)
        // Further processing
    }
}

Java

try (Cursor cursor = db.rawQuery("""
    SELECT name
    FROM Customers;
    """, null)) {
  while (cursor.moveToNext()) {
    String name = cursor.getString(0);
    // Further processing
  }
}

Sorguları parametreleştirme

Sorgu dizeniz, yalnızca çalışma zamanında bilinen bir parametre içerebilir. Örneğin:

Kotlin

fun getNameById(id: Long): String? {
    db.rawQuery(
        "SELECT name FROM customers WHERE id=$id", null
    ).use { cursor ->
        return if (cursor.moveToFirst()) {
            cursor.getString(0)
        } else {
            null
        }
    }
}

Java

@Nullable
public String getNameById(long id) {
  try (Cursor cursor = db.rawQuery(
      "SELECT name FROM customers WHERE id=" + id, null)) {
    if (cursor.moveToFirst()) {
      return cursor.getString(0);
    } else {
      return null;
    }
  }
}

Önceki kodda her sorgu farklı bir dize oluşturur ve bu nedenle ifade önbelleğinden yararlanmaz. Her çağrının yürütülebilmesi için SQLite'ın çağrıyı derlemesi gerekir. Bunun yerine, id bağımsız değişkenini bir parametre ile değiştirebilir ve değeri selectionArgs ile bağlayabilirsiniz:

Kotlin

fun getNameById(id: Long): String? {
    db.rawQuery(
        """
          SELECT name
          FROM customers
          WHERE id=?
        """.trimIndent(), arrayOf(id.toString())
    ).use { cursor ->
        return if (cursor.moveToFirst()) {
            cursor.getString(0)
        } else {
            null
        }
    }
}

Java

@Nullable
public String getNameById(long id) {
  try (Cursor cursor = db.rawQuery("""
          SELECT name
          FROM customers
          WHERE id=?
      """, new String[] {String.valueOf(id)})) {
    if (cursor.moveToFirst()) {
      return cursor.getString(0);
    } else {
      return null;
    }
  }
}

Artık sorgu bir kez derlenip önbelleğe alınabilir. Derlenen sorgu, getNameById(long) öğesinin farklı çağrıları arasında yeniden kullanılır.

Benzersiz değerler için DISTINCT kullanın

DISTINCT anahtar kelimesini kullanmak, işlenmesi gereken veri miktarını azaltarak sorgularınızın performansını artırabilir. Örneğin, bir sütundaki yalnızca benzersiz değerleri döndürmek istiyorsanız DISTINCT işlevini kullanın:

Kotlin

db.rawQuery("""
    SELECT DISTINCT name
    FROM Customers;
    """.trimIndent(),
    null
).use { cursor ->
    while (cursor.moveToNext()) {
        // Only iterate over distinct names in Kotlin
        // Process distinct name
    }
}

Java

try (Cursor cursor = db.rawQuery("""
    SELECT DISTINCT name
    FROM Customers;
    """, null)) {
  while (cursor.moveToNext()) {
    // Only iterate over distinct names in Java
    // Process distinct name
  }
}

Mümkün olduğunda topla işlevlerini kullanın

Satır verileri olmadan toplu sonuçlar için topla işlevlerini kullanın. Örneğin, aşağıdaki kod en az bir eşleşen satır olup olmadığını kontrol eder:

Kotlin

// This is not the most efficient way of doing this.
// See the following example for a better approach.

db.rawQuery("""
    SELECT id, name
    FROM Customers
    WHERE city = 'Paris';
    """.trimIndent(),
    null
).use { cursor ->
    if (cursor.moveToFirst()) {
        // At least one customer from Paris
        // Handle found
    } else {
        // No customers from Paris
        // Handle not found
}

Java

// This is not the most efficient way of doing this.
// See the following example for a better approach.

try (Cursor cursor = db.rawQuery("""
    SELECT id, name
    FROM Customers
    WHERE city = 'Paris';
    """, null)) {
  if (cursor.moveToFirst()) {
    // At least one customer from Paris
    // Handle found
  } else {
    // No customers from Paris
    // Handle not found
  }
}

Yalnızca ilk satırı getirmek için EXISTS() işlevini kullanarak eşleşen bir satır yoksa 0, bir veya daha fazla satır eşleşiyorsa 1 değerini döndürebilirsiniz:

Kotlin

db.rawQuery("""
    SELECT EXISTS (
        SELECT null
        FROM Customers
        WHERE city = 'Paris';
    );
    """.trimIndent(),
    null
).use { cursor ->
    if (cursor.moveToFirst() && cursor.getInt(0) == 1) {
        // At least one customer from Paris
        // Handle found
    } else {
        // No customers from Paris
        // Handle not found
    }
}

Java

try (Cursor cursor = db.rawQuery("""
    SELECT EXISTS (
      SELECT null
      FROM Customers
      WHERE city = 'Paris'
    );
    """, null)) {
  if (cursor.moveToFirst() && cursor.getInt(0) == 1) {
    // At least one customer from Paris
    // Handle found
  } else {
    // No customers from Paris
    // Handle not found
  }
}

Uygulama kodunuzda SQLite toplama işlevlerini kullanma:

  • COUNT: Bir sütunda kaç satır olduğunu sayar.
  • SUM: Bir sütundaki tüm sayısal değerleri toplar.
  • MIN veya MAX: En düşük veya en yüksek değeri belirler. Sayısal sütunlar, DATE türleri ve metin türleri için kullanılabilir.
  • AVG: Ortalama sayısal değeri bulur.
  • GROUP_CONCAT: Dizeleri isteğe bağlı bir ayırıcıyla birleştirir.

Cursor.getCount() yerine COUNT() kullanın

Aşağıdaki örnekte, Cursor.getCount() işlevi veritabanındaki tüm satırları okur ve tüm satır değerlerini döndürür:

Kotlin

// This is not the most efficient way of doing this.
// See the following example for a better approach.

db.rawQuery("""
    SELECT id
    FROM Customers;
    """.trimIndent(),
    null
).use { cursor ->
    val count = cursor.getCount()
    // Use count
}

Java

// This is not the most efficient way of doing this.
// See the following example for a better approach.

try (Cursor cursor = db.rawQuery("""
    SELECT id
    FROM Customers;
    """, null)) {
  int count = cursor.getCount();
  // Use count
}

Ancak COUNT() kullanıldığında veritabanı yalnızca sayıyı döndürür:

Kotlin

db.rawQuery("""
    SELECT COUNT(*)
    FROM Customers;
    """.trimIndent(),
    null
).use { cursor ->
    cursor.moveToFirst()
    val count = cursor.getInt(0)
    // Use count
}

Java

try (Cursor cursor = db.rawQuery("""
    SELECT COUNT(*)
    FROM Customers;
    """, null)) {
  cursor.moveToFirst();
  int count = cursor.getInt(0);
  // Use count
}

Kod yerine sorguları iç içe yerleştirme

SQL, birleştirilebilir ve alt sorguları, birleştirmeleri ve yabancı anahtar kısıtlamalarını destekler. Bir sorgunun sonucunu, uygulama kodunu kullanmadan başka bir sorguda kullanabilirsiniz. Bu sayede SQLite'tan veri kopyalama ihtiyacı azalır ve veritabanı motoru sorgunuzu optimize edebilir.

Aşağıdaki örnekte, en çok müşterinin hangi şehirde olduğunu bulmak için bir sorgu çalıştırabilir, ardından sonucu başka bir sorguda kullanarak o şehirdeki tüm müşterileri bulabilirsiniz:

Kotlin

// This is not the most efficient way of doing this.
// See the following example for a better approach.

db.rawQuery("""
    SELECT city
    FROM Customers
    GROUP BY city
    ORDER BY COUNT(*) DESC
    LIMIT 1;
    """.trimIndent(),
    null
).use { cursor ->
    if (cursor.moveToFirst()) {
        val topCity = cursor.getString(0)
        db.rawQuery("""
            SELECT name, city
            FROM Customers
            WHERE city = ?;
        """.trimIndent(),
        arrayOf(topCity)).use { innerCursor ->
            while (innerCursor.moveToNext()) {
                // Process inner cursor data
            }
        }
    }
}

Java

// This is not the most efficient way of doing this.
// See the following example for a better approach.

try (Cursor cursor = db.rawQuery("""
    SELECT city
    FROM Customers
    GROUP BY city
    ORDER BY COUNT(*) DESC
    LIMIT 1;
    """, null)) {
  if (cursor.moveToFirst()) {
    String topCity = cursor.getString(0);
    try (Cursor innerCursor = db.rawQuery("""
        SELECT name, city
        FROM Customers
        WHERE city = ?;
        """, new String[] {topCity})) {
        while (innerCursor.moveToNext()) {
          // Process inner cursor data
        }
    }
  }
}

Sonucu önceki örneğin yarısı kadar sürede almak için iç içe yerleştirilmiş ifadeler içeren tek bir SQL sorgusu kullanın:

Kotlin

db.rawQuery("""
    SELECT name, city
    FROM Customers
    WHERE city IN (
        SELECT city
        FROM Customers
        GROUP BY city
        ORDER BY COUNT (*) DESC
        LIMIT 1;
    );
    """.trimIndent(),
    null
).use { cursor ->
    if (cursor.moveToNext()) {
        // Process cursor data
    }
}

Java

try (Cursor cursor = db.rawQuery("""
    SELECT name, city
    FROM Customers
    WHERE city IN (
      SELECT city
      FROM Customers
      GROUP BY city
      ORDER BY COUNT(*) DESC
      LIMIT 1
    );
    """, null)) {
  while(cursor.moveToNext()) {
    // Process cursor data
  }
}

SQL'de benzersizliği kontrol etme

Bir satırın, tablodaki belirli bir sütun değeri benzersiz olmadığı sürece eklenmemesi gerekiyorsa bu benzersizliği sütun kısıtlaması olarak zorunlu kılmak daha verimli olabilir.

Aşağıdaki örnekte, eklenecek satırı doğrulamak için bir sorgu, ekleme işlemini gerçekleştirmek için başka bir sorgu çalıştırılır:

Kotlin

// This is not the most efficient way of doing this.
// See the following example for a better approach.

db.rawQuery(
    """
    SELECT EXISTS (
        SELECT null
        FROM customers
        WHERE username = ?
    );
    """.trimIndent(),
    arrayOf(customer.username)
).use { cursor ->
    if (cursor.moveToFirst() && cursor.getInt(0) == 1) {
        throw AddCustomerException(customer)
    }
}
db.execSQL(
    "INSERT INTO customers VALUES (?, ?, ?)",
    arrayOf(
        customer.id.toString(),
        customer.name,
        customer.username
    )
)

Java

// This is not the most efficient way of doing this.
// See the following example for a better approach.

try (Cursor cursor = db.rawQuery("""
    SELECT EXISTS (
      SELECT null
      FROM customers
      WHERE username = ?
    );
    """, new String[] { customer.username })) {
  if (cursor.moveToFirst() && cursor.getInt(0) == 1) {
    throw new AddCustomerException(customer);
  }
}
db.execSQL(
    "INSERT INTO customers VALUES (?, ?, ?)",
    new String[] {
      String.valueOf(customer.id),
      customer.name,
      customer.username,
    });

Benzersizlik kısıtlamasını Kotlin veya Java'da kontrol etmek yerine, tabloyu tanımlarken SQL'de kontrol edebilirsiniz:

CREATE TABLE Customers(
  id INTEGER PRIMARY KEY,
  name TEXT,
  username TEXT UNIQUE
);

SQLite, aşağıdakilerle aynı işlemi yapar:

CREATE TABLE Customers(...);
CREATE UNIQUE INDEX CustomersUsername ON Customers(username);

Şimdi bir satır ekleyip SQLite'ın kısıtlamayı kontrol etmesini sağlayabilirsiniz:

Kotlin

try {
    db.execSQL(
        "INSERT INTO Customers VALUES (?, ?, ?)",
        arrayOf(customer.id.toString(), customer.name, customer.username)
    )
} catch(e: SQLiteConstraintException) {
    throw AddCustomerException(customer, e)
}

Java

try {
  db.execSQL(
      "INSERT INTO Customers VALUES (?, ?, ?)",
      new String[] {
        String.valueOf(customer.id),
        customer.name,
        customer.username,
      });
} catch (SQLiteConstraintException e) {
  throw new AddCustomerException(customer, e);
}

SQLite, birden çok sütun içeren benzersiz dizinleri destekler:

CREATE TABLE table(...);
CREATE UNIQUE INDEX unique_table ON table(column1, column2, ...);

SQLite, kısıtlamaları Kotlin veya Java kodundan daha hızlı ve daha az ek yükle doğrular. Uygulama kodu yerine SQLite kullanmak en iyi uygulamadır.

Tek bir işlemde birden fazla ekleme işlemini toplu olarak gerçekleştirme

İşlem, birden fazla işlemi yürütür. Bu sayede hem verimlilik hem de doğruluk artar. Veri tutarlılığını artırmak ve performansı hızlandırmak için ekleme işlemlerini toplu olarak yapabilirsiniz:

Kotlin

db.beginTransaction()
try {
    customers.forEach { customer ->
        db.execSQL(
            "INSERT INTO Customers VALUES (?, ?, ?)",
            arrayOf(customer.id.toString(), customer.name, "customerValue")
        )
    }
    db.setTransactionSuccessful()
} finally {
    db.endTransaction()
}

Java

db.beginTransaction();
try {
  for (Customer customer : customers) {
    db.execSQL(
        "INSERT INTO Customers VALUES (?, ?, ?)",
        new String[] {
          String.valueOf(customer.id),
          customer.name,
          "customerValue"
        });
  }
  db.setTransactionSuccessful();
} finally {
  db.endTransaction();
}