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, dass Leistungsprobleme auftreten, die schwer zu reproduzieren und zu beheben sind.
Wenn Sie eine höhere Leistung erzielen möchten, sollten Sie die folgenden Leistungsprinzipien beachten:
Weniger Zeilen und Spalten lesen: Optimieren Sie Ihre Abfragen so, dass 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 übertragen: Berechnungen, Filter- und Sortiervorgänge in den SQL-Abfragen ausführen. Die Verwendung der SQLite-Abfrage-Engine 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 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
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
In SQLite werden Mutationen implementiert, indem sie an ein Log angehängt werden, das gelegentlich in die Datenbank komprimiert wird. Dieser Vorgang wird als Write-Ahead-Logging (WAL) bezeichnet.
Aktivieren Sie WAL, sofern Sie nicht ATTACH
DATABASE
verwenden.
Synchronisierungsmodus lockern
Bei Verwendung von WAL wird standardmäßig bei jedem Commit ein fsync
ausgegeben, um sicherzustellen, dass die Daten auf die Festplatte geschrieben werden. Das verbessert die Datenbeständigkeit, verlangsamt aber die Commits.
SQLite bietet eine Option zum Steuern 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 einer Festplatte gespeichert werden. 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 das Laufwerk geschrieben. Bei den meisten Apps führt diese Einstellung zu Leistungsverbesserungen ohne nennenswerte Kosten.
Effiziente Tabellenschemas definieren
Um die Leistung zu optimieren und den Datenverbrauch zu minimieren, sollten Sie ein effizientes Tabellenschema definieren. SQLite erstellt effiziente Abfragepläne und Daten, was zu einem schnelleren Datenabruf führt. Dieser Abschnitt enthält Best Practices zum Erstellen von Tabellenschemas.
Achte auf INTEGER PRIMARY KEY
Definieren und füllen Sie für dieses Beispiel eine Tabelle so:
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, TN |
Die Spalte rowid
ist ein Index, der die Einfügereihenfolge beibehält. Abfragen, die nach rowid
filtern, werden als schnelle B-Baum-Suche implementiert. Abfragen, die nach id
filtern, sind dagegen ein langsamer Tabellenscan.
Wenn Sie vorhaben, Suchvorgänge nach id
durchzuführen, können Sie die Speicherung der Spalte rowid
vermeiden, um weniger Daten zu speichern und die Datenbank insgesamt zu beschleunigen:
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, TN |
Da Sie die Spalte rowid
nicht speichern müssen, sind id
-Abfragen schnell. Die Tabelle ist jetzt nach id
statt nach Anzeigenauftrag sortiert.
Abfragen mit Indexen beschleunigen
SQLite verwendet Indizes, 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 beim Filtern nach city
die gesamte Tabelle gescannt werden:
SELECT id, name
WHERE city = 'London, England';
Bei einer App mit vielen Stadt-Abfragen können Sie diese Abfragen mit einem Index beschleunigen:
CREATE INDEX city_index ON Customers(city);
Ein Index wird als zusätzliche Tabelle implementiert, die nach der Indexspalte sortiert und rowid
zugeordnet ist:
Stadt | rowid |
---|---|
Gary, IN | 2 |
Liverpool, England | 1 |
Sevier County, TN | 3 |
Die Speicherkosten für die Spalte city
haben sich verdoppelt, da sie jetzt sowohl in der Originaltabelle als auch im Index vorhanden ist. Da Sie den Index verwenden, sind die Kosten für zusätzlichen Speicherplatz den Vorteil schnellerer Abfragen wert.
Sie sollten jedoch keinen Index beibehalten, den Sie nicht verwenden, um die Speicherkosten ohne Leistungssteigerung bei Abfragen zu vermeiden.
Mehrspaltige Indexe erstellen
Wenn in Ihren Abfragen mehrere Spalten kombiniert werden, können Sie mehrspaltige Indexe erstellen, um die Abfrage vollständig zu beschleunigen. Sie können auch einen Index für eine äußere Spalte verwenden und die innere Suche als linearen Scan ausführen lassen.
Angenommen, Sie haben die folgende Abfrage:
SELECT id, name
WHERE city = 'London, England'
ORDER BY city, name
Sie können die Abfrage mit einem mehrspaltigen Index in derselben Reihenfolge wie in der Abfrage beschleunigen:
CREATE INDEX city_name_index ON Customers(city, name);
Wenn Sie jedoch nur einen Index für city
haben, wird die äußere Sortierung weiterhin beschleunigt, während für die innere Sortierung ein linearer Scan erforderlich ist.
Das funktioniert auch mit Anfragen 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 Primärschlüssel aus Spalten sollten Sie WITHOUT
ROWID
in Betracht ziehen.
Kleine Datenmengen als BLOB
und große Datenmengen als Datei speichern
Wenn Sie einer Zeile große Daten zuordnen möchten, z. B. ein Miniaturbild 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-Schritte aufgerundet. Bei sehr kleinen Dateien, bei denen der Rundungsfehler erheblich ist, ist es effizienter, sie als BLOB
in der Datenbank 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 Reaktionszeiten minimieren und die Verarbeitungseffizienz maximieren.
Nur die benötigten Zeilen lesen
Mit Filtern können Sie Ihre Ergebnisse eingrenzen, indem Sie bestimmte Kriterien wie Zeitraum, 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()) { ... } }
Java
try (Cursor cursor = db.rawQuery(""" SELECT name FROM Customers LIMIT 10; """, null)) { while (cursor.moveToNext()) { ... } }
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 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 parametrisieren
Ihr Abfragestring enthält möglicherweise einen Parameter, 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 wird bei jeder Abfrage ein anderer String erstellt. Daher wird der Anweisungscache nicht genutzt. Für jeden Aufruf muss SQLite ihn kompilieren, bevor er ausgeführt werden kann. Stattdessen können Sie das id
-Argument 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.
Iteration in SQL, nicht im Code
Verwenden Sie eine einzelne Abfrage, die alle gewünschten Ergebnisse zurückgibt, anstatt eine Programmschleife, die SQL-Abfragen durchläuft, 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
Die Verwendung des Keywords DISTINCT
kann die Leistung Ihrer Abfragen verbessern, da 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 ... } }
Java
try (Cursor cursor = db.rawQuery(""" SELECT DISTINCT name FROM Customers; """, null)) { while (cursor.moveToNext()) { // Only iterate over distinct names in Java ... } }
Nach Möglichkeit Aggregatfunktionen verwenden
Verwenden Sie Aggregatfunktionen für aggregierte Ergebnisse ohne Zeilendaten. Mit dem folgenden Code wird beispielsweise geprüft, ob es mindestens eine übereinstimmende Zeile gibt:
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, wie viele Zeilen in einer Spalte vorhanden sind.SUM
: Addiert alle numerischen Werte in einer Spalte.MIN
oderMAX
: Legt den niedrigsten oder höchsten Wert fest. Funktioniert für numerische Spalten,DATE
-Typen und Texttypen.AVG
: Ermittelt den durchschnittlichen numerischen Wert.GROUP_CONCAT
: Verkettet Strings mit einem optionalen Trennzeichen.
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); ... }
Abfragen statt Code verschachteln
SQL ist zusammensetzbar und unterstützt Unterabfragen, Joins und Fremdschlüsselbeschränkungen. Sie können das Ergebnis einer Abfrage in einer anderen Abfrage verwenden, ohne App-Code zu durchlaufen. Dadurch muss 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 es die meisten Kunden gibt, 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()) { ... } } } }
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()) { ... } } } }
Um das Ergebnis in der Hälfte der Zeit des vorherigen Beispiels zu erhalten, 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 eingefügt werden darf, wenn ein bestimmter Spaltenwert in der Tabelle eindeutig ist, kann es effizienter sein, diese Eindeutigkeit als Spaltenbeschränkung zu erzwingen.
Im folgenden Beispiel wird eine Abfrage ausgeführt, um die einzufügende Zeile zu validieren, und eine andere, 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 macht dasselbe wie Folgendes:
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 empfiehlt sich, SQLite anstelle von App-Code zu verwenden.
Mehrere Einfügungen in einer einzelnen Transaktion zusammenfassen
Bei einer Transaktion werden mehrere Vorgänge festgeschrieben, was nicht nur die Effizienz, sondern auch die Richtigkeit verbessert. Um die Datenkonsistenz zu verbessern und die Leistung zu steigern, können Sie Einfügungen in Batches ausführen:
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 zu lernen.
Für verschiedene Android-Plattformversionen werden unterschiedliche Revisionen von SQLite verwendet. Wenn Sie dieselbe Engine wie auf einem Android-Gerät verwenden möchten, verwenden Sie adb shell
und führen Sie sqlite3
auf Ihrem Zielgerät aus.
Sie können SQLite bitten, die Ausführung von Abfragen zu messen:
sqlite> .timer on
sqlite> SELECT ...
Run Time: real ... user ... sys ...
EXPLAIN QUERY PLAN
Sie können SQLite bitten, zu erläutern, wie eine Abfrage beantwortet werden soll, indem Sie EXPLAIN QUERY PLAN
verwenden:
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. Das wird als lineare Komplexität bezeichnet. SQLite muss alle Zeilen lesen und nur die Zeilen beibehalten, die mit Kunden aus Paris übereinstimmen. Um das Problem zu beheben, können 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 auffordern, immer Abfragepläne zu erläutern:
sqlite> .eqp on
Weitere Informationen finden Sie unter Abfrageplanung.
SQLite Analyzer
SQLite bietet die Befehlszeile sqlite3_analyzer
, mit der Sie zusätzliche Informationen ausgeben können, die zur Fehlerbehebung bei der Leistung verwendet werden können. Die Installationsdateien finden Sie auf der SQLite-Downloadseite.
Mit adb pull
können Sie eine Datenbankdatei von einem Zielgerät auf Ihre Workstation herunterladen, um sie zu analysieren:
adb pull /data/data/<app_package_name>/databases/<db_name>.db
SQLite Browser
Sie können auch das GUI-Tool SQLite Browser auf der SQLite-Downloadseite installieren.
Android-Logging
Android misst die Zeit für SQLite-Abfragen und protokolliert sie für Sie:
# 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 Anfragen einzuschließen:
data_sources {
config {
name: "linux.ftrace"
ftrace_config {
atrace_categories: "database"
}
}
}
dumpsys meminfo
adb shell dumpsys meminfo <package-name>
gibt Statistiken zur Arbeitsspeichernutzung der App aus, einschließlich einiger Details zum SQLite-Arbeitsspeicher. Das folgende Beispiel stammt aus der Ausgabe von adb shell dumpsys meminfo com.google.android.gms.persistent
auf dem Gerät eines Entwicklers:
DATABASES
pgsz dbsz Lookaside(b) cache hits cache misses cache size Dbname
PER CONNECTION STATS
4 52 45 8 41 6 /data/user/10/com.google.android.gms/databases/gaia-discovery
4 8 0 0 0 (attached) temp
4 52 56 5 23 6 /data/user/10/com.google.android.gms/databases/gaia-discovery (1)
4 252 95 233 124 12 /data/user_de/10/com.google.android.gms/databases/phenotype.db
4 8 0 0 0 (attached) temp
4 252 17 0 17 1 /data/user_de/10/com.google.android.gms/databases/phenotype.db (1)
4 9280 105 103169 69805 25 /data/user/10/com.google.android.gms/databases/phenotype.db
4 20 0 0 0 (attached) temp
4 9280 108 13877 6394 25 /data/user/10/com.google.android.gms/databases/phenotype.db (2)
4 8 0 0 0 (attached) temp
4 9280 105 12548 5519 25 /data/user/10/com.google.android.gms/databases/phenotype.db (3)
4 8 0 0 0 (attached) temp
4 9280 107 18328 7886 25 /data/user/10/com.google.android.gms/databases/phenotype.db (1)
4 8 0 0 0 (attached) temp
4 36 51 156 29 5 /data/user/10/com.google.android.gms/databases/mobstore_gc_db_v0
4 36 97 47 27 10 /data/user/10/com.google.android.gms/databases/context_feature_default.db
4 36 56 3 16 4 /data/user/10/com.google.android.gms/databases/context_feature_default.db (2)
4 300 40 2111 24 5 /data/user/10/com.google.android.gms/databases/gservices.db
4 300 39 3 17 4 /data/user/10/com.google.android.gms/databases/gservices.db (1)
4 20 17 0 14 1 /data/user/10/com.google.android.gms/databases/gms.notifications.db
4 20 33 1 15 2 /data/user/10/com.google.android.gms/databases/gms.notifications.db (1)
4 120 40 143 163 4 /data/user/10/com.google.android.gms/databases/android_pay
4 120 123 86 32 19 /data/user/10/com.google.android.gms/databases/android_pay (1)
4 28 33 4 17 3 /data/user/10/com.google.android.gms/databases/googlesettings.db
POOL STATS
cache hits cache misses cache size Dbname
13 68 81 /data/user/10/com.google.android.gms/databases/gaia-discovery
233 145 378 /data/user_de/10/com.google.android.gms/databases/phenotype.db
147921 89616 237537 /data/user/10/com.google.android.gms/databases/phenotype.db
156 30 186 /data/user/10/com.google.android.gms/databases/mobstore_gc_db_v0
50 57 107 /data/user/10/com.google.android.gms/databases/context_feature_default.db
2114 43 2157 /data/user/10/com.google.android.gms/databases/gservices.db
1 31 32 /data/user/10/com.google.android.gms/databases/gms.notifications.db
229 197 426 /data/user/10/com.google.android.gms/databases/android_pay
4 18 22 /data/user/10/com.google.android.gms/databases/googlesettings.db
Unter DATABASES
finden Sie:
pgsz
: die Größe einer Datenbankseite in KB.dbsz
: Die Größe der gesamten Datenbank in Seiten. Um die Größe in KB zu erhalten, multiplizieren Siepgsz
mitdbsz
.Lookaside(b)
: Der Speicher, der dem SQLite-Lookaside-Puffer pro Verbindung zugewiesen wird, in Byte. Diese sind in der Regel sehr klein.cache hits
: SQLite verwaltet einen Cache von Datenbankseiten. Das ist die Anzahl der Page-Cache-Treffer.cache misses
: Anzahl der Page-Cache-Fehler (Anzahl).cache size
: Anzahl der Seiten im Cache (Anzahl). Um die Größe in KB zu erhalten, multiplizieren Sie diese Zahl mitpgsz
.Dbname
: Pfad zur DB-Datei. In unserem Beispiel haben einige Datenbanken(1)
oder eine andere Zahl an ihren Namen angehängt, um anzugeben, dass es mehr als eine Verbindung zur selben zugrunde liegenden Datenbank gibt. Statistiken werden pro Verbindung erfasst.
Unter POOL STATS
finden Sie:
cache hits
: SQLite speichert vorbereitete Anweisungen im Cache und versucht, sie beim Ausführen von Abfragen wiederzuverwenden, um Aufwand und Speicherplatz beim Kompilieren von SQL-Anweisungen zu sparen. Die Anzahl der Cache-Treffer für Anweisungen.cache misses
: Anzahl der Cache-Fehler für Anweisungen (Anzahl).cache size
: Ab Android 17 wird hier die Gesamtzahl der vorbereiteten Anweisungen im Cache aufgeführt. In früheren Versionen entspricht dieser Wert der Summe der Treffer und Fehler, die in den beiden anderen Spalten aufgeführt sind, und stellt nicht die Cachegröße dar.
Empfehlungen für dich
- Hinweis: Linktext wird angezeigt, wenn JavaScript deaktiviert ist.
- Benchmarks in Continuous Integration ausführen
- Eingefrorene Frames
- Baseline-Profile ohne Macrobenchmark erstellen und messen