Sprawdzone metody dotyczące wydajności SQLite

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 BLOBkolumnie 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, namephone:

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 lub MAX: określa najniższą lub najwyższą wartość. Działa w przypadku kolumn liczbowych, typów DATE 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 przez dbsz.
  • 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ę przez pgsz.
  • 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.