Best Practices für die SQLite-Leistung

Android bietet integrierte Unterstützung für SQLite, ein eine effiziente SQL-Datenbank. Mit diesen Best Practices können Sie und dafür sorgen, dass sie auch bei zunehmenden Datenmengen schnell und vorhersehbar schnell bleibt. Mit diesen Best Practices verringern Sie auch die Wahrscheinlichkeit, Es stößt auf Leistungsprobleme, die sich nur schwer reproduzieren lassen, zur Fehlerbehebung.

Beachten Sie die folgenden Leistungsprinzipien, um eine höhere Leistung zu erzielen:

  • Weniger Zeilen und Spalten anzeigen: Optimieren Sie Ihre Abfragen, um nur die notwendige Daten. Minimieren Sie die aus der Datenbank gelesene Datenmenge, kann sich ein übermäßiger Datenabruf negativ auf die Leistung auswirken.

  • Push-Arbeit an SQLite-Engine: Berechnungen, Filterung und Sortierung durchführen Operationen innerhalb der SQL-Abfragen. Die Verwendung der Abfrage-Engine von SQLite kann erheblich die Leistung zu verbessern.

  • Datenbankschema ändern: Datenbankschema zur Unterstützung von SQLite entwerfen effiziente Abfragepläne und Datendarstellungen zu erstellen. Tabellen richtig indexieren und optimieren Sie Tabellenstrukturen, um die Leistung zu verbessern.

Außerdem können Sie mit den verfügbaren Tools zur Fehlerbehebung SQLite-Datenbank, um Bereiche zu identifizieren, Optimierung.

Wir empfehlen die Verwendung der Jetpack Room-Bibliothek.

Datenbank für Leistung konfigurieren

Führen Sie die Schritte in diesem Abschnitt aus, um Ihre Datenbank für eine optimale die Performance in SQLite.

Write-Ahead-Logging aktivieren

SQLite implementiert Mutationen durch Anhängen an ein Log, was gelegentlich in die Datenbank komprimiert. Dies wird als Write-Ahead-Logging (WAL) angeben.

Aktivieren WAL außer Sie verwenden ATTACH DATABASE.

Synchronisierungsmodus lockern

Bei Verwendung von WAL wird standardmäßig bei jedem Commit ein fsync ausgegeben, um sicherzustellen, erreichen die Daten das Laufwerk. Dies verbessert die Langlebigkeit der Daten, verlangsamt aber Commits übergeben.

SQLite bietet eine Option, um synchrone Modus an. Wenn Sie Aktivieren Sie WAL und setzen Sie den synchronen Modus auf NORMAL:

Kotlin

db.execSQL("PRAGMA synchronous = NORMAL")

Java

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

Bei dieser Einstellung kann ein Commit zurückgegeben werden, bevor die Daten auf einem Laufwerk gespeichert sind. Wenn ein wenn das Gerät heruntergefahren wird, beispielsweise bei Stromausfall oder bei einer Kernel-Panic, übergebene Daten verloren gehen. Aufgrund des Loggings wird Ihre Datenbank korrumpiert.

Wenn nur Ihre App abstürzt, erreichen Ihre Daten das Laufwerk weiterhin. Bei den meisten Apps ohne materielle Kosten Leistungsverbesserungen zu erzielen.

Effiziente Tabellenschemas definieren

Um die Leistung zu optimieren und den Datenverbrauch zu minimieren, definieren Sie eine effiziente Tabellenschema. SQLite erstellt effiziente Abfragepläne und Daten, was zu schnelleren Datenabruf. Dieser Abschnitt enthält Best Practices zum Erstellen einer Tabelle Schemas.

INTEGER PRIMARY KEY in Betracht ziehen

Definieren Sie für dieses Beispiel eine Tabelle und füllen Sie sie so aus:

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');

Die Tabellenausgabe sieht so aus:

