Android oferuje wbudowaną obsługę SQLite, wydajnej bazy danych SQL. Aby zoptymalizować wydajność aplikacji i zapewnić jej szybkość oraz przewidywalną szybkość działania w miarę wzrostu ilości danych, stosuj te sprawdzone metody. Stosując te sprawdzone metody, zmniejszasz też ryzyko wystąpienia problemów z wydajnością, które są trudne do odtworzenia i rozwiązania.
Aby uzyskać większą wydajność, postępuj zgodnie z tymi zasadami:
Odczytuj mniej wierszy i kolumn: optymalizuj zapytania, aby pobierać tylko niezbędne dane. Zminimalizuj ilość danych odczytywanych z bazy danych, ponieważ nadmierne pobieranie danych może wpływać na wydajność.
Przekazywanie pracy do silnika SQLite: wykonywanie obliczeń, filtrowania i sortowania w zapytaniach SQL. Korzystanie z silnika zapytań SQLite może znacznie zwiększyć wydajność.
Zmodyfikuj schemat bazy danych: zaprojektuj schemat bazy danych tak, aby ułatwić SQLite tworzenie wydajnych planów zapytań i reprezentacji danych. Prawidłowo indeksuj tabele i optymalizuj ich struktury, aby zwiększyć wydajność.
Możesz też użyć dostępnych narzędzi do rozwiązywania problemów, aby zmierzyć wydajność bazy danych SQLite i określić obszary wymagające optymalizacji.
Zalecamy korzystanie z biblioteki Jetpack Room.
Konfigurowanie bazy danych pod kątem wydajności
Aby skonfigurować bazę danych pod kątem optymalnej wydajności w SQLite, wykonaj czynności opisane w tej sekcji.
Włącz logowanie z wyprzedzeniem
SQLite implementuje mutacje, dołączając je do dziennika, który od czasu do czasu kompresuje do bazy danych. Nazywa się to logowaniem z wyprzedzeniem (WAL).
Włącz WAL, chyba że używasz ATTACH
DATABASE
.
Zmień tryb synchronizacji
Podczas korzystania z WAL domyślnie każde zatwierdzenie wysyła polecenie fsync
, aby zapewnić, że dane trafią na dysk. Zwiększa to trwałość danych, ale spowalnia zatwierdzanie zmian.
SQLite ma opcję sterowania trybem synchronicznym. Jeśli włączysz WAL, 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 nastąpi wyłączenie urządzenia, np. z powodu utraty zasilania lub błędu jądra, zatwierdzone dane mogą zostać utracone. Jednak dzięki rejestrowaniu dzienników baza danych nie jest uszkodzona.
Jeśli awarii ulegnie tylko aplikacja, dane nadal trafią na dysk. W przypadku większości aplikacji to ustawienie poprawia wydajność bez ponoszenia znaczących kosztów.
Definiowanie wydajnych schematów tabeli
Aby zoptymalizować wydajność i zminimalizować zużycie danych, zdefiniuj wydajny schemat tabeli. SQLite tworzy wydajne plany zapytań i dane, co przyspiesza pobieranie danych. W tej sekcji znajdziesz sprawdzone metody tworzenia schematów tabel.
Rozważ kategorię INTEGER PRIMARY KEY
.
W tym przykładzie 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ą następujące:
rowid | id | nazwa | miasto |
---|---|---|---|
1 | 456 | John Lennon | Liverpool, Anglia |
2 | 123 | Michael Jackson | Gary, Indiana |
3 | 789 | Dolly Parton | Hrabstwo Sevier, Tennessee |
Kolumna rowid
to indeks, który zachowuje kolejność wstawiania. Zapytania, które filtrują według rowid
, są realizowane jako szybkie wyszukiwanie w drzewie B, ale zapytania, które filtrują według id
, są powolnym skanowaniem tabeli.
Jeśli planujesz wyszukiwanie według id
, możesz uniknąć przechowywania kolumny rowid
, aby zmniejszyć ilość danych w pamięci i przyspieszyć działanie bazy danych:
CREATE TABLE Customers(
id INTEGER PRIMARY KEY,
name TEXT,
city TEXT
);
Tabela wygląda teraz tak:
id | nazwa | miasto |
---|---|---|
123 | Michael Jackson | Gary, Indiana |
456 | John Lennon | Liverpool, Anglia |
789 | Dolly Parton | Hrabstwo Sevier, Tennessee |
Nie musisz przechowywać kolumny rowid
, więc zapytania id
są szybkie. Uwaga: tabela jest teraz sortowana według id
, a nie według zamówienia reklamowego.
Przyspieszanie zapytań za pomocą indeksów
SQLite używa indeksów, aby przyspieszyć wykonywanie zapytań. Podczas filtrowania (WHERE
), sortowania (ORDER BY
) lub agregowania (GROUP BY
) kolumny, jeśli tabela ma indeks dla tej kolumny, zapytanie jest przyspieszane.
W poprzednim przykładzie filtrowanie według city
wymaga przeskanowania całej tabeli:
SELECT id, name
WHERE city = 'London, England';
W przypadku aplikacji z wieloma zapytaniami dotyczącymi miast możesz przyspieszyć te zapytania za pomocą indeksu:
CREATE INDEX city_index ON Customers(city);
Indeks jest implementowany jako dodatkowa tabela posortowana według kolumny indeksu i odwzorowana na rowid
:
miasto | rowid |
---|---|
Gary, Indiana | 2 |
Liverpool, Anglia | 1 |
Hrabstwo Sevier, Tennessee | 3 |
Pamiętaj, że koszt przechowywania kolumny city
jest teraz dwukrotnie wyższy, ponieważ występuje ona zarówno w tabeli pierwotnej, jak i w indeksie. Ponieważ używasz indeksu, koszt dodatkowego miejsca na dane jest wart korzyści w postaci szybszych zapytań.
Nie utrzymuj jednak indeksu, którego nie używasz, aby uniknąć płacenia za pamięć masową bez zwiększania wydajności zapytań.
Tworzenie indeksów wielokolumnowych
Jeśli zapytania łączą wiele kolumn, możesz utworzyć indeksy wielokolumnowe, aby w pełni przyspieszyć zapytanie. Możesz też użyć indeksu w kolumnie zewnętrznej i pozwolić na wykonanie wyszukiwania wewnętrznego jako skanowania liniowego.
Na przykład w przypadku tego zapytania:
SELECT id, name
WHERE city = 'London, England'
ORDER BY city, name
Zapytanie możesz przyspieszyć za pomocą indeksu wielokolumnowego w tej samej kolejności co w zapytaniu:
CREATE INDEX city_name_index ON Customers(city, name);
Jeśli jednak masz indeks tylko w przypadku city
, sortowanie zewnętrzne jest nadal przyspieszane, a sortowanie wewnętrzne wymaga skanowania liniowego.
Działa to również w przypadku zapytań z prefiksami. Na przykład indeksON Customers (city, name)
przyspiesza też filtrowanie, porządkowanie i grupowaniecity
, ponieważ tabela indeksu wielokolumnowego jest uporządkowana według podanych indeksów w określonej kolejności.
Rozważ kategorię WITHOUT ROWID
.
Domyślnie SQLite tworzy w tabeli kolumnę rowid
, gdzie rowid
jest niejawnym INTEGER PRIMARY KEY AUTOINCREMENT
. Jeśli masz już kolumnę, która jest INTEGER PRIMARY KEY
, stanie się ona aliasem kolumny rowid
.
W przypadku tabel, które mają klucz podstawowy inny niż INTEGER
lub klucz złożony z kolumn, rozważ użycie WITHOUT
ROWID
.
przechowywać małe ilości danych jako BLOB
, a duże ilości danych jako plik;
Jeśli chcesz powiązać z wierszem duże ilości danych, np. miniaturę obrazu lub zdjęcie kontaktu, możesz przechowywać dane w BLOB
kolumnie lub w plikuBLOB
, a następnie zapisać ścieżkę do pliku w kolumnie.
Rozmiar plików jest zwykle zaokrąglany w górę do wielokrotności 4 KB. W przypadku bardzo małych plików, w których błąd zaokrąglenia jest znaczny, bardziej efektywne jest przechowywanie ich w bazie danych jako BLOB
. SQLite minimalizuje wywołania systemu plików i w niektórych przypadkach jest szybszy niż bazowy system plików.
Zwiększanie wydajności zapytań
Aby zwiększyć wydajność zapytań w SQLite, postępuj zgodnie z tymi sprawdzonymi metodami, które pozwolą Ci zminimalizować czas odpowiedzi i zmaksymalizować efektywność przetwarzania.
Odczytywanie tylko potrzebnych wierszy
Filtry pozwalają zawęzić wyniki przez określenie pewnych kryteriów, takich jak zakres dat, lokalizacja lub 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()) { ... } }
Odczytywanie tylko potrzebnych kolumn
Unikaj wybierania niepotrzebnych kolumn, które mogą spowolnić zapytania i marnować zasoby. Zamiast tego wybierz tylko kolumny, które są używane.
W przykładzie poniżej 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); ... } }
Wystarczy jednak kolumna 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); ... } }
Parametryzowanie zapytań
Ciąg zapytania może zawierać parametr, który jest znany tylko w czasie działania, np.:
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; } } }
W powyższym kodzie każde zapytanie tworzy inny ciąg znaków, więc nie korzysta z pamięci podręcznej instrukcji. Każde wywołanie wymaga skompilowania przez SQLite, zanim będzie można je wykonać. Zamiast tego możesz zastąpić argument id
parametrem i powiązać wartość z parametrem selectionArgs
:
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; } } }
Teraz zapytanie można skompilować i zapisać w pamięci podręcznej. Skompilowane zapytanie jest ponownie używane
między różnymi wywołaniami funkcji getNameById(long)
.
Iterowanie w SQL, a nie w kodzie
Używaj jednego zapytania, które zwraca wszystkie wyniki docelowe, zamiast pętli programowej iterującej zapytania SQL w celu zwrócenia poszczególnych wyników. Pętla programowa jest około 1000 razy wolniejsza niż pojedyncze zapytanie SQL.
Używaj DISTINCT
w przypadku unikalnych wartości.
Użycie słowa kluczowego DISTINCT
może zwiększyć skuteczność zapytań, ponieważ zmniejsza ilość danych, które trzeba przetworzyć. Jeśli na przykład 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 ... } }
W miarę możliwości używaj funkcji agregacji
Używaj funkcji agregujących, aby uzyskiwać zagregowane wyniki bez danych wierszy. Na przykład poniższy 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 użyć funkcji EXISTS()
, która zwraca wartość 0
, jeśli pasujący wiersz nie istnieje, oraz 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żywaj w kodzie aplikacji funkcji agregujących SQLite:
COUNT
: zlicza wiersze 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 znaków z opcjonalnym separatorem.
Używaj COUNT()
zamiast Cursor.getCount()
W przykładzie poniżej 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(); ... }
Jednak po użyciu COUNT()
baza danych zwróci 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); ... }
Zagnieżdżanie zapytań zamiast kodu
SQL jest kompozycyjny i obsługuje podzapytania, złączenia i ograniczenia klucza obcego. Możesz użyć wyniku jednego zapytania w innym zapytaniu bez przechodzenia przez kod aplikacji. Zmniejsza to konieczność kopiowania danych z SQLite i umożliwia silnikowi bazy danych optymalizację zapytania.
W tym przykładzie możesz uruchomić zapytanie, aby znaleźć miasto z największą liczbą klientów, a następnie użyć wyniku w innym zapytaniu, 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 w połowie czasu potrzebnego w poprzednim przykładzie, użyj jednego 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()) { ... } }
Sprawdzanie unikalności w SQL
Jeśli wiersz nie może zostać wstawiony, chyba że wartość określonej kolumny jest unikalna w tabeli, bardziej efektywne może być wymuszenie tej unikalności jako ograniczenia kolumny.
W tym przykładzie uruchamiane jest jedno zapytanie, aby sprawdzić wiersz do wstawienia, a drugie, aby go wstawić:
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ć unikalne ograniczenie w języku Kotlin lub Java, możesz to zrobić w SQL podczas definiowania tabeli:
CREATE TABLE Customers(
id INTEGER PRIMARY KEY,
name TEXT,
username TEXT UNIQUE
);
SQLite robi to samo co:
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 indeksy unikalne z wieloma kolumnami:
CREATE TABLE table(...);
CREATE UNIQUE INDEX unique_table ON table(column1, column2, ...);
SQLite weryfikuje ograniczenia szybciej i przy mniejszym obciążeniu niż kod Kotlin lub Java. Sprawdzoną metodą jest używanie SQLite zamiast kodu aplikacji.
Grupowanie wielu wstawień w ramach jednej transakcji
Transakcja obejmuje wiele operacji, co zwiększa nie tylko wydajność, ale też poprawność. Aby zwiększyć spójność danych i przyspieszyć działanie, możesz wykonywać wstawienia partiami:
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() }
Korzystanie z narzędzi do rozwiązywania problemów
SQLite udostępnia te narzędzia do rozwiązywania problemów, które pomagają mierzyć wydajność:
Korzystanie z interaktywnego wiersza poleceń SQLite
Uruchom SQLite na swoim komputerze, aby wykonywać zapytania i się uczyć.
Różne wersje platformy Android używają różnych wersji SQLite. Aby używać tego samego silnika co na urządzeniu z Androidem, użyj adb shell
i uruchom sqlite3
na urządzeniu docelowym.
Możesz poprosić SQLite o zmierzenie czasu wykonywania zapytań:
sqlite> .timer on
sqlite> SELECT ...
Run Time: real ... user ... sys ...
EXPLAIN QUERY PLAN
Możesz poprosić SQLite o wyjaśnienie, w jaki sposób zamierza odpowiedzieć na zapytanie, używając tego polecenia:EXPLAIN QUERY PLAN
sqlite> EXPLAIN QUERY PLAN
SELECT id, name
FROM Customers
WHERE city = 'Paris';
QUERY PLAN
`--SCAN Customers
W poprzednim przykładzie wymagane jest pełne skanowanie 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 pasują do klientów z Paryża. Aby to naprawić, 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śnianie planów zapytań:
sqlite> .eqp on
Więcej informacji znajdziesz w artykule Planowanie zapytań.
Analizator SQLite
SQLite udostępnia interfejs wiersza poleceń sqlite3_analyzer
, który umożliwia zrzucanie dodatkowych informacji, które można wykorzystać do rozwiązywania problemów z wydajnością. Aby zainstalować SQLite, otwórz stronę pobierania SQLite.
Możesz użyć adb pull
, aby pobrać plik bazy danych z urządzenia docelowego na stację roboczą w celu analizy:
adb pull /data/data/<app_package_name>/databases/<db_name>.db
SQLite Browser
Możesz też zainstalować narzędzie GUI SQLite Browser na stronie Pobieranie SQLite.
Logowanie na Androidzie
Android mierzy czas zapytań SQLite i rejestruje je:
# Enable query time logging
$ adb shell setprop log.tag.SQLiteTime VERBOSE
# Disable query time logging
$ adb shell setprop log.tag.SQLiteTime ERROR
Śledzenie Perfetto
Podczas konfigurowania Perfetto możesz dodać te elementy, aby uwzględnić ścieżki poszczególnych zapytań:
data_sources {
config {
name: "linux.ftrace"
ftrace_config {
atrace_categories: "database"
}
}
}
dumpsys meminfo
adb shell dumpsys meminfo <package-name>
wyświetli statystyki dotyczące wykorzystania pamięci przez aplikację, w tym szczegółowe informacje o pamięci SQLite. Na przykład ten fragment pochodzi z danych wyjściowych polecenia adb shell dumpsys meminfo com.google.android.gms.persistent
na urządzeniu dewelopera:
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
W sekcji DATABASES
znajdziesz:
pgsz
: rozmiar jednej strony bazy danych w KB.dbsz
: rozmiar całej bazy danych w stronach. Aby uzyskać rozmiar w KB, pomnóżpgsz
przezdbsz
.Lookaside(b)
: pamięć przydzielona do bufora lookaside SQLite na połączenie w bajtach. Zwykle są one bardzo małe.cache hits
: SQLite utrzymuje pamięć podręczną stron bazy danych. Jest to liczba trafień w pamięci podręcznej strony.cache misses
: liczba nieudanych prób pobrania strony z pamięci podręcznej (liczba).cache size
: liczba stron w pamięci podręcznej (liczba). Aby uzyskać rozmiar w KB, pomnóż tę liczbę przezpgsz
.Dbname
: ścieżka do pliku bazy danych. W naszym przykładzie niektóre bazy danych mają w nazwie dopisany znak(1)
lub inną liczbę, co oznacza, że istnieje więcej niż jedno połączenie z tą samą bazą danych. Statystyki są śledzone dla każdego połączenia.
W sekcji POOL STATS
znajdziesz:
cache hits
: SQLite buforuje przygotowane instrukcje i próbuje ich ponownie używać podczas wykonywania zapytań, aby zaoszczędzić trochę pracy i pamięci podczas kompilowania instrukcji SQL. Jest to liczba trafień w pamięci podręcznej instrukcji (liczba).cache misses
: liczba nieudanych prób pobrania z pamięci podręcznej instrukcji (liczba).cache size
: od Androida 17 ta wartość zawiera łączną liczbę przygotowanych instrukcji w pamięci podręcznej. W starszych wersjach ta wartość jest równa sumie trafień i nietrafień wymienionych w pozostałych 2 kolumnach i nie reprezentuje rozmiaru pamięci podręcznej.
Polecane dla Ciebie
- Uwaga: tekst linku jest wyświetlany, gdy JavaScript jest wyłączony.
- Uruchamianie testów porównawczych w trybie ciągłej integracji
- Zablokowane klatki
- Tworzenie i pomiar profili podstawowych bez użycia Macrobenchmark