Best Practices für die SQLite-Leistung (Ansichten)

Konzepte und Jetpack Compose-Implementierung

Android bietet integrierte Unterstützung für SQLite, eine effiziente SQL-Datenbank. Beachten Sie diese Best Practices, um die Leistung Ihrer App zu optimieren und dafür zu sorgen, dass sie auch bei wachsenden Datenmengen schnell und zuverlässig bleibt. Wenn Sie diese Best Practices anwenden, verringern Sie auch die Wahrscheinlichkeit, auf Leistungsprobleme zu stoßen, die schwer zu reproduzieren und zu beheben sind.

So erzielen Sie eine höhere Leistung:

  • Weniger Zeilen und Spalten lesen: Optimieren Sie Ihre Abfragen, um nur die erforderlichen Daten abzurufen. Minimieren Sie die Menge der aus der Datenbank gelesenen Daten, da ein übermäßiger Datenabruf die Leistung beeinträchtigen kann.

  • Aufgaben an die SQLite-Engine übertragen: Führen Sie Berechnungen, Filterungen und Sortierungen in den SQL-Abfragen durch. Die Verwendung der Abfrage-Engine von SQLite kann die Leistung erheblich verbessern.

  • Datenbankschema ändern: Entwerfen Sie Ihr Datenbankschema so, dass SQLite effiziente Abfragepläne und Datendarstellungen erstellen kann. Indexieren Sie Tabellen richtig und optimieren Sie Tabellenstrukturen, um die Leistung zu verbessern.

Außerdem können Sie die verfügbaren Tools zur Fehlerbehebung verwenden, um die Leistung Ihrer SQLite-Datenbank zu messen und Bereiche zu identifizieren, die optimiert werden müssen.

Wir empfehlen die Verwendung der Jetpack Room-Bibliothek.

Datenbank für Leistung konfigurieren

Folgen Sie der Anleitung in diesem Abschnitt, um Ihre Datenbank für optimale Leistung in SQLite zu konfigurieren.

Synchronisierungsmodus lockern

Bei Verwendung von WAL wird standardmäßig bei jedem Commit ein fsync ausgegeben, um sicherzustellen, dass die Daten auf das Laufwerk geschrieben werden. Dadurch wird die Datenbeständigkeit verbessert, aber die Commits werden verlangsamt.

SQLite bietet eine Option zum Steuern des synchronen Modus. Wenn Sie WAL aktivieren, setzen Sie den synchronen Modus auf NORMAL:

Kotlin

// When opening the database
val paramsBuilder: SQLiteDatabase.OpenParams.Builder = SQLiteDatabase.OpenParams.Builder()
paramsBuilder.journalMode = SQLiteDatabase.SYNC_MODE_NORMAL

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

Java

// When opening the database
SQLiteDatabase.OpenParams.Builder paramsBuilder = new SQLiteDatabase.OpenParams.Builder();
paramsBuilder.setJournalMode(SQLiteDatabase.SYNC_MODE_NORMAL);

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

Bei dieser Einstellung kann ein Commit zurückgegeben werden, bevor die Daten auf einer Festplatte gespeichert sind. Wenn ein Gerät heruntergefahren wird, z. B. bei einem Stromausfall oder einer Kernel-Panik, können die übertragenen Daten verloren gehen. Aufgrund der Protokollierung wird Ihre Datenbank jedoch nicht beschädigt.

Wenn nur Ihre App abstürzt, werden Ihre Daten trotzdem auf die Festplatte geschrieben. Bei den meisten Apps führt diese Einstellung zu Leistungsverbesserungen ohne nennenswerte Kosten.

Abfrageleistung verbessern

Beachten Sie diese Best Practices, um die Abfrageleistung in SQLite zu verbessern, indem Sie die Antwortzeiten minimieren und die Verarbeitungseffizienz maximieren.

Nur die benötigten Zeilen lesen

Mit Filtern können Sie die Ergebnisse eingrenzen, indem Sie bestimmte Kriterien wie Datumsbereich, Standort oder Name angeben. Mit Limits können Sie die Anzahl der angezeigten Ergebnisse steuern:

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
  }
}

Nur die benötigten Spalten lesen

Vermeiden Sie es, unnötige Spalten auszuwählen, da dies Ihre Abfragen verlangsamen und Ressourcen verschwenden kann. Wählen Sie stattdessen nur die verwendeten Spalten aus.

Im folgenden Beispiel wählen Sie id, name und phone aus:

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
  }
}

Sie benötigen jedoch nur die Spalte name:

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
  }
}

Abfragen parametrisieren

Ihr Abfragestring kann einen Parameter enthalten, der erst zur Laufzeit bekannt ist, z. B.:

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;
    }
  }
}

