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

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:

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