IndexedDB Object Stores and Indexes in JavaScript
IndexedDB is a powerful, low-level API for client-side storage that allows web applications to store significant amounts of structured data, including files and blobs. This article covers how IndexedDB organizes data using object stores—the equivalent of tables in relational databases—and how it utilizes indexes to enable high-performance querying and sorting of stored JavaScript objects.
The IndexedDB Architecture Hierarchy
At its core, IndexedDB follows a distinct hierarchical structure:
- Database: The highest level of organization. An application can have multiple databases, identified by name and version.
- Object Stores: The primary storage mechanism within a database, holding individual data records.
- Records: Individual JavaScript objects or primitive values stored within an object store.
- Indexes: Specialized structures tied to an object store that enable querying records by specific object properties.
Object Stores: The Foundation of Storage
An object store holds records as key-value pairs, where the value is typically a JavaScript object. Unlike relational database tables, object stores are schema-less; records within the same store do not need to share the same structure.
Key Paths and Key Generators
Every record in an object store must have a unique primary key. IndexedDB handles keys in two main ways:
- In-line Keys (Key Path): The key is stored as a
property directly inside the JavaScript object (e.g.,
keyPath: 'id'). - Out-of-line Keys: The key is stored separately from the value, passed explicitly during data insertion.
- Auto-Incrementing Keys
(
autoIncrement: true): IndexedDB automatically assigns a sequentially increasing integer as the key for every new record.
// Creating an object store in the onupgradeneeded event
const request = indexedDB.open('AppDatabase', 1);
request.onupgradeneeded = (event) => {
const db = event.target.result;
// Create an object store with an in-line auto-incrementing key
const userStore = db.createObjectStore('users', {
keyPath: 'id',
autoIncrement: true,
});
};Indexes: Enabling Efficient Lookups
By default, an object store only allows direct retrieval of records using the primary key. To query or filter data based on other properties within stored objects, you must create indexes.
An index is a specialized lookup table created on a specific property (or array of properties) of the objects held in an object store.
Creating an Index
Indexes must be created within the onupgradeneeded
lifecycle event using the createIndex method:
request.onupgradeneeded = (event) => {
const db = event.target.result;
const userStore = db.createObjectStore('users', { keyPath: 'id', autoIncrement: true });
// Create an index for unique email lookups
userStore.createIndex('email_idx', 'email', { unique: true });
// Create an index for querying by role (non-unique)
userStore.createIndex('role_idx', 'role', { unique: false });
};Index Configuration Options
unique: When set totrue, the index rejects any new records containing a duplicate value for the indexed property.multiEntry: When set totrueon an array property, IndexedDB creates an individual index entry for each item in the array, allowing queries against any element contained within it.
Querying Data via Object Stores vs. Indexes
Interacting with data requires opening a transaction on the target object store:
1. Direct Lookup by Primary Key
const transaction = db.transaction(['users'], 'readonly');
const store = transaction.objectStore('users');
const getRequest = store.get(1); // Retrieves user with id = 1
getRequest.onsuccess = () => {
console.log(getRequest.result);
};2. Lookup Using an Index
const transaction = db.transaction(['users'], 'readonly');
const store = transaction.objectStore('users');
const emailIndex = store.index('email_idx');
const queryRequest = emailIndex.get('alex@example.com');
queryRequest.onsuccess = () => {
console.log(queryRequest.result);
};3. Range Queries and Cursors
For retrieving multiple records matching a condition, indexes can be
paired with IDBKeyRange and cursors:
const transaction = db.transaction(['users'], 'readonly');
const store = transaction.objectStore('users');
const roleIndex = store.index('role_idx');
// Query all users where role is "admin"
const range = IDBKeyRange.only('admin');
const cursorRequest = roleIndex.openCursor(range);
cursorRequest.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
console.log(cursor.value);
cursor.continue(); // Move to next record
}
};Summary
IndexedDB structures data by decoupling primary storage (object stores) from query access patterns (indexes). Object stores preserve structured JavaScript data via primary keys, while indexes project secondary keys onto that data, allowing fast, filtered, and sorted retrieval directly on the client side.