komplexe Queries ohne den gesamten Object Store zu laden
Fortgeschrittene Abfragen in IndexedDB gehen weit über getAll() hinaus: Cursor-Iteration mit IDBKeyRange, Compound Keys über mehrere Felder und Multi-Entry-Indexe für Array-Werte erlauben präzise, speicherschonende Queries direkt auf der Speicher-Engine des Browsers. Wer diese Werkzeuge beherrscht, muss keine Daten mehr client-seitig filtern, sondern lässt IndexedDB die Vorselektion übernehmen.
Inhaltsverzeichnis
- 1. Warum getAll() bei fortgeschrittenen Abfragen nicht reicht
- 2. Cursor-Iteration: das Grundwerkzeug für gezielte Abfragen
- 3. IDBKeyRange: Bereiche statt Einzelwerte
- 4. Cursor-Richtung und Duplikat-Behandlung
- 5. Compound Keys: Abfragen über mehrere Felder
- 6. Multi-Entry-Indexe für Array-Werte
- 7. Paginierung mit cursor.advance() und continue()
- 8. Performance-Analyse mit Chrome DevTools
- 9. Abfragestrategien im direkten Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum getAll() bei fortgeschrittenen Abfragen nicht reicht
Viele Entwickler lernen IndexedDB über die einfache Methode getAll() kennen, die sämtliche Einträge eines Object Stores auf einmal in den Speicher lädt. Für kleine Datenmengen funktioniert das problemlos, aber sobald ein Object Store zehntausende oder hunderttausende Einträge enthält, wird getAll() zum Performance-Problem: Der gesamte Datensatz muss deserialisiert und im Arbeitsspeicher gehalten werden, nur um ihn anschließend client-seitig mit filter() und sort() weiter einzuschränken. Fortgeschrittene Abfragen in IndexedDB lösen genau dieses Problem, indem sie die Filterung direkt auf Datenbankebene vornehmen.
Das Kernwerkzeug für fortgeschrittene Abfragen in IndexedDB ist der IDBCursor, der Datensätze einzeln, in einer definierten Reihenfolge und mit optionaler Bereichseinschränkung durchläuft, ohne den gesamten Object Store zu materialisieren. Kombiniert mit IDBKeyRange für Bereichsabfragen, Compound Keys für mehrspaltige Suche und Multi-Entry-Indexen für Array-Felder entsteht ein Abfrage-Werkzeugkasten, der für viele Anwendungsfälle eine vollwertige SQL-WHERE-Klausel simulieren kann, ganz ohne SQL-Engine.
Der Umstieg von getAll() auf echte fortgeschrittene Abfragen in IndexedDB zahlt sich besonders bei Offline-First-Anwendungen aus, etwa Kalendern mit tausenden Terminen, Produktkatalogen mit Filterfunktionen oder Log-Viewern, die nach Zeitraum und Kategorie gleichzeitig filtern müssen. Wer diese Muster einmal verinnerlicht hat, verlässt sich nicht mehr auf clientseitiges Nachfiltern, sondern nutzt die Speicher-Engine des Browsers, die für genau diese Aufgabe gebaut wurde.
2. Cursor-Iteration: das Grundwerkzeug für gezielte Abfragen
Ein IDBCursor wird über objectStore.openCursor() oder index.openCursor() erzeugt und liefert die Datensätze nicht als Array, sondern nacheinander über wiederholte onsuccess-Events. Jeder Aufruf von cursor.continue() bewegt den Cursor zum nächsten passenden Eintrag, was bedeutet, dass zu jedem Zeitpunkt nur ein einziger Datensatz im Speicher gehalten werden muss. Für fortgeschrittene Abfragen in IndexedDB ist dieses Streaming-Verhalten der entscheidende Unterschied zu getAll().
Der Cursor-Ansatz erlaubt zudem, die Iteration frühzeitig abzubrechen, sobald genug Ergebnisse gesammelt wurden, ein Muster, das mit getAll() gar nicht möglich ist, da diese Methode immer den kompletten Datensatz lädt. Für eine Anwendung, die nur die ersten 20 Treffer einer Suche anzeigen muss, spart die frühzeitige Cursor-Terminierung erhebliche Zeit und Speicher, besonders bei Object Stores mit hunderttausenden Einträgen.
// Advanced query with a cursor: stop early once enough matches are found
async function findFirstMatches(db, storeName, predicate, limit) {
return new Promise((resolve, reject) => {
const results = [];
const tx = db.transaction(storeName, "readonly");
const request = tx.objectStore(storeName).openCursor();
request.onsuccess = (event) => {
const cursor = event.target.result;
if (!cursor || results.length >= limit) {
resolve(results);
return;
}
if (predicate(cursor.value)) {
results.push(cursor.value);
}
cursor.continue(); // move to the next record, one at a time
};
request.onerror = () => reject(request.error);
});
}
const activeOrders = await findFirstMatches(
db,
"orders",
(order) => order.status === "active",
20
);
3. IDBKeyRange: Bereiche statt Einzelwerte
Ohne IDBKeyRange müsste jede Bereichsabfrage in IndexedDB über den vollständigen Datensatz mit clientseitiger Filterung erfolgen. Mit IDBKeyRange.bound(lower, upper), IDBKeyRange.lowerBound(value) und IDBKeyRange.upperBound(value) lassen sich fortgeschrittene Abfragen in IndexedDB direkt auf der Speicher-Engine ausführen, sodass nur die tatsächlich passenden Einträge überhaupt aus dem Datenspeicher gelesen werden. Der Cursor bewegt sich dabei ausschließlich innerhalb des angegebenen Bereichs, was besonders bei sortierten Indexen wie Zeitstempeln enorme Performance-Vorteile bringt.
Ein häufiges Muster für fortgeschrittene Abfragen in IndexedDB ist die Datumsbereichs-Suche: Ein Index auf ein createdAt-Feld kombiniert mit IDBKeyRange.bound(startDate, endDate) liefert ausschließlich Einträge innerhalb eines Zeitfensters, ohne dass die Anwendung selbst Datumsvergleiche für jeden Eintrag durchführen muss. Die optionalen Parameter lowerOpen und upperOpen steuern zusätzlich, ob die Grenzwerte selbst ein- oder ausgeschlossen werden, analog zu > gegenüber >= in SQL.
// Advanced range query using IDBKeyRange on an indexed field
async function findOrdersInDateRange(db, startDate, endDate) {
return new Promise((resolve, reject) => {
const results = [];
const tx = db.transaction("orders", "readonly");
const index = tx.objectStore("orders").index("createdAt");
// Range includes startDate, excludes endDate (like >= and <)
const range = IDBKeyRange.bound(startDate, endDate, false, true);
const request = index.openCursor(range);
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
results.push(cursor.value);
cursor.continue();
} else {
resolve(results);
}
};
request.onerror = () => reject(request.error);
});
}
4. Cursor-Richtung und Duplikat-Behandlung
Der zweite Parameter von openCursor(range, direction) steuert die Iterationsrichtung fortgeschrittener Abfragen in IndexedDB. Die Werte "next" und "prev" durchlaufen den Index aufsteigend oder absteigend, während "nextunique" und "prevunique" bei nicht-eindeutigen Indexen jeden Schlüsselwert nur einmal zurückgeben, auch wenn mehrere Datensätze denselben Indexwert teilen. Diese Feinsteuerung ersetzt manuelles Deduplizieren nach dem Laden der Daten.
Ein praktisches Beispiel für Richtungssteuerung bei fortgeschrittenen Abfragen in IndexedDB: Eine Log-Anwendung, die die neuesten Einträge zuerst anzeigen soll, öffnet den Cursor mit "prev" auf einem nach Zeitstempel sortierten Index, statt alle Einträge zu laden und clientseitig umzukehren. Bei einem Index mit vielen Duplikaten, etwa einem Status-Feld mit wenigen möglichen Werten, liefert "nextunique" genau einen Datensatz pro Statuswert, ideal für das schnelle Auflisten aller vorkommenden Kategorien.
5. Compound Keys: Abfragen über mehrere Felder
Ein Compound Key, auch zusammengesetzter Index genannt, kombiniert mehrere Felder eines Objekts zu einem einzigen indizierten Schlüssel. Bei der Store-Erstellung wird das mit einem Array von Feldnamen definiert, etwa store.createIndex("byCustomerAndDate", ["customerId", "createdAt"]). Fortgeschrittene Abfragen in IndexedDB nutzen diesen zusammengesetzten Index, um in einem einzigen Cursor-Durchlauf sowohl nach Kunde als auch nach Datum zu filtern, statt zwei getrennte Abfragen client-seitig zu verschneiden.
Der entscheidende Vorteil von Compound Keys bei fortgeschrittenen Abfragen in IndexedDB liegt in der Reihenfolge der Felder: Ein Bereichs-Query mit IDBKeyRange.bound([customerId, startDate], [customerId, endDate]) funktioniert nur korrekt, wenn customerId als Präfix vor createdAt im Index steht, analog zu zusammengesetzten Indexen in relationalen Datenbanken, bei denen die Feldreihenfolge über die Nutzbarkeit für bestimmte Abfragemuster entscheidet.
// Create a compound index at store creation time (inside onupgradeneeded)
function setupCompoundIndex(db) {
const store = db.createObjectStore("orders", { keyPath: "id" });
store.createIndex("byCustomerAndDate", ["customerId", "createdAt"]);
}
// Advanced query: filter by customer AND date range in a single cursor pass
async function findCustomerOrdersInRange(db, customerId, startDate, endDate) {
return new Promise((resolve, reject) => {
const results = [];
const tx = db.transaction("orders", "readonly");
const index = tx.objectStore("orders").index("byCustomerAndDate");
// customerId must be the leading field for this range to work correctly
const range = IDBKeyRange.bound(
[customerId, startDate],
[customerId, endDate]
);
const request = index.openCursor(range);
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
results.push(cursor.value);
cursor.continue();
} else {
resolve(results);
}
};
request.onerror = () => reject(request.error);
});
}
6. Multi-Entry-Indexe für Array-Werte
Objekte mit Array-Feldern, etwa Bestellungen mit mehreren Tags oder Artikeln mit mehreren Kategorien, profitieren von der Option multiEntry: true beim Erstellen eines Index. Ohne diese Option würde IndexedDB das gesamte Array als einen einzigen Schlüssel behandeln, was Abfragen nach einem einzelnen enthaltenen Wert unmöglich macht. Mit multiEntry: true erzeugt IndexedDB stattdessen für jedes Array-Element einen eigenen Indexeintrag, der auf denselben Datensatz zurückverweist.
Für fortgeschrittene Abfragen in IndexedDB bedeutet das: Eine Suche nach allen Bestellungen mit dem Tag "eilig" liefert über einen Multi-Entry-Index in einem einzigen get()- oder Cursor-Aufruf alle Treffer, unabhängig davon, an welcher Position im Array der Tag steht oder wie viele weitere Tags der Datensatz sonst noch enthält. Das ist die native IndexedDB-Entsprechung zu einem Contains-Filter auf einer Array-Spalte in relationalen Datenbanken.
// Multi-entry index: each array element becomes its own index entry
function setupMultiEntryIndex(db) {
const store = db.createObjectStore("orders", { keyPath: "id" });
store.createIndex("byTag", "tags", { multiEntry: true });
}
// Advanced query: find all orders that contain a specific tag
async function findOrdersByTag(db, tag) {
const tx = db.transaction("orders", "readonly");
const index = tx.objectStore("orders").index("byTag");
return index.getAll(tag); // matches regardless of array position
}
const urgentOrders = await findOrdersByTag(db, "urgent");
7. Paginierung mit cursor.advance() und continue()
Für Paginierung bietet der Cursor zwei komplementäre Methoden: continue(key) bewegt den Cursor zum nächsten Eintrag oder optional direkt zu einem bestimmten Schlüssel, während advance(count) eine feste Anzahl von Einträgen überspringt, ohne sie zu materialisieren. Für fortgeschrittene Abfragen in IndexedDB mit Seiten-basierter Navigation ist advance() effizienter als das manuelle Zählen und Überspringen in einer continue()-Schleife, weil die Speicher-Engine das Überspringen intern optimieren kann.
Eine robuste Paginierungsstrategie für fortgeschrittene Abfragen in IndexedDB speichert nicht die Seitenzahl, sondern den zuletzt gesehenen Schlüssel, und nutzt IDBKeyRange.lowerBound(lastKey, true) für die nächste Seite. Dieses Cursor-basierte Paginierungsmuster bleibt auch bei sich ändernden Daten zwischen zwei Seitenaufrufen konsistent, während eine reine Offset-Paginierung mit advance() bei gleichzeitigen Schreibvorgängen Einträge doppelt anzeigen oder überspringen kann.
// Cursor-based pagination: stable even if records are inserted concurrently
async function getNextPage(db, indexName, lastSeenKey, pageSize) {
return new Promise((resolve, reject) => {
const results = [];
const tx = db.transaction("orders", "readonly");
const index = tx.objectStore("orders").index(indexName);
const range = lastSeenKey
? IDBKeyRange.lowerBound(lastSeenKey, true) // exclude the last seen key
: null;
const request = index.openCursor(range, "next");
request.onsuccess = (event) => {
const cursor = event.target.result;
if (!cursor || results.length >= pageSize) {
resolve({ items: results, lastKey: cursor ? cursor.key : null });
return;
}
results.push(cursor.value);
cursor.continue();
};
request.onerror = () => reject(request.error);
});
}
8. Performance-Analyse mit Chrome DevTools
Um zu prüfen, ob fortgeschrittene Abfragen in IndexedDB tatsächlich einen Index nutzen statt versehentlich über den Primärschlüssel zu iterieren, hilft der Application-Tab in Chrome DevTools, in dem sich der komplette Object Store inklusive aller definierten Indexe einsehen lässt. Ein häufiger Fehler ist, einen Index anzulegen, aber die Abfrage weiterhin über objectStore.openCursor() statt über index.openCursor() auszuführen, wodurch der Index nie zum Einsatz kommt und die Abfrage weiterhin linear über alle Einträge läuft.
Ein einfacher, aber wirkungsvoller Test für fortgeschrittene Abfragen in IndexedDB ist ein Zeitvergleich: Eine Abfrage über getAll() mit anschließendem clientseitigem Filter gegen dieselbe Abfrage über einen Cursor mit passendem Index und IDBKeyRange, gemessen mit performance.now() vor und nach der jeweiligen Operation. Bei Object Stores mit mehr als 10.000 Einträgen zeigt sich der Unterschied regelmäßig im Bereich von mehreren hundert Millisekunden zugunsten der indexbasierten Cursor-Abfrage.
9. Abfragestrategien im direkten Vergleich
Die folgende Tabelle stellt die gängigen Muster für fortgeschrittene Abfragen in IndexedDB den naiven Alternativen gegenüber, die viele Entwickler zunächst verwenden.
| Anforderung | Naiver Ansatz | Fortgeschrittene Abfrage | Vorteil |
|---|---|---|---|
| Werte in einem Bereich finden | getAll() + filter() |
index.openCursor(IDBKeyRange.bound(...)) |
Nur passende Einträge werden gelesen |
| Nach zwei Feldern filtern | Zwei Queries clientseitig verschneiden | Compound Index mit ["a", "b"] |
Ein Cursor-Durchlauf statt zweier Abfragen |
| Nach Array-Wert suchen | getAll() + Array.includes() |
Index mit multiEntry: true |
Direkter Treffer ohne vollständigen Scan |
| Neueste Einträge zuerst | getAll() + Array.reverse() |
openCursor(null, "prev") |
Reihenfolge kommt bereits sortiert aus der Engine |
| Seitenweise laden | Offset-Paginierung mit slice() |
Cursor mit letztem Schlüssel als Range-Start | Konsistent auch bei gleichzeitigen Schreibvorgängen |
Die Tabelle macht deutlich: Fortgeschrittene Abfragen in IndexedDB verschieben die Filterlogik von der Anwendung in die Speicher-Engine, die genau für diese Aufgabe optimiert ist. Der naive Ansatz mit getAll() funktioniert nur, solange die Datenmenge klein bleibt, während die indexbasierten Muster auch bei wachsenden Object Stores konstant performant bleiben.
Mironsoft
JavaScript-Architektur, Offline-Datenbanken und Query-Performance
IndexedDB-Abfragen, die auch bei großen Datenmengen schnell bleiben?
Wir modellieren Compound Keys, Multi-Entry-Indexe und Cursor-basierte Paginierung für IndexedDB-Datenbanken, die auch bei hunderttausenden Einträgen performant abfragbar bleiben.
Schema-Design
Index-Struktur und Compound Keys passend zu den tatsächlichen Abfragemustern planen
Query-Refactoring
Migration von getAll()-Filterlogik auf Cursor- und IDBKeyRange-basierte Abfragen
Performance-Audit
Messung und Optimierung bestehender IndexedDB-Abfragen mit Chrome DevTools
10. Zusammenfassung
Fortgeschrittene Abfragen in IndexedDB verwandeln eine simple Key-Value-Datenbank in ein leistungsfähiges Abfrage-Werkzeug, das komplexen SQL-Queries in vielen Fällen nahekommt. Cursor-Iteration mit IDBKeyRange ersetzt das Laden ganzer Object Stores durch gezieltes, speicherschonendes Durchlaufen nur der relevanten Einträge. Compound Keys ermöglichen Filterung über mehrere Felder in einem einzigen Index-Durchlauf, während Multi-Entry-Indexe Array-Felder wie Tags oder Kategorien effizient durchsuchbar machen.
Wer fortgeschrittene Abfragen in IndexedDB konsequent einsetzt, muss sich nicht mehr auf clientseitiges Filtern und Sortieren verlassen, das bei wachsenden Datenmengen unweigerlich zum Performance-Engpass wird. Die Kombination aus Cursor-Direction, Paginierung über den letzten gesehenen Schlüssel und passend modellierten Indexen liefert eine Abfrage-Basis, die auch bei Object Stores mit hunderttausenden Einträgen konstant schnell bleibt.
IndexedDB fortgeschrittene Abfragen — Das Wichtigste auf einen Blick
Cursor statt getAll()
openCursor() streamt Datensätze einzeln, ermöglicht frühzeitigen Abbruch und spart Arbeitsspeicher.
IDBKeyRange
Bereichsabfragen mit bound(), lowerBound(), upperBound() statt clientseitigem Vergleich.
Compound Keys
Mehrere Felder als ein Index, Feldreihenfolge entscheidet über nutzbare Range-Queries.
Multi-Entry-Index
multiEntry: true indiziert jedes Array-Element einzeln, ideal für Tag-Suchen.