Best Practices für die SQLite-Leistung

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 steigenden Datenmengen schnell und zuverlässig bleibt. Wenn Sie diese Best Practices beachten, verringern Sie auch die Wahrscheinlichkeit, dass Leistungsprobleme auftreten, die sich nur schwer reproduzieren und beheben lassen.

Beachten Sie die folgenden Leistungsprinzipien, um die Leistung zu steigern:

  • Weniger Zeilen und Spalten lesen: Optimieren Sie Ihre Abfragen, damit nur die erforderlichen Daten abgerufen werden. Minimieren Sie die Menge der aus der Datenbank gelesenen Daten, da sich ein übermäßiger Datenabruf auf die Leistung auswirken kann.

  • Arbeit an die SQLite-Engine übergeben: Führen Sie Berechnungen, Filter- und Sortiervorgänge in den SQL-Abfragen aus. 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. Tabellen richtig indexieren und Tabellenstrukturen optimieren, um die Leistung zu verbessern.

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

Wir empfehlen die Verwendung der Jetpack Room-Bibliothek.

Datenbank für die Leistung konfigurieren

Führen Sie die Schritte in diesem Abschnitt aus, um Ihre Datenbank für eine optimale Leistung in SQLite zu konfigurieren.

Write-Ahead-Logging aktivieren

SQLite implementiert Mutationen, indem sie an ein Protokoll angehängt werden, das gelegentlich in die Datenbank komprimiert wird. Dies wird als Write-Ahead-Logging (WAL) bezeichnet.

Aktivieren Sie WAL, es sei denn, Sie verwenden ATTACH DATABASE.

Synchronisierungsmodus lockern

Bei der Verwendung von WAL wird standardmäßig bei jedem Commit eine fsync ausgegeben, um sicherzustellen, dass die Daten das Laufwerk erreichen. Dies verbessert die Datenausfallsicherheit, verlangsamt aber Ihre Commits.

SQLite bietet eine Option zum Stellen des synchronen Modus. Wenn Sie WAL aktivieren, legen Sie den synchronen Modus auf NORMAL fest:

Kotlin

db.execSQL("PRAGMA synchronous = NORMAL")

Java

db.execSQL("PRAGMA synchronous = NORMAL");

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

Wenn nur Ihre App abstürzt, gelangen Ihre Daten trotzdem auf das Laufwerk. Bei den meisten Apps führt diese Einstellung zu Leistungsverbesserungen ohne nennenswerte Kosten.

Effizientere Tabellenschemas definieren

Definieren Sie ein effizientes Tabellenschema, um die Leistung zu optimieren und den Datenverbrauch zu minimieren. SQLite erstellt effiziente Abfragepläne und Daten, was zu einem schnelleren Abrufen von Daten führt. Dieser Abschnitt enthält Best Practices zum Erstellen von Tabellenschemata.

Achte auf INTEGER PRIMARY KEY

Definieren und füllen Sie für dieses Beispiel eine Tabelle so aus:

CREATE TABLE Customers(
  id INTEGER,
  name TEXT,
  city TEXT
);
INSERT INTO Customers Values(456, 'John Lennon', 'Liverpool, England');
INSERT INTO Customers Values(123, 'Michael Jackson', 'Gary, IN');
INSERT INTO Customers Values(789, 'Dolly Parton', 'Sevier County, TN');

Die Tabellenausgabe sieht so aus:

rowid id Name Stadt
1 456 John Lennon Liverpool, England
2 123 Michael Jackson Gary, IN
3 789 Dolly Parton Sevier County, Tennessee

Die Spalte rowid ist ein Index, der den Einfügevorgang beibehält. Abfragen, die nach rowid filtern, werden als schnelle B-Baumsuche implementiert. Abfragen, die nach id filtern, sind jedoch ein langsamer Tabellenscan.

Wenn Sie nach id suchen möchten, können Sie das Speichern der Spalte rowid vermeiden. So wird weniger Speicherplatz belegt und die Datenbank insgesamt schneller:

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

Ihre Tabelle sieht jetzt so aus:

id Name Stadt
123 Michael Jackson Gary, IN
456 John Lennon Liverpool, England
789 Dolly Parton Sevier County, Tennessee

Da die Spalte rowid nicht gespeichert werden muss, sind id-Abfragen schnell. Die Tabelle wird jetzt nach id statt nach Anzeigenauftrag sortiert.

Abfragen mit Indexen beschleunigen

SQLite verwendet Indexe, um Abfragen zu beschleunigen. Wenn Sie eine Spalte filtern (WHERE), sortieren (ORDER BY) oder aggregieren (GROUP BY) und die Tabelle einen Index für die Spalte hat, wird die Abfrage beschleunigt.

Im vorherigen Beispiel muss die gesamte Tabelle gescannt werden, um nach city zu filtern:

SELECT id, name
WHERE city = 'London, England';

Bei einer App mit vielen Abfragen nach Städten können Sie diese Abfragen mit einem Index beschleunigen:

CREATE INDEX city_index ON Customers(city);

Ein Index wird als zusätzliche Tabelle implementiert, nach der Indexspalte sortiert und rowid zugeordnet:

