Android bietet integrierte Unterstützung für SQLite, ein eine effiziente SQL-Datenbank. Mit diesen Best Practices können Sie und dafür sorgen, dass sie auch bei zunehmenden Datenmengen schnell und vorhersehbar schnell bleibt. Mit diesen Best Practices verringern Sie auch die Wahrscheinlichkeit, Es stößt auf Leistungsprobleme, die sich nur schwer reproduzieren lassen, zur Fehlerbehebung.
Beachten Sie die folgenden Leistungsprinzipien, um eine höhere Leistung zu erzielen:
Weniger Zeilen und Spalten anzeigen: Optimieren Sie Ihre Abfragen, um nur die notwendige Daten. Minimieren Sie die aus der Datenbank gelesene Datenmenge, kann sich ein übermäßiger Datenabruf negativ auf die Leistung auswirken.
Push-Arbeit an SQLite-Engine: Berechnungen, Filterung und Sortierung durchführen Operationen innerhalb der SQL-Abfragen. Die Verwendung der Abfrage-Engine von SQLite kann erheblich die Leistung zu verbessern.
Datenbankschema ändern: Datenbankschema zur Unterstützung von SQLite entwerfen effiziente Abfragepläne und Datendarstellungen zu erstellen. Tabellen richtig indexieren und optimieren Sie Tabellenstrukturen, um die Leistung zu verbessern.
Außerdem können Sie mit den verfügbaren Tools zur Fehlerbehebung SQLite-Datenbank, um Bereiche zu identifizieren, Optimierung.
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 die Performance in SQLite.
Write-Ahead-Logging aktivieren
SQLite implementiert Mutationen durch Anhängen an ein Log, was gelegentlich in die Datenbank komprimiert. Dies wird als Write-Ahead-Logging (WAL) angeben.
Aktivieren
WAL
außer Sie verwenden ATTACH
DATABASE
.
Synchronisierungsmodus lockern
Bei Verwendung von WAL wird standardmäßig bei jedem Commit ein fsync
ausgegeben, um sicherzustellen,
erreichen die Daten das Laufwerk. Dies verbessert die Langlebigkeit der Daten, verlangsamt aber
Commits übergeben.
SQLite bietet eine Option, um synchrone
Modus an. Wenn Sie
Aktivieren Sie WAL und setzen Sie den synchronen Modus auf NORMAL
:
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 sind. Wenn ein wenn das Gerät heruntergefahren wird, beispielsweise bei Stromausfall oder bei einer Kernel-Panic, übergebene Daten verloren gehen. Aufgrund des Loggings wird Ihre Datenbank korrumpiert.
Wenn nur Ihre App abstürzt, erreichen Ihre Daten das Laufwerk weiterhin. Bei den meisten Apps ohne materielle Kosten Leistungsverbesserungen zu erzielen.
Effiziente Tabellenschemas definieren
Um die Leistung zu optimieren und den Datenverbrauch zu minimieren, definieren Sie eine effiziente Tabellenschema. SQLite erstellt effiziente Abfragepläne und Daten, was zu schnelleren Datenabruf. Dieser Abschnitt enthält Best Practices zum Erstellen einer Tabelle Schemas.
INTEGER PRIMARY KEY
in Betracht ziehen
Definieren Sie für dieses Beispiel eine Tabelle und füllen Sie sie 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 (Zeilen-ID) | 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
Index, der den Anzeigenauftrag beibehält. Suchanfragen, die
nach rowid
filtern, werden als schnelle B-Baum-Suche implementiert. Suchanfragen, die
Nach id
filtern ist ein langsamer Tabellenscan.
Wenn Sie Suchvorgänge bis zum id
durchführen möchten, können Sie das Speichern des
rowid
-Spalte für weniger Daten im Speicher und eine
schnellere Datenbank:
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 Sie die Spalte rowid
nicht speichern müssen, sind id
-Abfragen schnell. Hinweis
, dass die Tabelle jetzt basierend auf id
und nicht nach dem Anzeigenauftrag sortiert ist.
Abfragen mit Indexen beschleunigen
SQLite nutzt
Indexe
um Abfragen zu beschleunigen. Beim Filtern (WHERE
), Sortieren (ORDER BY
) oder
Beim Aggregieren (GROUP BY
) einer Spalte, wenn die Tabelle einen Index für die Spalte hat,
Abfrage beschleunigt wird.
Im vorherigen Beispiel muss zum Filtern nach city
die gesamte Tabelle gescannt werden:
SELECT id, name
WHERE city = 'London, England';
Bei einer Anwendung mit vielen Suchanfragen nach Städten können Sie diese Abfragen mit einem Index:
CREATE INDEX city_index ON Customers(city);
Ein Index wird als zusätzliche Tabelle implementiert, sortiert nach der Indexspalte
zugeordnet zu rowid
:
Stadt | Rowid (Zeilen-ID) |
---|---|
Gary, IN | 2 |
Liverpool, England | 1 |
Sevier County, Tennessee | 3 |
Die Speicherkosten der Spalte city
sind jetzt doppelt so hoch,
die jetzt sowohl in der Originaltabelle
als auch im Index vorhanden sind. Da Sie die Methode
Index ist, sind die Kosten für zusätzlichen Speicher den Nutzen schnellerer Abfragen wert.
Pflegen Sie jedoch keinen Index, den Sie nicht nutzen,
wenn keine Steigerung
der Abfrageleistung erforderlich ist.
Mehrspaltige Indexe erstellen
Wenn Ihre Abfragen mehrere Spalten umfassen, können Sie mehrspaltige Abfragen Indexe um die Abfrage vollständig zu beschleunigen. Sie können einen Index auch für eine externe Spalte verwenden und lassen Sie die Suche innerhalb als linearen Scan durchführen.
Nehmen wir zum Beispiel die folgende Abfrage:
SELECT id, name
WHERE city = 'London, England'
ORDER BY city, name
Sie können die Abfrage mit einem mehrspaltigen Index in der gleichen Reihenfolge wie in der Abfrage angegeben:
CREATE INDEX city_name_index ON Customers(city, name);
Wenn Sie jedoch nur einen Index für city
haben, ist die externe Sortierung weiterhin
beschleunigt werden, während für die innere Sortierung
ein linearer Scan erforderlich ist.
Dies funktioniert auch mit Präfixanfragen. Beispiel: Ein Index
ON Customers (city, name)
beschleunigt auch das Filtern, Sortieren und Gruppieren.
nach city
, da die Indextabelle für einen mehrspaltigen Index nach dem
in der angegebenen Reihenfolge
angezeigt werden sollen.
WITHOUT ROWID
in Betracht ziehen
Standardmäßig erstellt SQLite eine rowid
-Spalte für Ihre Tabelle, wobei rowid
ein
implizites INTEGER PRIMARY KEY AUTOINCREMENT
Wenn Sie bereits eine Spalte haben,
INTEGER PRIMARY KEY
ist, wird diese Spalte zu einem Alias von rowid
.
Für Tabellen, die einen anderen Primärschlüssel als INTEGER
haben, oder eine Zusammensetzung aus
Spalten finden Sie WITHOUT
ROWID
.
Kleine Daten als BLOB
und große Daten als Datei speichern
Wenn Sie große Datenmengen mit einer Zeile verknüpfen möchten, z. B. die Miniaturansicht eines Bildes
oder ein Foto für einen Kontakt, können Sie die Daten entweder in der Spalte BLOB
oder in
und speichern Sie dann den Dateipfad in der Spalte.
Dateien werden in der Regel in Schritten von 4 KB aufgerundet. Bei sehr kleinen Dateien,
der Rundungsfehler erheblich ist, ist es effizienter, sie in der
Datenbank als BLOB
. SQLite minimiert Dateisystemaufrufe und ist schneller als die
zugrunde liegendes Dateisystem
.
Abfrageleistung verbessern
Befolgen Sie diese Best Practices, um die Abfrageleistung in SQLite zu verbessern, indem Sie Reaktionszeiten und maximale Verarbeitungseffizienz.
Nur die benötigten Zeilen lesen
Mit Filtern können Sie die Ergebnisse eingrenzen, indem Sie bestimmte Kriterien, wie Zeitraum, Standort oder Name. Mit Limits können Sie die Anzahl der der angezeigten Ergebnisse:
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 die Auswahl nicht benötigter Spalten, Abfragen verlangsamen und Ressourcen verschwenden. 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); ... } }
DISTINCT
für eindeutige Werte verwenden
Mit dem Keyword DISTINCT
können Sie die Leistung Ihrer Suchanfragen verbessern, indem Sie
die zu verarbeitende Datenmenge reduziert wird. Wenn Sie zum Beispiel
Um nur die eindeutigen Werte aus einer Spalte zurückzugeben, 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. Beispiel: Der Parameter Der folgende Code prü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 ... } }
Um nur die erste Zeile abzurufen, können Sie EXISTS()
verwenden, um 0
zurückzugeben, wenn eine Übereinstimmung
Zeile nicht vorhanden 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 ... } }
SQLite-Aggregat verwenden Funktionen in Ihrer App Code:
COUNT
: zählt, wie viele Zeilen sich in einer Spalte befinden.SUM
: Addiert alle numerischen Werte in einer Spalte.MIN
oderMAX
: Hiermit wird der niedrigste oder höchste Wert festgelegt. Funktioniert für numerische SpaltenDATE
- und Texttypen.AVG
: Ermittelt den numerischen durchschnittlichen Wert.GROUP_CONCAT
: verkettet Strings mit einem optionalen Trennzeichen.
COUNT()
statt Cursor.getCount()
verwenden
Im
im folgenden Beispiel
Cursor.getCount()
-Funktion
liest 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(); ... }
Bei Verwendung von COUNT()
gibt die Datenbank jedoch nur die
Anzahl:
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 zusammensetzbar und unterstützt Unterabfragen, Joins und Fremdschlüsseleinschränkungen. Sie können das Ergebnis einer Abfrage in einer anderen Abfrage verwenden, ohne die App Code. Dadurch müssen Daten nicht mehr aus SQLite kopiert werden und die Datenbank kann Ihre Suchanfrage zu optimieren.
Im folgenden Beispiel können Sie eine Abfrage ausführen, um zu ermitteln, in welcher Stadt Kundschaft finden, verwenden Sie dann das Ergebnis einer anderen Abfrage, um alle Kunden diese Stadt:
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 ein einzelnes SQL 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 im ist es möglicherweise effizienter, diese Eindeutigkeit als Spalte Einschränkung.
Im folgenden Beispiel wird eine Abfrage ausgeführt, um zu validieren, dass die Zeile eingefügt und eine weitere zum Einfü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 beim Definieren der Tabelle:
CREATE TABLE Customers(
id INTEGER PRIMARY KEY,
name TEXT,
username TEXT UNIQUE
);
SQLite tut dasselbe wie:
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, anstelle von App-Code SQLite zu verwenden.
Mehrere Einfügungen in einer einzigen Transaktion im Batch ausführen
Eine Transaktion führt mehrere Operationen durch, was verbessert, sondern auch auf Richtigkeit. Um die Datenkonsistenz und und die Leistung zu beschleunigen, können Sie das Einfügen im Batch tun:
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, um die Leistung zu messen.
Interaktiven Prompt von SQLite verwenden
SQLite auf Ihrem Computer ausführen, um Abfragen auszuführen und Neues zu lernen
Verschiedene Android-Plattformversionen verwenden unterschiedliche Versionen von SQLite. Zur Verwendung
die auf einem Android-Gerät läuft, verwenden Sie adb shell
und
sqlite3
auf deinem Zielgerät ausführen.
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, indem Sie die
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 aus Paris zu finden. Dies wird als lineare Komplexität bezeichnet. SQLite muss lesen alle Zeilen und behalten nur die Zeilen bei, 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 bitten, Abfragepläne:
sqlite> .eqp on
Weitere Informationen finden Sie unter Abfrageplanung
SQLite-Analysetool
SQLite bietet
sqlite3_analyzer
Befehlszeilenschnittstelle (CLI) verwenden, um zusätzliche Informationen auszulesen, die für
Probleme mit der Leistung zu beheben. Zur Installation besuche die
SQLite-Downloadseite
Mit adb pull
können Sie eine Datenbankdatei von einem Zielgerät auf Ihr
Workstation für die Analyse:
adb pull /data/data/<app_package_name>/databases/<db_name>.db
SQLite-Browser
Sie können auch das GUI-Tool installieren, SQLite Browser auf SQLite Downloadseite.
Android-Logging
Android synchronisiert 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
### Perfetto tracing {:#perfetto-tracing}
When [configuring Perfetto](https://perfetto.dev/docs/concepts/config), you may
add the following to include tracks for individual queries:
```protobuf
data_sources {
config {
name: "linux.ftrace"
ftrace_config {
atrace_categories: "database"
}
}
}
Empfehlungen für dich
- Hinweis: Der Linktext wird angezeigt, wenn JavaScript deaktiviert ist.
- Benchmarks in Continuous Integration ausführen
- Eingefrorene Frames
- Baseline-Profile ohne MacroBenchmark erstellen und messen