Lesson 0004 · ~14 minutes

Indexes, ranges & cursors

One skill: look up records by something other than the primary key — and know when a cursor beats dumping the whole store.

Win for this lesson

In a design review you can say: “secondary lookup needs an index at upgrade time; ranges use IDBKeyRange; huge sets stream with cursors — getAll is for modest results.”

If you skipped MDN after lesson 3

Fine for now. This lesson’s primary source is still MDN — Using IndexedDB (indexes + cursors sections). Skim the transaction lifetime parts while you’re there — they backfill lesson 3.

1. Primary key is not enough

Lesson 2–3: every record has one primary key. Product queries usually need more — “all notes for projectId,” “emails by domain,” “events in this day.” That is what an index is for: a secondary ordered lookup path on a property (or path) of the stored value.

Indexes are schema. You create them only in upgradeneeded — same deploy cost as new object stores. There is no free “add a WHERE in production without a version bump.”

// inside onupgradeneeded only
const store = db.createObjectStore("notes", { keyPath: "id" });
store.createIndex("by_project", "projectId", { unique: false });
store.createIndex("by_email", "email", { unique: true });

2. Query through the index

In a normal transaction you open the store, then the index, then read:

const tx = db.transaction("notes", "readonly");
const index = tx.objectStore("notes").index("by_project");

index.get(projectId);           // one match (first if non-unique)
index.getAll(projectId);        // all with that index key
index.getKey(projectId);        // primary key only
index.count(projectId);         // how many

Same request lifetime rules as lesson 3: keep work inside the transaction’s active turns; “done” for multi-step work is still tx.oncomplete.

3. Ranges, not only equality

IDBKeyRange expresses ordered ranges of keys (primary or index keys):

Factory Meaning
IDBKeyRange.only(x) Exactly x
IDBKeyRange.lowerBound(x, open?) ≥ or > x
IDBKeyRange.upperBound(x, open?) ≤ or < x
IDBKeyRange.bound(lo, hi, …) Between lo and hi
const range = IDBKeyRange.bound("2026-07-01", "2026-07-31");
index.getAll(range);
// or store.openCursor(range) on the primary key

Keys must be comparable IndexedDB key types (numbers, strings, dates, …). Design stable, ordered key shapes early — random objects are not valid keys.

4. When to cursor instead of getAll

get / getAll

Small or bounded results. Simple code. Memory cost is “all matching values at once.”

openCursor

Stream one record at a time; stop early; process large sets without loading everything into RAM.

openKeyCursor

Keys only (no values). Useful for existence, paging keys, or cheap scans.

const req = index.openCursor(IDBKeyRange.only(projectId));
req.onsuccess = () => {
  const cursor = req.result;
  if (!cursor) return; // done
  // cursor.key = index key; cursor.primaryKey; cursor.value
  renderRow(cursor.value);
  cursor.continue(); // next match
};
Default rule (steal this)

Prefer get/getAll with a tight range or equality for UI lists you know are modest. Reach for cursors when the set can be huge, you need early exit, or you only need keys. “Load the entire object store into memory every paint” is a design smell — with or without indexes.

5. Design cost of indexes

What this lesson is not

Full cursor update-in-place patterns, complex multiEntry tag systems, or multi-tab live fan-out. Next layers: quotas/eviction and integration deal-breakers.

6. Practice

equal-length choices. Immediate feedback.

Scenario A

When can you add a by_project index on an existing notes store?

Scenario B

Notes use primary key id and index by_project on projectId. Load all notes for one project?

Scenario C

An analytics store may hold hundreds of thousands of events. You need to process matches for one day without holding all of them in memory. Best default tool?

Scenario D

How do you express “keys from A to Z inclusive” for an index query?

Scenario E

Which tradeoff belongs in a design review about adding five new indexes?

7. What to remember

Ask your teacher Fuzzy on compound indexes, multiEntry, or a query shape for a feature you’re building? Ask in chat.

Primary source (read next)

MDN — Using IndexedDB — sections on creating indexes, using indexes, and cursors. This is also the backfill for lesson 3 if you skipped it. Optional: web.dev — Work with IndexedDB.