Stadt rowid
Gary, IN 2
Liverpool, England 1
Sevier County, Tennessee 3

Die Speicherkosten für die Spalte city sind jetzt doppelt so hoch, da sie jetzt sowohl in der ursprünglichen Tabelle als auch im Index vorhanden ist. Da Sie den Index verwenden, sind die zusätzlichen Speicherkosten die schnelleren Abfragen wert. Behalten Sie jedoch keinen Index bei, den Sie nicht verwenden, um Speicherkosten zu vermeiden, ohne dass sich die Abfrageleistung verbessert.

Mehrspaltige Indexe erstellen

Wenn Sie in Ihren Abfragen mehrere Spalten kombinieren, können Sie Indexe mit mehreren Spalten erstellen, um die Abfrage zu beschleunigen. Sie können auch einen Index für eine äußere Spalte verwenden und die interne Suche als linearen Scan ausführen lassen.

Beispiel:

SELECT id, name
WHERE city = 'London, England'
ORDER BY city, name

Sie können die Abfrage mit einem Index mit mehreren Spalten in der Reihenfolge beschleunigen, die in der Abfrage angegeben ist:

CREATE INDEX city_name_index ON Customers(city, name);

Wenn Sie jedoch nur einen Index für city haben, wird die äußere Sortierung trotzdem beschleunigt, während für die innere Sortierung ein linearer Scan erforderlich ist.

Das funktioniert auch bei Suchanfragen mit Präfix. Ein Index ON Customers (city, name) beschleunigt beispielsweise auch das Filtern, Sortieren und Gruppieren nach city, da die Indextabelle für einen mehrspaltigen Index nach den angegebenen Indexen in der angegebenen Reihenfolge sortiert wird.

Achte auf WITHOUT ROWID

Standardmäßig erstellt SQLite eine rowid-Spalte für Ihre Tabelle, wobei rowid ein impliziter INTEGER PRIMARY KEY AUTOINCREMENT ist. Wenn Sie bereits eine Spalte mit dem Namen INTEGER PRIMARY KEY haben, wird diese Spalte zu einem Alias von rowid.

Für Tabellen mit einem anderen Primärschlüssel als INTEGER oder einem zusammengesetzten Schlüssel aus mehreren Spalten können Sie WITHOUT ROWID verwenden.

Kleine Daten als BLOB und große Daten als Datei speichern

Wenn Sie einer Zeile große Daten zuordnen möchten, z. B. ein Thumbnail eines Bilds oder ein Foto für einen Kontakt, können Sie die Daten entweder in einer BLOB-Spalte oder in einer Datei speichern und dann den Dateipfad in der Spalte speichern.

Dateien werden in der Regel auf 4 KB aufgerundet. Bei sehr kleinen Dateien, bei denen der Rundungsfehler erheblich ist, ist es effizienter, sie in der Datenbank als BLOB zu speichern. SQLite minimiert Dateisystemaufrufe und ist in einigen Fällen schneller als das zugrunde liegende Dateisystem.

Abfrageleistung verbessern

Mit diesen Best Practices können Sie die Abfrageleistung in SQLite 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 Zeitraum, Standort oder Name angeben. Mithilfe von 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()) {
        ...
    }
}

Java

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

Nur die benötigten Spalten lesen

Wählen Sie keine nicht benötigten Spalten aus, da dies Ihre Abfragen verlangsamen und Ressourcen verschwenden kann. Wählen Sie stattdessen nur Spalten aus, die verwendet werden.

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)
        // ...
    }
}

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

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

Java

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

Abfragen mit SQL-Karten und nicht mit Stringkonkatenierung 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 obigen Code wird für jede Abfrage ein anderer String erstellt, sodass der Anweisungscache nicht genutzt werden kann. Jeder Aufruf muss von SQLite kompiliert werden, bevor er ausgeführt werden kann. Stattdessen können Sie das id-Argument durch einen Parameter ersetzen und den Wert mit selectionArgs verknüpfen:

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.

Iterationen in SQL, nicht im Code

Verwenden Sie eine einzelne Abfrage, die alle gewünschten Ergebnisse zurückgibt, anstelle einer programmatischen Schleife, die SQL-Abfragen iteriert, um einzelne Ergebnisse zurückzugeben. Die programmatische Schleife ist etwa 1.000-mal langsamer als eine einzelne SQL-Abfrage.

DISTINCT für eindeutige Werte verwenden

Mit dem Keyword DISTINCT lässt sich die Leistung Ihrer Abfragen verbessern, da die zu verarbeitende Datenmenge 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
        ...
    }
}

Java

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

Verwenden Sie nach Möglichkeit Aggregatfunktionen.

Verwenden Sie Aggregatfunktionen für aggregierte Ergebnisse ohne Zeilendaten. Im folgenden Code wird beispielsweise geprüft, 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
        ...
    } else {
        // No customers from Paris
        ...
}

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
    ...
  } else {
    // No customers from Paris
    ...
  }
}

