Sprawdzone metody dotyczące wydajności SQLite

Android oferuje wbudowane wsparcie dla SQLite, czyli wydajnej bazy danych SQL. Stosuj te sprawdzone metody, aby zoptymalizować wydajność aplikacji i utrzymać jej szybkość i stabilność wraz ze wzrostem ilości danych. Stosując te sprawdzone metody, zmniejszysz też ryzyko wystąpienia problemów z wydajnością, które są trudne do odtworzenia i rozwiązania.

Aby uzyskać większą wydajność, przestrzegaj tych zasad:

  • Czytanie mniejszej liczby wierszy i kolumn: zoptymalizuj zapytania, aby pobierać tylko niezbędne dane. Zminimalizuj ilość danych odczytywanych z bazy danych, ponieważ nadmierne pobieranie danych może wpłynąć na wydajność.

  • Przesyłanie pracy do mechanizmu SQLite: wykonywanie obliczeń, filtrowania i sortowania w ramach zapytań SQL. Korzystanie z silnika zapytań SQLite może znacznie poprawić wydajność.

  • Zmień schemat 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ść.

Możesz też użyć dostępnych narzędzi do rozwiązywania problemów, aby zmierzyć wydajność bazy danych SQLite i wyznaczyć obszary wymagające optymalizacji.

Zalecamy użycie 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łączanie logowania z zapisywaniem z wyprzedzeniem

SQLite wdraża mutacje, dołączając je do dziennika, który okresowo jest kompresowany w bazie danych. Nazywa się to zapisem z wyprzedzeniem (WAL).

Włącz WAL, chyba że używasz ATTACH DATABASE.

Zwolnij tryb synchronizacji

Podczas używania WAL domyślnie każda zmiana powoduje wysłanie fsync, aby dane dotarły na dysk. Zwiększa to trwałość danych, ale spowalnia procesy zatwierdzania.

SQLite ma opcję sterowania trybem synchronicznym. Jeśli włączysz WAL, ustaw tryb synchronizacji na NORMAL:

Kotlin

db.execSQL("PRAGMA synchronous = NORMAL")

Java

db.execSQL("PRAGMA synchronous = NORMAL");

Przy takim ustawieniu powiadomienie o zmianie może zostać zwrócone, zanim dane zostaną zapisane na dysku. Jeśli urządzenie zostanie wyłączone (np. z powodu utraty zasilania lub paniki jądra), zapisane dane mogą zostać utracone. Jednak dzięki rejestrowaniu Twoja baza danych nie jest uszkodzona.

Jeśli tylko aplikacja ulegnie awarii, dane nadal będą przesyłane na dysk. W przypadku większości aplikacji to ustawienie zapewnia poprawę wydajności bez dodatkowych kosztów.

Definiowanie wydajnych schematów tabeli

Aby zoptymalizować wydajność i zmniejszyć zużycie danych, zdefiniuj wydajny schemat tabeli. SQLite tworzy efektywne plany zapytań i dane, co prowadzi do szybszego pobierania danych. W tej sekcji znajdziesz sprawdzone metody tworzenia schematów tabel.

Rozważ INTEGER PRIMARY KEY

W tym przykładzie zdefiniuj tabelę i utwórz w niej dane 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 wyglądają tak:

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 z filtrem rowid są implementowane jako szybkie wyszukiwanie w drzewie B, ale zapytania z filtrem id są skanowane powoli.

Jeśli planujesz wyszukiwanie według wartości id, możesz zrezygnować z przechowywania kolumny rowid, aby zaoszczędzić miejsce na dane 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

Ponieważ nie musisz przechowywać kolumny rowid, zapytania id są szybkie. Pamiętaj, że tabela jest teraz sortowana według wartości id, a nie według kolejności wstawiania.

Przyspieszanie zapytań za pomocą indeksów

SQLite używa indektów do przyspieszania zapytań. Jeśli podczas filtrowania (WHERE), sortowania (ORDER BY) lub agregowania (GROUP BY) kolumny tabela ma dla niej indeks, zapytanie jest przyspieszone.

W poprzednim przykładzie filtrowanie według kolumny city wymaga skanowania całej tabeli:

SELECT id, name
WHERE city = 'London, England';

W przypadku aplikacji, która zawiera wiele zapytań dotyczących 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 przypisana do rowid:

miasto rowid
Gary, Indiana 2
Liverpool, Anglia 1
Hrabstwo Sevier, Tennessee 3

