Datenbankindizes in Android-Apps
Datenbankindizes beschleunigen gezielte Abfragen. Du lernst, wann sie helfen und welche Kosten sie in Android-Apps verursachen.
Datenbankindizes sind ein kleines Thema mit großer Wirkung auf die gefühlte Qualität deiner App. Wenn eine Liste beim Tippen in ein Suchfeld ruckelt, ein Offline-Feed zu lange lädt oder ein Repository im Data Layer regelmäßig dieselbe Tabelle durchsucht, kann ein passender Index den Unterschied zwischen flüssiger Nutzung und sichtbarer Wartezeit machen. Gleichzeitig ist ein Index kein kostenloser Performance-Schalter. Du setzt ihn dort ein, wo gemessene Abfragemuster eine schnellere Suche brauchen.
Was ist das?
Ein Datenbankindex ist eine zusätzliche Struktur, die der Datenbank hilft, bestimmte Zeilen schneller zu finden. Stell dir nicht die Tabelle selbst als sortierte Liste vor. Eine Tabelle kann viele Spalten und sehr viele Zeilen haben. Ohne passenden Index muss SQLite bei manchen Abfragen viele Einträge prüfen, bis die gesuchten Daten gefunden sind. Mit einem Index kann die Datenbank gezielter springen, ähnlich wie du in einem Stichwortverzeichnis schneller zur richtigen Seite kommst als durch vollständiges Lesen eines Buchs.
Im Android-Kontext geht es meist um SQLite, oft über Room. Room versteckt viele Details der Datenbankarbeit, aber es nimmt dir die fachliche Entscheidung nicht ab: Welche Abfragen kommen häufig vor? Welche Spalten werden in WHERE, ORDER BY oder JOIN genutzt? Welche Datenmenge kann lokal entstehen? Diese Fragen gehören in den Data Layer, weil dort Entities, DAOs, Repositories und Synchronisationslogik zusammenlaufen.
Für Anfänger ist das wichtigste mentale Modell: Ein Index beschleunigt bestimmte Lesezugriffe, macht aber Schreibzugriffe teurer. Beim Einfügen, Aktualisieren oder Löschen muss die Datenbank nicht nur die Tabelle ändern, sondern auch die Indexstruktur pflegen. Deshalb legst du nicht auf jede Spalte einen Index. Du wählst bewusst aus.
Wie funktioniert es?
SQLite kann für eine Abfrage verschiedene Ausführungspläne wählen. Wenn deine Query etwa alle Aufgaben eines Projekts nach projectId sucht, kann ein Index auf projectId helfen. Die Datenbank nutzt dann nicht blind jede Zeile, sondern eine strukturierte Abkürzung. Bei zusammengesetzten Abfragen kann auch ein zusammengesetzter Index sinnvoll sein, etwa auf projectId und updatedAt, wenn du häufig Aufgaben eines Projekts nach Änderungszeit sortierst.
In Room deklarierst du Indexe direkt an der Entity. Das macht die Entscheidung sichtbar und reviewbar. Ein einzelner Index sieht anders aus als ein zusammengesetzter Index. Wichtig ist die Reihenfolge bei zusammengesetzten Indexen: Ein Index auf projectId, updatedAt passt gut zu Abfragen, die zuerst nach projectId filtern und danach nach updatedAt sortieren oder weiter einschränken. Er ist nicht automatisch gleich gut für jede Abfrage, die nur updatedAt nutzt.
Im Alltag tauchen Indexe an mehreren Stellen auf. In einer Compose-App beobachtest du vielleicht einen Flow<List<Task>> aus Room und zeigst die Daten in einer Liste. Wenn die Query dahinter bei jeder Änderung langsam ist, wirkt sich das direkt auf UI-Aktualisierung, Scrollverhalten und Akkuverbrauch aus. In einer Offline-first-App wächst die lokale Datenbank oft über Wochen oder Monate. Eine Query, die mit 50 Testdatensätzen unauffällig ist, kann mit 20.000 synchronisierten Einträgen problematisch werden.
Performance ist dabei kein Bauchgefühl. Du solltest messen: mit realistischen Seed-Daten, mit Instrumentation Tests, mit Logging rund um kritische DAO-Aufrufe oder mit SQLite-Werkzeugen wie EXPLAIN QUERY PLAN, wenn du den verwendeten Plan verstehen willst. Auch Code-Review hilft: Wenn jemand eine neue DAO-Methode mit Filter auf eine bisher nicht indizierte Spalte einführt, ist das ein guter Moment für die Frage, ob diese Abfrage häufig genug ist, um einen Index zu rechtfertigen.
In der Praxis
Angenommen, deine App verwaltet Aufgaben, die offline verfügbar sein müssen. Die Startansicht zeigt alle offenen Aufgaben eines Projekts, sortiert nach Aktualisierungszeit. Diese Abfrage läuft häufig: beim Öffnen des Projekts, nach Sync-Ergebnissen und nach lokalen Änderungen.
@Entity(
tableName = "tasks",
indices = [
Index(value = ["projectId", "updatedAt"]),
Index(value = ["syncState"])
]
)
data class TaskEntity(
@PrimaryKey val id: String,
val projectId: String,
val title: String,
val done: Boolean,
val updatedAt: Long,
val syncState: String
)
@Dao
interface TaskDao {
@Query(
"""
SELECT * FROM tasks
WHERE projectId = :projectId AND done = 0
ORDER BY updatedAt DESC
"""
)
fun observeOpenTasks(projectId: String): Flow<List<TaskEntity>>
@Query(
"""
SELECT * FROM tasks
WHERE syncState = :state
"""
)
suspend fun findTasksForSync(state: String): List<TaskEntity>
}
Der Index auf projectId und updatedAt passt zur Liste offener Aufgaben pro Projekt. Der Index auf syncState passt zur Synchronisation, wenn deine App regelmäßig lokale Änderungen sucht, die noch hochgeladen werden müssen. Trotzdem fehlt hier eine bewusste Prüfung: Die Query filtert zusätzlich auf done. Je nach Datenverteilung kann ein anderer zusammengesetzter Index besser sein, zum Beispiel projectId, done, updatedAt. Das entscheidest du nicht nach Gefühl, sondern anhand deiner echten Abfragen und Datenmengen.
Eine brauchbare Entscheidungsregel lautet: Lege einen Index an, wenn eine Abfrage häufig ausgeführt wird, eine relevante Datenmenge durchsucht und messbar zu langsam ist. Gute Kandidaten sind Spalten in WHERE, JOIN und ORDER BY, besonders wenn sie in zentralen App-Flows liegen. Schlechte Kandidaten sind Spalten, die kaum filtern, selten verwendet werden oder bei jeder kleinen Änderung vorsorglich indiziert werden.
Eine typische Stolperfalle ist der Reflex, nach jedem Performanceproblem sofort Indexe zu ergänzen. Manchmal ist die Query selbst ungünstig, die Datenmenge zu groß für eine einzelne UI-Liste, oder das Repository lädt mehr Felder als nötig. Ein Index kann dann Symptome lindern, aber nicht die eigentliche Struktur verbessern. Eine zweite Stolperfalle ist fehlende Migration. Wenn du bei Room nachträglich einen Index ergänzt, ändert sich das Datenbankschema. Je nach Projekt brauchst du eine Migration oder Auto-Migration, sonst riskierst du Probleme beim Update bestehender Installationen.
Prüfe außerdem Schreibpfade. Eine App, die viele kleine Sync-Batches importiert, kann durch zu viele Indexe langsamer beim Speichern werden. Das ist besonders bei Offline-first-Architekturen relevant: Du willst lokale Lesezugriffe schnell halten, aber auch Synchronisation, Konfliktauflösung und Hintergrundarbeit nicht unnötig belasten. Indexing ist deshalb immer ein Tausch: schnellere passende Reads gegen Speicherbedarf und zusätzliche Arbeit bei Writes.
Für deine Lernpraxis kannst du ein kleines Experiment bauen. Erzeuge lokal einige tausend Datensätze, führe eine typische DAO-Abfrage mehrfach aus und miss die Zeit grob im Test oder Debug-Build. Ergänze danach einen passenden Index und vergleiche erneut. Noch wichtiger: Lies den Code so, als wärst du im Review. Passt der Index exakt zur Query? Wird diese Query häufig genutzt? Gibt es einen Test oder zumindest eine Messung, die den Nutzen belegt? Diese Fragen trainieren den Übergang vom reinen API-Wissen zu belastbarer Android-Entwicklung.
Fazit
Datenbankindizes helfen dir, lokale Abfragen in Android-Apps gezielt schneller zu machen, besonders in Data-Layer- und Offline-first-Szenarien mit wachsenden Datenmengen. Behandle sie als präzises Werkzeug: Miss zuerst die relevanten Query-Muster, setze Indexe passend zu WHERE, ORDER BY und JOIN, und prüfe danach die Kosten für Speicher, Migrationen und Schreibzugriffe. Nimm dir eine bestehende Room-DAO aus einem Übungsprojekt, identifiziere die häufigsten Abfragen, formuliere eine Index-Entscheidung und verteidige sie im Code-Review oder mit einem kleinen Performance-Test.