Sprawdzone metody dotyczące wydajności SQLite

Android ma wbudowaną obsługę SQLite, i efektywną bazę danych SQL. Stosuj te sprawdzone metody, aby zoptymalizować działanie aplikacji i utrzymać jej szybkość i stabilność wraz ze wzrostem ilości danych. Korzystając z tych sprawdzonych metod, zmniejszasz także ryzyko napotykasz problemy z wydajnością, które są trudne do odtworzenia; i rozwiązuj problemy.

Aby szybciej osiągnąć skuteczność, przestrzegaj tych zasad:

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

  • Przekazywanie zadań do silnika SQLite: wykonywanie obliczeń, filtrowanie i sortowanie w ramach zapytań SQL. Korzystanie z silnika zapytań SQLite może znacznie poprawić wydajność.

  • Zmodyfikuj schemat bazy danych: zaprojektuj schemat bazy danych, by ułatwić SQLite tworzyć wydajne plany zapytań i reprezentacje danych. Prawidłowe indeksowanie tabel i optymalizuj ich struktury, aby zwiększyć wydajność.

Możesz też skorzystać z dostępnych narzędzi do rozwiązywania problemów, aby mierzyć: i wydajność bazy danych SQLite, ułatwiając identyfikację obszarów wymagających optymalizacji.

Zalecamy korzystanie z biblioteki Jetpack Room.

Konfigurowanie bazy danych pod kątem wydajności

Wykonaj czynności opisane w tej sekcji, aby skonfigurować bazę danych pod kątem optymalizacji i wydajność w SQLite.

Włączanie logowania z wyprzedzeniem

SQLite implementuje mutacje, dołączając je do logu, który od czasu do czasu kompaktowe do bazy danych. Nazywa się to logowaniem z wyprzedzeniem (WAL).

Włącz zapisywanie logów z wyprzedzeniem chyba że korzystasz z ATTACH DATABASE.

Rozluźnij tryb synchronizacji

W przypadku korzystania z WAL domyślnie każde zatwierdzenie generuje żądanie fsync, aby zapewnić dane docierają do dysku. Zwiększa to trwałość danych, ale spowalnia proces zatwierdzania.

SQLite udostępnia opcję kontroli synchronicznej . Jeśli włączysz WAL, ustaw tryb synchroniczny 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 może nastąpić wyłączenie urządzenia, np. w przypadku utraty zasilania lub paniki w wyniku działania jądra systemu możesz utracić zatwierdzone dane. Jednak dzięki rejestrowaniu 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 tabel

Aby zoptymalizować wydajność i zminimalizować 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 tabeli.

Weź pod uwagę 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 będą wyglądać tak:

Rowid (identyfikator wiersza) id nazwa miasto
1 456 John Lennon Liverpool, Anglia
2 123 Michaela Jacksona Gary, Indiana
3 789 Wózek Parton Sevier County, Tennessee

Kolumna rowid to indeksu, który zachowuje zamówienie reklamowe. Zapytania z filtrem rowid są implementowane jako szybkie wyszukiwanie w drzewie B, ale zapytania z filtrem id są wykonywane jako wolne skanowanie tabeli.

Jeśli planujesz wyszukiwanie do id, możesz uniknąć przechowywania rowid oznacza mniej danych w pamięci i ogólną szybszej bazy danych:

CREATE TABLE Customers(
  id INTEGER PRIMARY KEY,
  name TEXT,
  city TEXT
);

Twoja tabela wygląda teraz tak:

id nazwa miasto
123 Michaela Jacksona Gary, Indiana
456 John Lennon Liverpool, Anglia
789 Wózek Parton Sevier County, Tennessee

Ponieważ nie musisz przechowywać kolumny rowid, zapytania dotyczące id są szybkie. Notatka że tabela jest teraz posortowana według parametru id, a nie zamówienia reklamowego.

Przyspieszanie zapytań za pomocą indeksów

Zastosowania SQLite indeksy aby przyspieszyć zapytania. Podczas filtrowania (WHERE), sortowania (ORDER BY) lub agreguje (GROUP BY) kolumnę, jeśli tabela ma dla niej indeks, zapytania są przyspieszone.

W poprzednim przykładzie filtrowanie według pola city wymaga przeskanowania 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

Zwróć uwagę, że koszt miejsca na dane w kolumnie city jest teraz podwójny, ponieważ jest to obecny zarówno w pierwotnej tabeli, 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 miejsce na dane, które nie przynosi korzyści w zakresie wydajności zapytań.

Tworzenie indeksów wielokolumnowych

Jeśli Twoje 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 pozwolić, aby wyszukiwanie wewnątrz zostało wykonane jako skanowanie liniowe.

Na przykład 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, określone w zapytaniu:

CREATE INDEX city_name_index ON Customers(city, name);

Jeśli jednak masz tylko indeks dotyczący bazy danych city, kolejność zewnętrzna pozostaje bez zmian. z przyspieszeniem, podczas gdy kolejność wewnętrzna wymaga skanowania liniowego.

Działa to też w przypadku zapytań z prefiksem. Na przykład indeks ON Customers (city, name) przyspiesza też filtrowanie, porządkowanie i grupowanie według city, ponieważ tabela indeksu dla indeksu wielokolumnowego jest posortowana według dla określonych indeksów w podanej kolejności.

Rozważ WITHOUT ROWID.

Domyślnie SQLite tworzy w tabeli kolumnę rowid, w której rowid jest wartością domyślnie INTEGER PRIMARY KEY AUTOINCREMENT. Jeśli masz już kolumnę, w której jest INTEGER PRIMARY KEY, ta kolumna zostanie aliasem domeny rowid.

W przypadku tabel, które mają klucz podstawowy inny niż INTEGER lub złożony kolumny, rozważ WITHOUT ROWID.