Rowid (Zeilen-ID) id Name Stadt
1 456 John Lennon Liverpool, England
2 123 Michael Jackson Gary, IN
3 789 Dolly Parton Sevier County, Tennessee

Die Spalte rowid ist Index, der den Anzeigenauftrag beibehält. Suchanfragen, die nach rowid filtern, werden als schnelle B-Baum-Suche implementiert. Suchanfragen, die Nach id filtern ist ein langsamer Tabellenscan.

Wenn Sie Suchvorgänge bis zum id durchführen möchten, können Sie das Speichern des rowid-Spalte für weniger Daten im Speicher und eine schnellere Datenbank:

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

Ihre Tabelle sieht jetzt so aus:

id Name Stadt
123 Michael Jackson Gary, IN
456 John Lennon Liverpool, England
789 Dolly Parton Sevier County, Tennessee

Da Sie die Spalte rowid nicht speichern müssen, sind id-Abfragen schnell. Hinweis , dass die Tabelle jetzt basierend auf id und nicht nach dem Anzeigenauftrag sortiert ist.

Abfragen mit Indexen beschleunigen

SQLite nutzt Indexe um Abfragen zu beschleunigen. Beim Filtern (WHERE), Sortieren (ORDER BY) oder Beim Aggregieren (GROUP BY) einer Spalte, wenn die Tabelle einen Index für die Spalte hat, Abfrage beschleunigt wird.

Im vorherigen Beispiel muss zum Filtern nach city die gesamte Tabelle gescannt werden:

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

Bei einer Anwendung mit vielen Suchanfragen nach Städten können Sie diese Abfragen mit einem Index:

CREATE INDEX city_index ON Customers(city);

Ein Index wird als zusätzliche Tabelle implementiert, sortiert nach der Indexspalte zugeordnet zu rowid:

Stadt Rowid (Zeilen-ID)
Gary, IN 2
Liverpool, England 1
Sevier County, Tennessee 3

Die Speicherkosten der Spalte city sind jetzt doppelt so hoch, die jetzt sowohl in der Originaltabelle als auch im Index vorhanden sind. Da Sie die Methode Index ist, sind die Kosten für zusätzlichen Speicher den Nutzen schnellerer Abfragen wert. Pflegen Sie jedoch keinen Index, den Sie nicht nutzen, wenn keine Steigerung der Abfrageleistung erforderlich ist.

Mehrspaltige Indexe erstellen

Wenn Ihre Abfragen mehrere Spalten umfassen, können Sie mehrspaltige Abfragen Indexe um die Abfrage vollständig zu beschleunigen. Sie können einen Index auch für eine externe Spalte verwenden und lassen Sie die Suche innerhalb als linearen Scan durchführen.

Nehmen wir zum Beispiel die folgende Abfrage:

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

Sie können die Abfrage mit einem mehrspaltigen Index in der gleichen Reihenfolge wie in der Abfrage angegeben:

CREATE INDEX city_name_index ON Customers(city, name);

Wenn Sie jedoch nur einen Index für city haben, ist die externe Sortierung weiterhin beschleunigt werden, während für die innere Sortierung ein linearer Scan erforderlich ist.

Dies funktioniert auch mit Präfixanfragen. Beispiel: Ein Index ON Customers (city, name) beschleunigt auch das Filtern, Sortieren und Gruppieren. nach city, da die Indextabelle für einen mehrspaltigen Index nach dem in der angegebenen Reihenfolge angezeigt werden sollen.

WITHOUT ROWID in Betracht ziehen

Standardmäßig erstellt SQLite eine rowid-Spalte für Ihre Tabelle, wobei rowid ein implizites INTEGER PRIMARY KEY AUTOINCREMENT Wenn Sie bereits eine Spalte haben, INTEGER PRIMARY KEY ist, wird diese Spalte zu einem Alias von rowid.

Für Tabellen, die einen anderen Primärschlüssel als INTEGER haben, oder eine Zusammensetzung aus Spalten finden Sie WITHOUT ROWID.

