Android oferuje wbudowaną obsługę SQLite – wydajnej bazy danych SQL. Postępuj zgodnie z tymi sprawdzonymi metodami, aby zoptymalizować wydajność swojej aplikacji, zapewniając jej działanie w miarę wzrostu ilości danych w przewidywalny sposób. Stosowanie tych sprawdzonych metod zmniejsza też ryzyko wystąpienia problemów z wydajnością, które są trudne do odtworzenia i rozwiązywania.
Aby szybciej osiągnąć skuteczność, przestrzegaj tych zasad:
Odczytuj mniej wierszy i kolumn: możesz optymalizować zapytania, aby pobierać tylko niezbędne dane. Ogranicz ilość danych odczytywanych z bazy danych, ponieważ ich nadmierne pobieranie może mieć wpływ na wydajność.
Przekazywanie zadań do mechanizmu SQLite: możesz wykonywać operacje obliczeniowe, filtrowania i sortowania w ramach zapytań SQL. Użycie mechanizmu zapytań SQLite może znacznie zwiększyć wydajność.
Modyfikowanie schematu bazy danych: zaprojektuj schemat bazy danych, aby ułatwić SQLite tworzenie wydajnych planów zapytań i reprezentacji danych. Prawidłowo indeksuj tabele i optymalizuj ich struktury, aby zwiększyć wydajność.
Dodatkowo możesz używać dostępnych narzędzi do rozwiązywania problemów, aby mierzyć wydajność bazy danych SQLite i w ten sposób identyfikować obszary wymagające optymalizacji.
Zalecamy skorzystanie z biblioteki Jetpack Room.
Skonfiguruj bazę danych pod kątem wydajności
Wykonaj czynności opisane w tej sekcji, aby skonfigurować bazę danych pod kątem optymalnej wydajności w SQLite.
Włącz logowanie z wyprzedzeniem
SQLite wdraża mutacje, dołączając je do logu, który czasami kompaktuje do bazy danych. Jest to tzw. Zapisywanie z wyprzedzeniem (WAL).
Włącz WAL, chyba że używasz ATTACH
DATABASE
.
Rozluźnij tryb synchronizacji
Gdy używany jest zapisywanie logów z wyprzedzeniem, domyślnie każde zatwierdzenie wysyła fsync
, aby zapewnić, że dane dotrą na dysk. Zwiększa to trwałość danych, ale spowalnia obciążenia.
SQLite ma opcję sterowania trybem synchronicznym. Jeśli włączysz zapisywanie logów z wyprzedzeniem, ustaw tryb synchroniczny na NORMAL
:
Kotlin
db.execSQL("PRAGMA synchronous = NORMAL")
Java
db.execSQL("PRAGMA synchronous = NORMAL");
W tym ustawieniu zatwierdzenie może zostać zwrócone, zanim dane zostaną zapisane na dysku. Jeśli urządzenie zostanie wyłączone, na przykład w wyniku utraty zasilania lub paniki z nią związane, przesłane dane mogą zostać utracone. Dzięki logowaniu Twoja baza danych nie jest uszkodzona.
Jeśli tylko aplikacja ulegnie awarii, dane nadal będą znajdować się na dysku. W przypadku większości aplikacji to ustawienie pozwala poprawić wydajność bez żadnych dodatkowych kosztów.
Definiowanie wydajnych schematów tabel
Aby zoptymalizować wydajność i zminimalizować wykorzystanie danych, zdefiniuj wydajny schemat tabeli. SQLite tworzy wydajne plany i dane zapytań, co przyspiesza pobieranie danych. W tej sekcji znajdziesz sprawdzone metody tworzenia schematów tabel.
Rozważ: INTEGER PRIMARY KEY
Na potrzeby tego przykład zdefiniuj i wypełnij tabelę w ten sposób:
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');
Dane wyjściowe tabeli są takie:
identyfikator wiersza | id | nazwa | miasto |
---|---|---|---|
1 | 456 | John Lennon | Liverpool, Anglia |
2 | 123 | Michaela Jacksona | Gary, Indiana |
3 | 789 | Dolly Parton | Hrabstwo Sevier, Tennessee |
Kolumna rowid
zawiera indeks, który zachowuje zamówienie reklamowe. Zapytania z filtrem rowid
są implementowane jako szybkie wyszukiwanie B-tree, ale zapytania z filtrem id
to powolne skanowanie tabeli.
Jeśli planujesz wyszukiwanie do id
, możesz uniknąć przechowywania kolumny rowid
, aby zmniejszyć ilość miejsca na dane i ogólnie przyspieszyć bazę danych:
CREATE TABLE Customers(
id INTEGER PRIMARY KEY,
name TEXT,
city TEXT
);
Tabela wygląda teraz tak:
id | nazwa | miasto |
---|---|---|
123 | Michaela Jacksona | Gary, Indiana |
456 | John Lennon | Liverpool, Anglia |
789 | Dolly Parton | Hrabstwo Sevier, Tennessee |
Ponieważ kolumna rowid
nie musi być przechowywana, zapytania id
działają szybko. Zwróć uwagę, że tabela jest teraz sortowana na podstawie kolumny id
, a nie zamówienia reklamowego.
Przyspiesz zapytania dzięki indeksom
SQLite używa indeksów do przyspieszania zapytań. Podczas filtrowania (WHERE
), sortowania (ORDER BY
) lub agregowania (GROUP BY
) kolumny, jeśli tabela ma indeks kolumny, wykonywanie zapytania jest przyspieszone.
W poprzednim przykładzie filtrowanie według parametru city
wymaga przeskanowania całej tabeli:
SELECT id, name
WHERE city = 'London, England';
W przypadku aplikacji z dużą liczbą zapytań dotyczących miast możesz przyspieszyć te zapytania za pomocą indeksu:
CREATE INDEX city_index ON Customers(city);
Indeks jest zaimplementowany jako dodatkowa tabela posortowana według kolumny indeksu i zmapowana na wartość rowid
:
miasto | identyfikator wiersza |
---|---|
Gary, Indiana | 2 |
Liverpool, Anglia | 1 |
Hrabstwo Sevier, Tennessee | 3 |
Zauważ, że koszt miejsca na dane kolumny city
jest teraz podwójny, ponieważ jest on teraz obecny zarówno w pierwotnej tabeli, jak i w indeksie. Używasz indeksu, więc koszt dodatkowego miejsca na dane jest wart korzyści szybszych zapytań.
Nie utrzymuj jednak indeksu, którego nie używasz, aby uniknąć opłat za miejsce na dane za brak wzrostu wydajności zapytań.
Tworzenie indeksów wielokolumnowych
Jeśli zapytania łączą wiele kolumn, możesz tworzyć indeksy wielokolumnowe, aby w pełni przyspieszyć wykonywanie zapytania. Możesz też użyć indeksu w kolumnie zewnętrznej i zezwolić na wyszukiwanie w środku za pomocą skanowania liniowego.
Przykładowo w przypadku następującego zapytania:
SELECT id, name
WHERE city = 'London, England'
ORDER BY city, name
Możesz przyspieszyć zapytanie za pomocą indeksu wielokolumnowego w tej samej kolejności, w jakiej zostało określone w zapytaniu:
CREATE INDEX city_name_index ON Customers(city, name);
Jeśli jednak indeks masz tylko w city
, porządkowanie zewnętrzne nadal będzie przyspieszone, a porządkowanie wewnętrzne wymaga skanowania liniowego.
Działa to też w przypadku zapytań opartych na prefiksie. Na przykład indeks ON Customers (city, name)
przyspiesza też filtrowanie, sortowanie i grupowanie według wartości city
, ponieważ tabela indeksu indeksu wielokolumnowego jest uporządkowana według podanych indeksów w podanej kolejności.
Rozważ: WITHOUT ROWID
Domyślnie SQLite tworzy dla tabeli kolumnę rowid
, gdzie rowid
to niejawna wartość INTEGER PRIMARY KEY AUTOINCREMENT
. Jeśli masz już kolumnę z wartością INTEGER PRIMARY KEY
, ta kolumna stanie się aliasem kolumny rowid
.
W przypadku tabel, które mają klucz podstawowy inny niż INTEGER
lub złożony z kolumn, rozważ użycie WITHOUT
ROWID
.
Przechowuj małe dane w formacie BLOB
, a duże jako plik
Jeśli chcesz powiązać duże dane z wierszem, na przykład miniaturę obrazu lub zdjęcie kontaktu, możesz je zapisać w kolumnie BLOB
lub w pliku, a potem zapisać w tej kolumnie ścieżkę pliku.
Pliki są zwykle zaokrąglane do przyrostów co 4 KB. W przypadku bardzo małych plików, gdzie błąd zaokrąglania jest istotny, lepiej zapisać je w bazie danych jako BLOB
. SQLite minimalizuje liczbę wywołań systemu plików i w niektórych przypadkach jest szybsza niż bazowy system plików.
Zwiększ wydajność zapytań
Postępuj zgodnie z tymi sprawdzonymi metodami, aby poprawić wydajność zapytań w SQLite przez zminimalizowanie czasów odpowiedzi i maksymalizację wydajności przetwarzania.
Odczytuj tylko potrzebne wiersze
Filtry pozwalają zawężać wyniki przez określenie kryteriów, takich jak zakres dat, lokalizacja czy nazwa. Limity pozwalają kontrolować liczbę wyświetlanych wyników:
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()) { ... } }
Odczytuj tylko potrzebne kolumny
Unikaj wybierania niepotrzebnych kolumn, ponieważ może to spowolnić wykonywanie zapytań i zużywać zasoby. Zamiast tego wybierz tylko używane kolumny.
W tym przykładzie wybierasz id
, name
i phone
:
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); ... } }
Potrzebujesz jednak tylko kolumny 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); ... } }
Użyj DISTINCT
dla unikalnych wartości
Używanie słowa kluczowego DISTINCT
może zwiększyć skuteczność zapytań dzięki zmniejszeniu ilości danych do przetworzenia. Jeśli np. chcesz zwrócić tylko unikalne wartości z kolumny, użyj funkcji 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 ... } }
Gdy to możliwe, używaj funkcji agregacji
Do wyników agregowanych bez danych wierszy używaj funkcji agregacji. Na przykład ten kod sprawdza, czy istnieje co najmniej 1 pasujący wiersz:
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 ... } }
Aby pobrać tylko pierwszy wiersz, możesz za pomocą funkcji EXISTS()
zwrócić wartość 0
, jeśli pasujący wiersz nie istnieje, oraz wartość 1
, jeśli pasuje co najmniej 1 wiersz:
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 ... } }
Użyj funkcji agregacji SQLite w kodzie aplikacji:
COUNT
: zlicza liczbę wierszy w kolumnie.SUM
: dodaje wszystkie wartości liczbowe w kolumnie.MIN
lubMAX
: określa najniższą lub najwyższą wartość. Działa w przypadku kolumn liczbowych, typówDATE
i typów tekstowych.AVG
: znajduje średnią wartość liczbową.GROUP_CONCAT
: łączy ciągi tekstowe z opcjonalnym separatorem.
Używaj COUNT()
zamiast Cursor.getCount()
W tym przykładzie funkcja Cursor.getCount()
odczytuje wszystkie wiersze z bazy danych i zwraca wszystkie wartości wierszy:
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(); ... }
Jeśli jednak użyjesz funkcji COUNT()
, baza danych zwraca tylko liczbę:
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); ... }
Zapytania dotyczące Nest zamiast kodu
Język SQL jest funkcją kompozycyjną i obsługuje podzapytania, złączenia i ograniczenia klucza obcego. Rezultatu jednego zapytania możesz użyć w innym zapytaniu bez przechodzenia do kodu aplikacji. Ogranicza to potrzebę kopiowania danych z SQLite i pozwala silnikowi bazy danych optymalizować zapytanie.
W tym przykładzie możesz uruchomić zapytanie, aby sprawdzić, w którym mieście jest najwięcej klientów, a następnie użyć wyniku innego zapytania, aby znaleźć wszystkich klientów z tego miasta:
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()) { ... } } } }
Aby uzyskać wynik o połowę szybciej z poprzedniego przykładu, użyj pojedynczego zapytania SQL z zagnieżdżonymi instrukcjami:
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()) { ... } }
Sprawdź unikalność w SQL
Jeśli nie można wstawić wiersza, chyba że konkretna wartość kolumny jest unikalna w tabeli, skuteczniejszym rozwiązaniem może być wymuszenie tej unikalności jako ograniczenie kolumny.
W poniższym przykładzie jedno zapytanie sprawdza, czy wiersz ma zostać wstawiony, a drugie – rzeczywiste wstawienie:
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, });
Zamiast sprawdzać ograniczenie unikalne w Kotlin lub Javie, możesz sprawdzić je w SQL podczas definiowania tabeli:
CREATE TABLE Customers(
id INTEGER PRIMARY KEY,
name TEXT,
username TEXT UNIQUE
);
SQLite działa tak samo jak poniżej:
CREATE TABLE Customers(...);
CREATE UNIQUE INDEX CustomersUsername ON Customers(username);
Teraz możesz wstawić wiersz i pozwolić SQLite sprawdzić ograniczenie:
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 obsługuje unikalne indeksy z wieloma kolumnami:
CREATE TABLE table(...);
CREATE UNIQUE INDEX unique_table ON table(column1, column2, ...);
SQLite weryfikuje ograniczenia szybciej i przy mniejszym nakładzie pracy niż w przypadku kodu Kotlin lub Java. Sprawdzoną metodą jest używanie SQLite zamiast kodu aplikacji.
Zbiorcze wstawianie wielu reklam w ramach 1 transakcji
Transakcja zawiera wiele operacji, co zwiększa nie tylko wydajność, ale także poprawność. Aby zwiększyć spójność danych i przyspieszyć wydajność, możesz przesyłać zbiorczo wstawienia:
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() }
Używanie narzędzi do rozwiązywania problemów
SQLite udostępnia poniższe narzędzia do rozwiązywania problemów, które pomagają mierzyć wydajność.
Użyj interaktywnego promptu SQLite
Uruchom SQLite na swoim komputerze, aby wykonywać zapytania i uczyć się.
Różne wersje platformy Androida używają różnych wersji SQLite. Aby używać tego samego silnika, który działa na urządzeniu z Androidem, wpisz adb shell
i uruchom sqlite3
na urządzeniu docelowym.
Możesz poprosić SQLite o określanie czasu wykonywania zapytań:
sqlite> .timer on
sqlite> SELECT ...
Run Time: real ... user ... sys ...
EXPLAIN QUERY PLAN
Możesz poprosić SQLite o wyjaśnienie, jak ma odpowiadać na zapytanie, używając EXPLAIN QUERY PLAN
:
sqlite> EXPLAIN QUERY PLAN
SELECT id, name
FROM Customers
WHERE city = 'Paris';
QUERY PLAN
`--SCAN Customers
Poprzedni przykład wymaga pełnego skanowania tabeli bez indeksu, aby znaleźć wszystkich klientów z Paryża. Jest to tzw. złożoność liniowa. SQLite musi odczytać wszystkie wiersze i zachować tylko te, które odpowiadają klientom z Paryża. Aby rozwiązać ten problem, możesz dodać indeks:
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=?
Jeśli używasz interaktywnej powłoki, możesz poprosić SQLite o wyjaśnienie planów zapytań:
sqlite> .eqp on
Więcej informacji znajdziesz w artykule o planowaniu zapytań.
Analizator SQLite
SQLite oferuje interfejs wiersza poleceń sqlite3_analyzer
(CLI) do pobierania dodatkowych informacji, które przydają się do rozwiązywania problemów z wydajnością. Aby zainstalować, wejdź na stronę pobierania SQLite.
Za pomocą adb pull
możesz pobrać plik bazy danych z urządzenia docelowego na stację roboczą w celu przeprowadzenia analizy:
adb pull /data/data/<app_package_name>/databases/<db_name>.db
Przeglądarka SQLite
Możesz też zainstalować narzędzie GUI SQLite Browser na stronie pobranych plików SQLite.
Logowanie w Androidzie
Android łączy zapytania SQLite i rejestruje je za Ciebie:
# 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"
}
}
}
Polecane dla Ciebie
- Uwaga: tekst linku jest wyświetlany, gdy JavaScript jest wyłączony
- Wykonywanie testów porównawczych w trybie ciągłej integracji
- Zablokowane klatki
- Tworzenie i pomiar profili bazowych bez użycia Macroporównanie