Im vorherigen Code erstellt jede Abfrage einen anderen String und profitiert daher nicht vom Anweisungscache. Bei jedem Aufruf muss SQLite die Abfrage kompilieren, bevor sie ausgeführt werden kann. Stattdessen können Sie das Argument id durch einen Parameter ersetzen und den Wert mit selectionArgs binden:

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;
    }
  }
}

Jetzt kann die Abfrage einmal kompiliert und im Cache gespeichert werden. Die kompilierte Abfrage wird bei verschiedenen Aufrufen von getNameById(long) wiederverwendet.

DISTINCT für eindeutige Werte verwenden

Die Verwendung des Schlüsselworts DISTINCT kann die Leistung Ihrer Abfragen verbessern, indem die Menge der zu verarbeitenden Daten reduziert wird. Wenn Sie beispielsweise nur die eindeutigen Werte aus einer Spalte zurückgeben möchten, verwenden Sie DISTINCT:

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
  }
}

Aggregatfunktionen nach Möglichkeit verwenden

Verwenden Sie Aggregatfunktionen für aggregierte Ergebnisse ohne Zeilendaten. Der folgende Code prüft beispielsweise, ob mindestens eine übereinstimmende Zeile vorhanden ist:

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
  }
}

Wenn Sie nur die erste Zeile abrufen möchten, können Sie mit EXISTS() 0 zurückgeben, wenn keine übereinstimmende Zeile vorhanden ist, und 1, wenn eine oder mehrere Zeilen übereinstimmen:

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
  }
}

Verwenden Sie SQLite-Aggregatfunktionen in Ihrem App Code:

  • COUNT: Zählt die Anzahl der Zeilen in einer Spalte.
  • SUM: Addiert alle numerischen Werte in einer Spalte.
  • MIN oder MAX: Bestimmt den niedrigsten oder höchsten Wert. Funktioniert für numerische Spalten, DATE-Typen und Texttypen.
  • AVG: Berechnet den durchschnittlichen numerischen Wert.
  • GROUP_CONCAT: Verkettet Strings mit einem optionalen Trennzeichen.

COUNT() anstelle von Cursor.getCount() verwenden

Im folgenden Beispiel liest die Cursor.getCount() Funktion alle Zeilen aus der Datenbank und gibt alle Zeilenwerte zurück:

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
}

Wenn Sie jedoch COUNT() verwenden, gibt die Datenbank nur die Anzahl zurück:

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
}

Abfragen anstelle von Code verschachteln

SQL ist zusammensetzbar und unterstützt Unterabfragen, Joins und Fremdschlüsseleinschränkungen. Sie können das Ergebnis einer Abfrage in einer anderen Abfrage verwenden, ohne App-Code zu verwenden. Dadurch müssen keine Daten aus SQLite kopiert werden und die Datenbank-Engine kann Ihre Abfrage optimieren.

Im folgenden Beispiel können Sie eine Abfrage ausführen, um herauszufinden, in welcher Stadt die meisten Kunden sind, und das Ergebnis dann in einer anderen Abfrage verwenden, um alle Kunden aus dieser Stadt zu finden:

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
        }
    }
  }
}

Wenn Sie das Ergebnis in der Hälfte der Zeit des vorherigen Beispiels erhalten möchten, verwenden Sie eine einzelne SQL-Abfrage mit verschachtelten Anweisungen:

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
  }
}

Eindeutigkeit in SQL prüfen

Wenn eine Zeile nur eingefügt werden darf, wenn ein bestimmter Spaltenwert in der Tabelle eindeutig ist, ist es möglicherweise effizienter, diese Eindeutigkeit als Spalteneinschränkung zu erzwingen.

Im folgenden Beispiel wird eine Abfrage ausgeführt, um die einzufügende Zeile zu validieren, und eine weitere, um sie tatsächlich einzufügen:

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,
    });

Anstatt die eindeutige Einschränkung in Kotlin oder Java zu prüfen, können Sie sie in SQL prüfen, wenn Sie die Tabelle definieren:

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

SQLite führt dasselbe aus wie der folgende Code:

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

Jetzt können Sie eine Zeile einfügen und SQLite die Einschränkung prüfen lassen:

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 unterstützt eindeutige Indexe mit mehreren Spalten:

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

SQLite validiert Einschränkungen schneller und mit weniger Aufwand als Kotlin- oder Java-Code. Es ist eine Best Practice, SQLite anstelle von App-Code zu verwenden.

Mehrere Einfügungen in einer einzelnen Transaktion zusammenfassen

Bei einer Transaktion werden mehrere Vorgänge zusammen ausgeführt, was nicht nur die Effizienz, sondern auch die Korrektheit verbessert. Um die Datenkonsistenz zu verbessern und die Leistung zu beschleunigen, können Sie Einfügungen zusammenfassen:

Kotlin

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

Java

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