Wenn Sie nur die erste Zeile abrufen möchten, können Sie EXISTS() verwenden, um 0 zurückzugeben, 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
        ...
    } else {
        // No customers from Paris
        ...
    }
}

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
    ...
  } else {
    // No customers from Paris
    ...
  }
}

Verwenden Sie SQLite-Aggregatfunktionen in Ihrem App-Code:

  • COUNT: Zählt die Anzahl der Zeilen in einer Spalte.
  • SUM: Hiermit werden alle numerischen Werte in einer Spalte addiert.
  • MIN oder MAX: Bestimmt den niedrigsten oder höchsten Wert. Funktioniert für numerische Spalten, DATE-Typen und Texttypen.
  • AVG: Der Mittelwert wird berechnet.
  • GROUP_CONCAT: Strings werden mit einem optionalen Trennzeichen verknüpft.

COUNT() anstelle von Cursor.getCount() verwenden

Im folgenden Beispiel liest die Funktion Cursor.getCount() 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()
}

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();
  ...
}

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

Java

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

Nest-Abfragen anstelle von Code

SQL ist kombinierbar und unterstützt Unterabfragen, Joins und Fremdschlüsseleinschränkungen. Sie können das Ergebnis einer Abfrage in einer anderen Abfrage verwenden, ohne den App-Code durchgehen zu müssen. Dadurch müssen weniger 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 dann das Ergebnis 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()) {
                ...
            }
        }
    }
}

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()) {
          ...
        }
    }
  }
}

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()) {
        ...
    }
}

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()) {
    ...
  }
}

Eindeutigkeit in SQL prüfen

Wenn eine Zeile nur dann 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 Folgendes aus:

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 Overhead als Kotlin- oder Java-Code. Es empfiehlt sich, SQLite anstelle von App-Code zu verwenden.

Mehrere Einträge in einer einzigen Transaktion einfügen

Mit einer Transaktion werden mehrere Vorgänge verbindlich 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 mehrere Einträge gleichzeitig einfügen:

Kotlin

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

Java

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

Tools zur Fehlerbehebung verwenden

SQLite bietet die folgenden Tools zur Fehlerbehebung, mit denen Sie die Leistung messen können.

Interaktiven Prompt von SQLite verwenden

Führen Sie SQLite auf Ihrem Computer aus, um Abfragen auszuführen und mehr zu erfahren. Für verschiedene Android-Plattformversionen werden unterschiedliche SQLite-Versionen verwendet. Wenn Sie dieselbe Engine verwenden möchten, die auf einem Android-Gerät verwendet wird, verwenden Sie adb shell und führen Sie sqlite3 auf Ihrem Zielgerät aus.

Sie können SQLite bitten, Abfragen zu messen:

sqlite> .timer on
sqlite> SELECT ...
Run Time: real ... user ... sys ...

EXPLAIN QUERY PLAN

Sie können SQLite bitten, zu erklären, wie eine Abfrage beantwortet werden soll. Verwenden Sie dazu EXPLAIN QUERY PLAN:

sqlite> EXPLAIN QUERY PLAN
SELECT id, name
FROM Customers
WHERE city = 'Paris';
QUERY PLAN
`--SCAN Customers

Im vorherigen Beispiel ist ein vollständiger Tabellenscan ohne Index erforderlich, um alle Kunden aus Paris zu finden. Dies wird als lineare Komplexität bezeichnet. SQLite muss alle Zeilen lesen und nur die behalten, die mit Kunden aus Paris übereinstimmen. Sie können das Problem beheben, indem Sie einen Index hinzufügen:

sqlite> CREATE INDEX Idx1 ON Customers(city);
sqlite> EXPLAIN QUERY PLAN
SELECT id, name
FROM Customers
WHERE city = 'Paris';
QUERY PLAN
`--SEARCH test USING INDEX Idx1 (city=?

Wenn Sie die interaktive Shell verwenden, können Sie SQLite anweisen, Abfragepläne immer zu erläutern:

sqlite> .eqp on

Weitere Informationen finden Sie unter Abfrageplanung.

SQLite-Analysetool

SQLite bietet die Befehlszeile sqlite3_analyzer, mit der zusätzliche Informationen gedumpt werden können, die zur Behebung von Leistungsproblemen verwendet werden können. Informationen zur Installation finden Sie auf der SQLite-Downloadseite.

Mit adb pull können Sie eine Datenbankdatei von einem Zielgerät zur Analyse auf Ihre Workstation herunterladen:

adb pull /data/data/<app_package_name>/databases/<db_name>.db

SQLite-Browser

Sie können auch das GUI-Tool SQLite Browser auf der Downloadseite von SQLite installieren.

Android-Protokollierung

Android protokolliert die Ausführungszeiten von SQLite-Abfragen:

# Enable query time logging
$ adb shell setprop log.tag.SQLiteTime VERBOSE
# Disable query time logging
$ adb shell setprop log.tag.SQLiteTime ERROR

Perfetto-Tracing

Wenn Sie Perfetto konfigurieren, können Sie Folgendes hinzufügen, um Tracks für einzelne Abfragen einzubeziehen:

data_sources {
  config {
    name: "linux.ftrace"
    ftrace_config {
      atrace_categories: "database"
    }
  }
}