Chapter 49: IndexedDB Client-Side Database

Indexes & Range Queries

Constructing secondary B-Tree indexes, multi-entry tag querying, composite index lookups, and range searches with `IDBKeyRange`.

LEARNING OBJECTIVES
  • Understand how IDBIndex enables fast lookups on non-primary object properties.
  • Create unique, compound, and multiEntry array indexes using createIndex().
  • Construct targeted key queries with IDBKeyRange.only(), bound(), lowerBound(), and upperBound().
  • Execute range queries across secondary indexes using index.getAll() and index.count().
🎬 INTERACTIVE VISUAL PIPELINE Core Architecture Simulation
🌐
1. Input
Directives & Tags
⚙️
2. Parse
Tokenizer & AST
🌳
3. Layout
Box Model & Flow
🎨
4. Render
GPU Paint & Composite
PHASE 1: INPUT & DIRECTIVES
Browser receives declarative markup stream, parsing tag tokens and initializing component state.

📖 The Mental Model & Story (Intuitive Foundation)

Imagine a traditional university library with 100,000 physical books.

The books are arranged on shelves strictly by their call number / barcode (the Primary Key). If you know the exact barcode, you can walk straight to the shelf and grab the book in seconds.

What happens if you don't know the barcode, but you want to find:

  • "All books authored by Stephen King?"
  • "All computer science textbooks published between 2020 and 2024?"
  • "All books tagged with the keyword 'algorithms'?"

Without an Index, the librarian would have to inspect every single one of the 100,000 books from shelf 1 to shelf 1,000 (an $O(N)$ full table scan).

An IDBIndex is the physical card catalog system in the center of the library:

  1. The Author Catalog (by_author): An alphabetized card index pointing directly to the shelf location of every book.
  2. The Publication Date Range (IDBKeyRange.bound(2020, 2024)): Pulling out only the drawer slice between 2020 and 2024.
  3. The Multi-Entry Subject Tag Index (multiEntry: true): If a book has three tags (['AI', 'Robotics', 'Python']), the librarian files three separate index cards so the book can be discovered through any of those three search terms.

Technical Deep Dive & Specifications

The createIndex() Method

Indexes can only be declared inside onupgradeneeded via the IDBObjectStore instance:

store.createIndex(indexName, keyPath, {
  unique: false,     // Enforces strict uniqueness if true
  multiEntry: false  // If keyPath resolves to an Array, indexes each element individually
});

The 4 Index Types

