IndexedDB Cursors and Indexes: Querying Guide
IndexedDB provides a powerful, client-side database system within web
browsers to store structured data. This article explores how IndexedDB
indexes enable efficient data lookups beyond primary keys, how cursors
allow step-by-step iteration over datasets, and how JavaScript utilizes
IDBKeyRange and transactions to query, filter, and retrieve
complex structured records.
Understanding Object Stores and Indexes
In IndexedDB, records are JavaScript objects stored inside an
object store. While each record has a primary key
(keyPath), querying records based solely on the primary key
limits data retrieval.
An index is a specialized data structure built on top of an object store that allows you to look up records by specific object properties.
Creating an Index
Indexes must be created inside the onupgradeneeded
lifecycle event when opening a database:
const request = indexedDB.open("StoreDB", 1);
request.onupgradeneeded = (event) => {
const db = event.target.result;
const store = db.createObjectStore("products", { keyPath: "id" });
// Create indexes on object properties
store.createIndex("by_category", "category", { unique: false });
store.createIndex("by_price", "price", { unique: false });
};Direct Lookups with Indexes
Once created, you can retrieve items directly using
.get() or .getAll() on the index object:
const transaction = db.transaction("products", "readonly");
const store = transaction.objectStore("products");
const index = store.index("by_category");
// Retrieve all records where category === "Electronics"
const request = index.getAll("Electronics");
request.onsuccess = () => {
console.log(request.result);
};What Are IndexedDB Cursors?
A cursor is a pointer mechanism used to iterate over
multiple records in an object store or index one at a time. While
methods like getAll() load the entire matching dataset into
memory at once, cursors process records sequentially, making them ideal
for large datasets, batch updates, and custom filtering.
Key Types of Cursors
openCursor(): Iterates through records, providing access to both the key and the full record value.openKeyCursor(): Iterates only through the keys, saving memory when the record payload is not needed.
Cursor Directions
Cursors support different traversal directions: * "next"
(default): Ascending order. * "prev": Descending order. *
"nextunique": Ascending order, skipping duplicate index
values. * "prevunique": Descending order, skipping
duplicate index values.
Querying Structured Records with JavaScript
JavaScript queries structured records in IndexedDB by combining
Indexes, Key Ranges
(IDBKeyRange), and Cursors.
1. Defining Ranges with
IDBKeyRange
To filter records within specific boundaries, IndexedDB provides the
IDBKeyRange utility:
IDBKeyRange.only(value): Matches an exact value.IDBKeyRange.lowerBound(lower, [open]): Matches keys greater than (or equal to)lower.IDBKeyRange.upperBound(upper, [open]): Matches keys less than (or equal to)upper.IDBKeyRange.bound(lower, upper, [lowerOpen], [upperOpen]): Matches keys betweenlowerandupper.
2. Iterating with a Cursor and Range
The following example demonstrates querying products with a price between $50 and $200 in descending order:
const transaction = db.transaction("products", "readonly");
const store = transaction.objectStore("products");
const priceIndex = store.index("by_price");
// Define a key range: 50 <= price <= 200
const priceRange = IDBKeyRange.bound(50, 200);
// Open a cursor on the index with the range and descending direction
const cursorRequest = priceIndex.openCursor(priceRange, "prev");
cursorRequest.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
console.log(`Product ID: ${cursor.primaryKey}, Price: ${cursor.key}`, cursor.value);
// Move to the next record
cursor.continue();
} else {
console.log("Iteration complete.");
}
};
cursorRequest.onerror = (event) => {
console.error("Cursor iteration failed:", event.target.error);
};3. Modifying Records During Cursor Iteration
Cursors can also modify or delete records in place when opened with a
readwrite transaction:
const transaction = db.transaction("products", "readwrite");
const store = transaction.objectStore("products");
store.openCursor().onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
if (cursor.value.discontinued) {
cursor.delete(); // Removes the current record
} else if (cursor.value.needsDiscount) {
const updated = { ...cursor.value, price: cursor.value.price * 0.9 };
cursor.update(updated); // Updates the current record
}
cursor.continue();
}
};Summary
- Indexes map object properties for quick lookups and sorting without scanning entire stores.
- IDBKeyRange specifies boundary conditions (exact, ranges, open/closed) for queries.
- Cursors traverse query results sequentially, enabling memory-efficient iteration, complex filtering, and in-place updates.