Kleine Daten als BLOB und große Daten als Datei speichern

Wenn Sie große Datenmengen mit einer Zeile verknüpfen möchten, z. B. die Miniaturansicht eines Bildes oder ein Foto für einen Kontakt, können Sie die Daten entweder in der Spalte BLOB oder in und speichern Sie dann den Dateipfad in der Spalte.

Dateien werden in der Regel in Schritten von 4 KB aufgerundet. Bei sehr kleinen Dateien, der Rundungsfehler erheblich ist, ist es effizienter, sie in der Datenbank als BLOB. SQLite minimiert Dateisystemaufrufe und ist schneller als die zugrunde liegendes Dateisystem .

Abfrageleistung verbessern

Befolgen Sie diese Best Practices, um die Abfrageleistung in SQLite zu verbessern, indem Sie Reaktionszeiten und maximale Verarbeitungseffizienz.

Nur die benötigten Zeilen lesen

Mit Filtern können Sie die Ergebnisse eingrenzen, indem Sie bestimmte Kriterien, wie Zeitraum, Standort oder Name. Mit Limits können Sie die Anzahl der der angezeigten Ergebnisse:

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()) {
    ...
  }
}

Nur die benötigten Spalten lesen

Vermeiden Sie die Auswahl nicht benötigter Spalten, Abfragen verlangsamen und Ressourcen verschwenden. Wählen Sie stattdessen nur Spalten aus, die verwendet werden.

Im folgenden Beispiel wählen Sie id, name und phone aus:

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

Sie benötigen jedoch nur die Spalte 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);
    ...
  }
}

DISTINCT für eindeutige Werte verwenden

Mit dem Keyword DISTINCT können Sie die Leistung Ihrer Suchanfragen verbessern, indem Sie die zu verarbeitende Datenmenge reduziert wird. Wenn Sie zum Beispiel Um nur die eindeutigen Werte aus einer Spalte zurückzugeben, verwenden Sie 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
    ...
  }
}

Nach Möglichkeit Aggregatfunktionen verwenden

Verwenden Sie Aggregatfunktionen für aggregierte Ergebnisse ohne Zeilendaten. Beispiel: Der Parameter Der folgende Code prüft, ob mindestens eine übereinstimmende Zeile vorhanden ist:

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

Um nur die erste Zeile abzurufen, können Sie EXISTS() verwenden, um 0 zurückzugeben, wenn eine Übereinstimmung Zeile nicht vorhanden und 1, wenn eine oder mehrere Zeilen übereinstimmen:

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

SQLite-Aggregat verwenden Funktionen in Ihrer App Code:

  • COUNT: zählt, wie viele Zeilen sich in einer Spalte befinden.
  • SUM: Addiert alle numerischen Werte in einer Spalte.
  • MIN oder MAX: Hiermit wird der niedrigste oder höchste Wert festgelegt. Funktioniert für numerische Spalten DATE- und Texttypen.
  • AVG: Ermittelt den numerischen durchschnittlichen Wert.
  • GROUP_CONCAT: verkettet Strings mit einem optionalen Trennzeichen.

COUNT() statt Cursor.getCount() verwenden

Im im folgenden Beispiel Cursor.getCount()-Funktion liest alle Zeilen aus der Datenbank und gibt alle Zeilenwerte zurück:

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

Bei Verwendung von COUNT() gibt die Datenbank jedoch nur die Anzahl:

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

Nest-Abfragen anstelle von Code

SQL ist zusammensetzbar und unterstützt Unterabfragen, Joins und Fremdschlüsseleinschränkungen. Sie können das Ergebnis einer Abfrage in einer anderen Abfrage verwenden, ohne die App Code. Dadurch müssen Daten nicht mehr aus SQLite kopiert werden und die Datenbank kann Ihre Suchanfrage zu optimieren.