+----------------------------------------------------------------------------------------------------+
|                                    INDEXEDDB INDEX TAXONOMY                                        |
+-------------------+----------------------------+-----------------+---------------------------------+
| Index Type        | Definition                 | Stored Data     | Resulting Index Keys            |
+-------------------+----------------------------+-----------------+---------------------------------+
| 1. Standard Index | `createIndex('by_email',   | `{ email:       | Key: `"[email protected]"` ➔         |
|                   |   'email', {unique:true})` |    "alice..." }`| Points to Record Primary Key    |
+-------------------+----------------------------+-----------------+---------------------------------+
| 2. Multi-Entry    | `createIndex('by_tag',     | `{ tags:        | Key: `"web"` ➔ Points to PKey   |
|    (Array Index)  |   'tags', {multiEntry:t})` |   ['web','js']}`| Key: `"js"`  ➔ Points to PKey   |
+-------------------+----------------------------+-----------------+---------------------------------+
| 3. Compound Index | `createIndex('dept_salary',| `{ dept: "eng", | Key: `["eng", 120000]` ➔        |
|    (Multi-Field)  |   ['dept', 'salary'])`     |    salary: ...}`| Points to Record Primary Key    |
+-------------------+----------------------------+-----------------+---------------------------------+
| 4. Nested Path    | `createIndex('by_city',    | `{ address: {   | Key: `"Austin"` ➔               |
|    (Dot Notation) |   'address.city')`         |    city: ...}}` | Points to Record Primary Key    |
+-------------------+----------------------------+-----------------+---------------------------------+

IDBKeyRange Range Mechanics

To query an index or store within a specific boundary, use the global IDBKeyRange factory:

                          IDBKeyRange Methods & Intervals
                          
1. IDBKeyRange.only(10)
   Matches ONLY: [ 10 ]
   
2. IDBKeyRange.lowerBound(10, false)  -->  [10, +Infinity)   (Inclusive: >= 10)
   IDBKeyRange.lowerBound(10, true)   -->  (10, +Infinity)   (Exclusive: > 10)

3. IDBKeyRange.upperBound(50, false)  -->  (-Infinity, 50]   (Inclusive: <= 50)
   IDBKeyRange.upperBound(50, true)   -->  (-Infinity, 50)   (Exclusive: < 50)

4. IDBKeyRange.bound(10, 50, false, false)  -->  [10, 50]    (10 <= x <= 50)
   IDBKeyRange.bound(10, 50, true, false)   -->  (10, 50]    (10 < x <= 50)
   IDBKeyRange.bound(10, 50, false, true)   -->  [10, 50)    (10 <= x < 50)
   IDBKeyRange.bound(10, 50, true, true)    -->  (10, 50)    (10 < x < 50)

💻 Interactive Code Playground

Starter Code

Save this file as indexes.html and open it in your browser.

Line-by-Line Code Breakdown

  • Line 58 (store.createIndex('by_price', 'price', { unique: false })): Generates an index over the numeric price attribute allowing multiple items to share identical prices.
  • Line 60 (store.createIndex('by_tag', 'tags', { multiEntry: true })): Unpacks the tags: ['portable', 'pro', 'wireless'] array into three distinct index entries pointing to the same record.
  • Line 62 (store.createIndex('by_cat_price', ['category', 'price'])): Creates a compound B-tree index sorted primary-by-category, secondary-by-price.
  • Lines 84–92 (IDBKeyRange.bound(100, 500, false, false)): Defines a closed interval $[100, 500]$ and queries matching records via index.getAll(priceRange).
  • Lines 108–117 (IDBKeyRange.bound(['electronics', 400], ['electronics', 3000])): Filters the compound index exclusively for the 'electronics' category with prices ranging from $400 to $3,000.

Expected Browser Render Output


SYS: ACTIVE
HULL: 98%
CORE: STABLE
NET: ONLINE
HTML STARSHIP CODE TERMINAL playground.html
LIVE RENDER & DIAGNOSTICS CORE TEMP: 45°C
INSPECTING DOM: VALID
TAGS: SCANNING...
[02:55:00] 🛠️ CatalogDB configured with 3 indexes and 5 seeded records.
[02:55:00] ✅ CatalogDB ready for queries.
[02:55:05] 📊 Found 3 items between $100 and $500:
   - Mechanical Keyboard ($120)
   - Noise-Canceling Headphones ($350)
   - 4K Monitor ($450)
[02:55:10] 🏷️ Found 3 items with tag "wireless":
   - Wireless Mouse (Tags: wireless, usb)
   - Noise-Canceling Headphones (Tags: audio, wireless)
   - Laptop Pro 16 (Tags: portable, pro, wireless)

🏋️ Hands-On Exercise

🎯 The Challenge: Filter Employees by Department & Salary Threshold

Instructions:

  1. Open database HRSystem with store employees (keyPath: "id").
  2. Create an index by_salary on property salary.
  3. Seed 4 employees:
    • { id: 'e1', name: 'John Doe', department: 'Engineering', salary: 110000 }
    • { id: 'e2', name: 'Jane Smith', department: 'Sales', salary: 95000 }
    • { id: 'e3', name: 'Alice Lee', department: 'Engineering', salary: 145000 }
    • { id: 'e4', name: 'Bob Ray', department: 'Marketing', salary: 70000 }
  4. Query all employees earning strictly greater than $100,000 using IDBKeyRange.lowerBound(100000, true) (exclusive lower bound) and display their names.

🏁 Starter Code Sandbox

SYS: ACTIVE
HULL: 98%
CORE: STABLE
NET: ONLINE
STARTER CODE SANDBOX exercise.html
LIVE RENDER & DIAGNOSTICS CORE TEMP: 45°C
INSPECTING DOM: VALID
TAGS: SCANNING...

⚠️ Common Pitfalls

  1. Forgetting multiEntry: true on Array Properties: If an object has tags: ['a', 'b'] and multiEntry is false (the default), IndexedDB treats the whole ['a', 'b'] array as a single compound key. Querying for the individual string 'a' will yield zero results.
  2. Attempting to Create Indexes Outside onupgradeneeded: Calling store.createIndex() inside a regular transaction throws an InvalidStateError.
  3. Unique Index Violations Rolling Back Transactions: If an index is marked { unique: true }, any store.add() or store.put() that introduces a duplicate index value will throw ConstraintError and abort the parent transaction.

💡 Pro Tips

  1. Order Matters in Compound Indexes: An index on ['country', 'state', 'city'] can optimize queries on ['country'] or ['country', 'state'], but cannot optimize queries filtering solely by 'city'. Define your index key paths from highest to lowest cardinality.
  2. Use index.count(range) for Pagination Metadata: To display total results without fetching payloads, query index.count(range).

📌 Key Takeaways

  • An IDBIndex creates a secondary B-Tree search index on one or more non-primary properties.
  • multiEntry: true splits array properties so that every individual element becomes an index key.
  • IDBKeyRange provides four interval constructors: only(), lowerBound(), upperBound(), and bound().
  • The boolean parameters in bound() control whether interval bounds are open (exclusive) or closed (inclusive).
  • Indexes are queried using index.get(), index.getAll(), index.getKey(), and index.count().
  • --
⭐ LEARN: HTML 🌟 ⚔️ QUIZ BATTLE ARENA // ACTIVE
3x
STREAK!
BONUS ACTIVE
COMBO
? Question 1 / 3

What happens if you create an index on tags WITHOUT setting multiEntry: true, and save a record { tags: ['react', 'vue'] }?

Question 1 / 3 Topic: HTML Fundamentals
00:45 REMAINING
XP REWARD
+250 XP
3x
STREAK!
BONUS ACTIVE
COMBO
? Question 2 / 3

Which IDBKeyRange method matches all values strictly between 20 and 50, excluding both 20 and 50?

Question 2 / 3 Topic: HTML Fundamentals
00:45 REMAINING
XP REWARD
+250 XP
3x
STREAK!
BONUS ACTIVE
COMBO
? Question 3 / 3

Can an index created with { unique: true } contain multiple records with null or missing properties?

Question 3 / 3 Topic: HTML Fundamentals
00:45 REMAINING
XP REWARD
+250 XP