Pamiętaj, że koszt miejsca na dane w kolumnie city jest teraz podwójny, ponieważ występuje zarówno w pierwotnej tabeli, jak i w indeksie. Korzystasz z indeksu, więc koszt dodatkowego miejsca na dane jest wart korzyści płynących z szybszego wykonywania zapytań. Nie utrzymuj jednak indeksu, którego nie używasz, aby uniknąć płacenia za miejsce na dane, które nie przynosi korzyści w zakresie wydajności zapytań.

Tworzenie indeksów wielokolumnowych

Jeśli zapytania łączą wiele kolumn, możesz utworzyć indeksy wielokolumnowe, aby w pełni przyspieszyć zapytania. Możesz też użyć indeksu w kolumnie zewnętrznej i zezwolić, aby wewnętrzne wyszukiwanie było wykonywane jako skanowanie liniowe.

Na przykład w przypadku tego zapytania:

SELECT id, name
WHERE city = 'London, England'
ORDER BY city, name

Możesz przyspieszyć zapytanie, korzystając z indeksu wielokolumnowego w takim samym porządku, jak podano w zapytaniu:

CREATE INDEX city_name_index ON Customers(city, name);

Jeśli jednak masz tylko indeks na city, sortowanie zewnętrzne jest nadal przyspieszone, a sortowanie wewnętrzne wymaga skanowania liniowego.

Ta funkcja działa też w przypadku zapytań z prefiksem. Na przykład indeksON Customers (city, name) przyspiesza też filtrowanie, sortowanie i grupowanie według city, ponieważ tabela indeksu dla indeksu wielokolumnowego jest uporządkowana według podanych indeksów w danym porządku.

Rozważ WITHOUT ROWID

Domyślnie SQLite tworzy dla tabeli kolumnę rowid, gdzie rowid jest domyślną INTEGER PRIMARY KEY AUTOINCREMENT. Jeśli masz już kolumnę o nazwie 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.

przechowywać małe dane jako BLOB, a duże dane jako plik;

Jeśli chcesz powiązać z wierszem duże dane, np. miniaturę obrazu lub zdjęcie kontaktu, możesz je zapisać w kolumnie BLOB lub w pliku, a potem w kolumnie zapisać ścieżkę do pliku.

Pliki są zazwyczaj zaokrąglone do 4 KB. W przypadku bardzo małych plików, w których przypadku błąd zaokrąglenia jest znaczący, wydajniejsze jest przechowywanie ich w bazie danych jako BLOB. SQLite minimalizuje wywołania systemu plików i w niektórych przypadkach jest szybsze niż system plików.

Poprawianie wydajności zapytań

Aby zwiększyć wydajność zapytań w SQLite przez minimalizowanie czasu odpowiedzi i maksymalizowanie efektywności przetwarzania, stosuj te sprawdzone metody.

odczytywać tylko wiersze, których potrzebujesz;

Filtry umożliwiają zawężenie wyników przez określenie określonych kryteriów, takich jak zakres dat, lokalizacja lub nazwa. Limity umożliwiają kontrolowanie liczby 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()) {
    ...
  }
}

odczytywać tylko te kolumny, których potrzebujesz;

Unikaj wybierania niepotrzebnych kolumn, które mogą spowolnić zapytania i marnować zasoby. Zamiast tego wybierz tylko kolumny, które są używane.

W tym przykładzie 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);
    ...
  }
}

Parametryzuj zapytania za pomocą kart SQL, a nie przez konkatenację ciągu znaków.

Ciąg zapytania może zawierać parametr, który jest znany dopiero w czasie wykonywania, 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 można korzystać z pamięci podręcznej instrukcji. Każde wywołanie wymaga skompilowania kodu 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ć raz i zapisać w pamięci podręcznej. Kompilowane zapytanie jest używane ponownie w różnych wywołaniach funkcji getNameById(long).

Iteruj w kodzie SQL, a nie w kodzie programu

Zamiast pętli programowej iterującej zapytania SQL w celu zwrócenia poszczególnych wyników, użyj pojedynczego zapytania zwracającego wszystkie wyszukiwane wyniki. Automatyczny cykl jest około 1000 razy wolniejszy niż pojedyncze zapytanie SQL.

Użyj kolumny DISTINCT do przechowywania unikalnych wartości

Użycie słowa kluczowego DISTINCT może poprawić wydajność 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
    ...
  }
}

Stosuj funkcje agregujące, gdy tylko jest to możliwe