Im folgenden Beispiel können Sie eine Abfrage ausführen, um zu ermitteln, in welcher Stadt Kundschaft finden, verwenden Sie dann das Ergebnis einer anderen Abfrage, um alle Kunden diese Stadt:

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()) {
          ...
        }
    }
  }
}

Um das Ergebnis in der Hälfte der Zeit des vorherigen Beispiels zu erhalten, verwenden Sie ein einzelnes SQL mit verschachtelten Anweisungen:

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()) {
    ...
  }
}

Eindeutigkeit in SQL prüfen

Wenn eine Zeile nur eingefügt werden darf, wenn ein bestimmter Spaltenwert im ist es möglicherweise effizienter, diese Eindeutigkeit als Spalte Einschränkung.

Im folgenden Beispiel wird eine Abfrage ausgeführt, um zu validieren, dass die Zeile eingefügt und eine weitere zum Einfügen:

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

Anstatt die eindeutige Einschränkung in Kotlin oder Java zu prüfen, können Sie sie in SQL beim Definieren der Tabelle:

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

SQLite tut dasselbe wie:

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

Jetzt können Sie eine Zeile einfügen und SQLite die Einschränkung prüfen lassen:

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 unterstützt eindeutige Indexe mit mehreren Spalten:

CREATE TABLE table(...);
CREATE UNIQUE INDEX unique_table ON table(column1, column2, ...);

SQLite validiert Einschränkungen schneller und mit weniger Aufwand als Kotlin oder Java Code. Es empfiehlt sich, anstelle von App-Code SQLite zu verwenden.

Mehrere Einfügungen in einer einzigen Transaktion im Batch ausführen

Eine Transaktion führt mehrere Operationen durch, was verbessert, sondern auch auf Richtigkeit. Um die Datenkonsistenz und und die Leistung zu beschleunigen, können Sie das Einfügen im Batch tun:

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()
}

Tools zur Fehlerbehebung verwenden

SQLite bietet die folgenden Tools zur Fehlerbehebung, um die Leistung zu messen.

Interaktiven Prompt von SQLite verwenden

SQLite auf Ihrem Computer ausführen, um Abfragen auszuführen und Neues zu lernen Verschiedene Android-Plattformversionen verwenden unterschiedliche Versionen von SQLite. Zur Verwendung die auf einem Android-Gerät läuft, verwenden Sie adb shell und sqlite3 auf deinem Zielgerät ausführen.

Sie können SQLite bitten, Abfragen zu messen:

sqlite> .timer on
sqlite> SELECT ...
Run Time: real ... user ... sys ...

EXPLAIN QUERY PLAN

Sie können SQLite bitten, zu erklären, wie eine Abfrage beantwortet werden soll, indem Sie die EXPLAIN QUERY PLAN:

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

Im vorherigen Beispiel ist ein vollständiger Tabellenscan ohne Index erforderlich, um alle aus Paris zu finden. Dies wird als lineare Komplexität bezeichnet. SQLite muss lesen alle Zeilen und behalten nur die Zeilen bei, die mit Kunden aus Paris übereinstimmen. Um das Problem zu beheben können Sie einen Index hinzufügen:

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=?

Wenn Sie die interaktive Shell verwenden, können Sie SQLite bitten, Abfragepläne:

sqlite> .eqp on

Weitere Informationen finden Sie unter Abfrageplanung

SQLite-Analysetool

SQLite bietet sqlite3_analyzer Befehlszeilenschnittstelle (CLI) verwenden, um zusätzliche Informationen auszulesen, die für Probleme mit der Leistung zu beheben. Zur Installation besuche die SQLite-Downloadseite

Mit adb pull können Sie eine Datenbankdatei von einem Zielgerät auf Ihr Workstation für die Analyse:

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

SQLite-Browser

Sie können auch das GUI-Tool installieren, SQLite Browser auf SQLite Downloadseite.

Android-Logging

Android synchronisiert SQLite-Abfragen und protokolliert sie für Sie:

# 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"
    }
  }
}