Przechowuj małe dane jako BLOB, a duże jako plik

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

Pliki są zwykle zaokrąglane do 4 KB. W przypadku bardzo małych plików, gdzie błąd zaokrąglania jest istotny, więc lepiej przechowywać je w jako BLOB. SQLite minimalizuje wywołania systemu plików i jest szybszy niż bazowego systemu plików ale nie zawsze tak jest.

Zwiększ wydajność zapytań

Aby poprawić wydajność zapytań w SQLite, postępuj zgodnie z tymi sprawdzonymi metodami, które pozwolą zminimalizować czas odpowiedzi i zmaksymalizować wydajność przetwarzania.

Odczytaj tylko potrzebne wiersze

Filtry pozwalają zawęzić wyniki przez określenie określonych kryteriów, np. zakres dat, lokalizacja lub nazwa. Limity pozwalają kontrolować liczbę wyników, które widzisz:

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 zaznaczania zbędnych kolumn, ponieważ może to spowodować spowalniać zapytania i marnować zasoby. Zamiast tego wybierz tylko kolumny w ich wykorzystaniu.

W poniższym 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);
    ...
  }
}

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);
    ...
  }
}

Parametrowanie zapytań za pomocą kart SQL, a nie konkatenacji ciągu znaków

Ciąg zapytania może zawierać parametr, który jest znany tylko w czasie działania, np. w następujący sposób:

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 poprzednim kodzie każde zapytanie tworzy inny ciąg znaków, przez co nie można korzystać z pamięci podręcznej instrukcji. Każde wywołanie wymaga do skompilowania kodu SQLite przed wykonaniem. Zamiast tego możesz zamienić argument id na parameter oraz powiąż wartość za pomocą algorytmu 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. Skompilowane zapytanie jest ponownie wykorzystywane między różnymi wywołaniami funkcji getNameById(long).

Wykonuj iteracje w SQL, a nie w kodzie

Użyj jednego zapytania, które zwraca wszystkie kierowane wyniki, zamiast wyników automatycznych zapętlanie zapytań SQL w celu zwrócenia poszczególnych wyników. Automatyzacja jest około 1000 razy wolniejsza niż pojedyncze zapytanie SQL.

Użyj wartości DISTINCT w przypadku wartości unikalnych

Użycie słowa kluczowego DISTINCT może zwiększyć skuteczność zapytań o zmniejszając ilość danych do przetworzenia. Na przykład: aby zwrócić tylko unikalne wartości z kolumny, użyj 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

Używaj funkcji agregacji do agregacji wyników bez danych wierszy. Na przykład parametr 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 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 znajduje się 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 wartości liczbowych kolumny, DATE typy i typy tekstu.
  • AVG: znajduje średnią wartość liczbową.
  • GROUP_CONCAT: łączy ciągi znaków za pomocą opcjonalnego separatora.

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

W z tego przykładu, 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 metody COUNT(), baza danych zwróci tylko liczba:

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 Nest zamiast kodu

SQL można komponować i obsługiwać podzapytania, złączenia oraz ograniczenia kluczy obcych. Wynik jednego zapytania możesz wykorzystać w innym bez konieczności korzystania z kodu aplikacji. Zmniejsza to potrzebę kopiowania danych z SQLite i umożliwia bazie danych do optymalizacji zapytania.

W poniższym przykładzie możesz uruchomić zapytanie, aby sprawdzić, w którym mieście klientów, użyj go w kolejnym zapytaniu, by znaleźć wszystkich klientów to miasto:

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 porównaniu z poprzednim przykładem, 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()) {
    ...
  }
}

Sprawdzanie unikalności w SQL

Jeśli nie można wstawić wiersza, o ile dana wartość kolumny nie jest unikalna w tabeli, lepiej wykorzystać tę niepowtarzalność w formie kolumny. .

W poniższym przykładzie wykonywane jest jedno zapytanie w celu sprawdzenia, czy wiersz i drugi w celu 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ć unikalne ograniczenie w Kotlin lub Javie, możesz je sprawdzić 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 Kotlin i Javie. w kodzie. Sprawdzoną metodą jest używanie SQLite zamiast kodu aplikacji.

Zbiorcze umieszczanie wielu wstawienia w jednej transakcji

Transakcja zatwierdza wiele operacji, przez co poprawia brak ale i jej poprawność. Aby poprawić spójność danych możesz zwiększyć skuteczność, możesz zbiorczo wstawiać:

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 następujące 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żyć funkcji w tym samym mechanizmie co w urządzeniu z Androidem, używaj funkcji adb shell i uruchom sqlite3 na urządzeniu docelowym.

Możesz poprosić SQLite o zapytania o czas:

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 skanowania całej tabeli bez indeksu. Jest to tzw. złożoność liniowa. SQLite musi odczytać ze wszystkich wierszy i zachowaj tylko te wiersze, 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, aby zawsze wyjaśniał plany zapytań:

sqlite> .eqp on

Więcej informacji: Planowanie zapytań.

SQLite Analyzer

SQLite zapewnia sqlite3_analyzer interfejsu wiersza poleceń (CLI) do pobierania dodatkowych informacji, które mogą być używane i rozwiązywać problemy z wydajnością. Aby ją zainstalować, wejdź na Strona pobierania SQLite.

Za pomocą adb pull możesz pobrać plik bazy danych z urządzenia docelowego na stacja robocza do analizy:

adb pull /data/data/<app_package_name>/databases/<db_name>.db

Przeglądarka SQLite

Możesz również zainstalować narzędzie GUI Przeglądarka SQLite w SQLite Strona Pobrane pliki

Logowanie w Androidzie

Android wysyła zapytania SQLite do użytkowników 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"
    }
  }
}