Używaj funkcji agregacji do wyników zbiorczych bez danych wierszy. Na przykład kod poniżej sprawdza, czy istnieje co najmniej 1 wiersz pasujący do kryteriów:

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 zwróci wartość 0, jeśli pasujący wiersz nie istnieje, i 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żywaj w kodzie aplikacji funkcji agregacji SQLite:

  • COUNT: zlicza, ile wierszy jest w kolumnie.
  • SUM: dodaje wszystkie wartości liczbowe w kolumnie.
  • MIN lub MAX: określają one najmniejszą lub najwyższą wartość. Działa w przypadku kolumn liczbowych, typów DATE i typów tekstowych.
  • AVG: zwraca średnią wartość liczbową.
  • GROUP_CONCAT: łączenie ciągów znaków za pomocą opcjonalnego separatora.

Użyj COUNT() zamiast Cursor.getCount().

W tym przykładzie funkcja Cursor.getCount() odczytuje wszystkie wiersze z bazy danych i zwróci wszystkie ich wartości:

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 przy użyciu 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 do Nest zamiast kodu,

SQL jest kompozytywny i obsługuje zapytania w nawiasach klamrowych, złączenia i ograniczenia kluczy obcych. Wyniki jednego zapytania możesz wykorzystać w innym bez konieczności korzystania z aplikacji. Dzięki temu nie trzeba kopiować danych z SQLite, a silnik bazy danych może zoptymalizować zapytanie.

W tym przykładzie możesz uruchomić zapytanie, aby znaleźć miasto z największą liczbą klientów, a potem użyć tego 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 w poprzednim przykładzie, użyj pojedynczego zapytania SQL z zagnieszkami:

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, dopóki dana wartość kolumny nie jest unikalna w tabeli, może być bardziej efektywne wymuszenie tej unikalności jako ograniczenia kolumny.

W tym przykładzie jedno zapytanie służy do sprawdzenia wiersza, który ma zostać wstawiony, a drugie – do jego faktycznego wstawienia:

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 Kotlinie lub Javie, 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:

CREATE TABLE Customers(...);
CREATE UNIQUE INDEX CustomersUsername ON Customers(username);

Teraz możesz wstawić wiersz i pozwolić SQLite na sprawdzenie ograniczenia:

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 z mniejszym obciążeniem niż kod Kotlin lub Java. Sprawdzoną metodą jest używanie SQLite zamiast kodu aplikacji.

Przeprowadzanie wielu wstawień w ramach jednej transakcji

Transakcja powoduje wykonanie wielu operacji, co poprawia nie tylko wydajność, ale też poprawność. Aby zwiększyć spójność danych i przyspieszyć skuteczność, możesz grupować w paczkach:

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 ułatwiają pomiar wydajności.

Używanie interaktywnego promptu SQLite

Uruchom SQLite na swoim komputerze, aby wykonywać zapytania i uczyć się. Różne wersje platformy Android używają różnych wersji SQLite. Aby użyć tego samego silnika, którego używa się 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, jak zamierza odpowiedzieć na zapytanie, używając EXPLAIN QUERY PLAN:

sqlite> EXPLAIN QUERY PLAN
SELECT id, name
FROM Customers
WHERE city = 'Paris';
QUERY PLAN
`--SCAN Customers

W poprzednim przykładzie znalezienie wszystkich klientów z Paryża wymaga pełnego skanowania tabeli bez indeksu. Jest to tzw. złożoność liniowa. SQLite musi odczytać wszystkie wiersze i zatrzymać 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 korzystasz z interaktywnej powłoki, możesz poprosić SQLite, aby zawsze wyświetlało plany zapytań:

sqlite> .eqp on

Więcej informacji znajdziesz w artykule Planowanie zapytań.

SQLite Analyzer

SQLite udostępnia interfejs wiersza poleceń sqlite3_analyzer, który umożliwia zapisywanie dodatkowych informacji, które można wykorzystać do rozwiązywania problemów z wydajnością. Aby zainstalować SQLite, otwórz stronę pobierania SQLite.

Za pomocą polecenia adb pull możesz pobrać plik bazy danych z urządzenia docelowego na stację roboczą w celu przeanalizowania:

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 w Androidzie

Android rejestruje czas wykonywania zapytań SQLite i je loguje:

# 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 usługi Peretto możesz dodać te elementy, aby uwzględnić ścieżki dla poszczególnych zapytań:

data_sources {
  config {
    name: "linux.ftrace"
    ftrace_config {
      atrace_categories: "database"
    }
  }
}