Indexing
Description — Couchbase Lite JavaScript — Indexing for Query Performance
Related Content — SQL++ for Mobile | Query Resultsets | Live Queries
Overview
Indexes speed up queries by creating efficient lookup structures for your data. Without indexes, queries must scan every document in a collection, which becomes slow as your data grows.
Couchbase Lite JavaScript uses IndexedDB’s native indexing capabilities, which provides fast query performance but has some unique constraints compared to native Couchbase Lite platforms.
IndexedDB Indexing Constraints
| Due to IndexedDB requirements, indexes must be declared when opening the database. You cannot create or delete indexes while the database is open. |
Key Constraints:
-
Declaration at Open Time - All indexes must be specified in the
DatabaseConfigwhen callingDatabase.open() -
Limited Data Types - Only numbers, strings, and arrays of these types can be indexed
-
No Dynamic Index Management - Cannot add or remove indexes without closing and reopening the database
-
Per-Collection Indexes - Each collection has its own set of indexes
Declaring Indexes
Indexes are declared in the collection configuration when opening the database:
const config: DatabaseConfig = {
name: 'myapp',
version: 1,
collections: {
products: {
indexes: ['name', 'price', 'category']
}
}
};
const db = await Database.open(config);
Index Types
Property Indexes
The most common type of index is a property index, which indexes a single document property:
const productConfig: DatabaseConfig = {
name: 'store',
version: 1,
collections: {
products: {
indexes: [
'sku', // Product SKU
'price', // Product price
'inStock' // Stock status
]
}
}
};
Array Indexes
You can index array properties to enable efficient queries on array contents:
const articleConfig: DatabaseConfig = {
name: 'blog',
version: 1,
collections: {
articles: {
indexes: [
'tags', // Array of tags
'categories' // Array of categories
]
}
}
};
Nested Property Indexes
Index nested object properties using dot notation:
const userConfig: DatabaseConfig = {
name: 'app',
version: 1,
collections: {
users: {
indexes: [
'profile.name',
'profile.email',
'address.city',
'address.country'
]
}
}
};
Compound Indexes
While IndexedDB doesn’t support true compound indexes, you can create multiple single-property indexes that work together:
const orderConfig: DatabaseConfig = {
name: 'orders',
version: 1,
collections: {
orders: {
indexes: [
'customerId', // Filter by customer
'status', // Filter by status
'orderDate', // Sort by date
'totalAmount' // Sort/filter by amount
]
}
}
};
TypeScript Schema with Indexes
When using TypeScript, declare both your document schema and indexes together:
interface Product {
sku: string;
name: string;
price: number;
category: string;
tags: string[];
}
interface StoreSchema {
products: Product;
}
// Configure with type-safe indexes
const storeConfig: DatabaseConfig<StoreSchema> = {
name: 'store',
version: 1,
collections: {
products: {
indexes: ['sku', 'price', 'category', 'tags']
}
}
};
const storeDb = await Database.open(storeConfig);
Modifying Indexes
To add or remove indexes, you must close and reopen the database with the new configuration:
// Close database first
await database.close();
// Reopen with new indexes
const updatedConfig: DatabaseConfig = {
name: 'myapp',
version: 2, // Increment version
collections: {
products: {
indexes: [
'name',
'price',
'category',
'manufacturer' // New index added
]
}
}
};
const updatedDb = await Database.open(updatedConfig);
How Queries Use Indexes
Couchbase Lite automatically selects appropriate indexes for your queries. The query optimizer:
-
Analyzes the WHERE clause
-
Identifies indexed properties used in the query
-
Selects the most efficient index
-
Falls back to collection scanning if no suitable index exists
Indexable Data Types
IndexedDB can only index certain data types:
Indexable Types:
-
Numbers - Integers and floating-point values
-
Strings - Text values
-
Arrays - Arrays containing numbers or strings
Non-Indexable Types:
-
Objects (nested documents)
-
Booleans
-
Dates (must be converted to numbers or strings)
-
null values
-
Blobs
interface Event {
title: string;
startDate: string; // ISO date string: "2024-01-15T10:00:00Z"
timestamp: number; // Unix timestamp: 1705315200000
}
const eventConfig: DatabaseConfig = {
name: 'events',
version: 1,
collections: {
events: {
indexes: [
'startDate', // Index ISO date string
'timestamp' // Index numeric timestamp
]
}
}
};
const database = await Database.open(eventConfig);
Index Maintenance
Automatic Updates:
-
Indexes update automatically when documents are saved
-
No manual maintenance required
-
Updates happen synchronously during document saves
Performance Impact:
-
More indexes = slower write operations
-
Each indexed property adds overhead to saves
-
Balance query performance vs. write performance
Query Optimization Strategies
Use Indexes Effectively
Structure queries to take advantage of indexes:
const indexedQuery = database.createQuery(`
SELECT *
FROM products
WHERE category = 'electronics' -- Uses category index
ORDER BY price -- Uses price index
LIMIT 50
`);
const results = await indexedQuery.execute();
Avoid Full Collection Scans
Queries without indexed properties scan the entire collection:
const unindexedQuery = database.createQuery(`
SELECT *
FROM products
WHERE description LIKE '%wireless%' -- No index on description
`);
// This will be slow on large datasets
const slowResults = await unindexedQuery.execute();
Use LIMIT for Large Result Sets
Even with indexes, limit results for better performance:
const limitedQuery = database.createQuery(`
SELECT *
FROM products
WHERE category = $category
ORDER BY price DESC LIMIT 20
OFFSET $offset
`);
// Fetch first page
limitedQuery.parameters = {
category: 'electronics',
offset: 0
};
const page1 = await limitedQuery.execute();
// Fetch next page
limitedQuery.parameters = {
category: 'electronics',
offset: 20
};
const page2 = await limitedQuery.execute();
Common Indexing Patterns
Email/Username Lookups
const authConfig: DatabaseConfig = {
name: 'auth',
version: 1,
collections: {
users: {
indexes: ['email', 'username']
}
}
};
// Fast lookup by email
const userQuery = database.createQuery(`
SELECT *
FROM users
WHERE email = $email
`);
userQuery.parameters = {email: 'user@example.com'};
const user = await userQuery.execute();
Status Filtering
const workflowConfig: DatabaseConfig = {
name: 'workflow',
version: 1,
collections: {
tasks: {
indexes: ['status', 'priority', 'assignedTo']
}
}
};
// Query by status
const activeTasksQuery = database.createQuery(`
SELECT *
FROM tasks
WHERE status IN ('pending', 'in-progress')
AND assignedTo = $userId
ORDER BY priority DESC
`);
Date Range Queries
const analyticsConfig: DatabaseConfig = {
name: 'analytics',
version: 1,
collections: {
events: {
indexes: ['timestamp', 'eventType']
}
}
};
// Query date range
const dateRangeQuery = database.createQuery(`
SELECT *
FROM events
WHERE timestamp >= $startDate
AND timestamp <= $endDate
ORDER BY timestamp DESC
`);
dateRangeQuery.parameters = {
startDate: Date.now() - (7 * 24 * 60 * 60 * 1000), // 7 days ago
endDate: Date.now()
};
const recentEvents = await dateRangeQuery.execute();
Category Filtering
const catalogConfig: DatabaseConfig = {
name: 'catalog',
version: 1,
collections: {
products: {
indexes: ['category', 'subcategory', 'brand']
}
}
};
// Hierarchical category query
const categoryQuery = database.createQuery(`
SELECT *
FROM products
WHERE category = $category
AND subcategory = $subcategory
ORDER BY name
`);
categoryQuery.parameters = {
category: 'electronics',
subcategory: 'laptops'
};
const laptops = await categoryQuery.execute();
Indexing Limitations
Missing Features:
-
No Full-Text Search Indexes - FTS with
MATCH()not available -
No Vector Indexes - Vector search without indexes is very slow
-
No Partial Indexes - Cannot index subset of documents
-
No Expression Indexes - Only property values can be indexed
-
No Multi-Property Compound Indexes - Each index is single-property
Workarounds:
-
For FTS: Consider using a JavaScript FTS library and storing results
-
For vector search: Use
EUCLIDEAN_DISTANCE()on small datasets only -
For partial indexes: Filter in WHERE clause instead
-
For expression indexes: Store computed values as properties
Performance Testing
Always test index effectiveness with realistic data:
async function testQueryPerformance() {
// Test without index
const startUnindexed = performance.now();
const unindexedResults = await database.createQuery(`
SELECT *
FROM products
WHERE description LIKE '%test%'
`).execute();
const timeUnindexed = performance.now() - startUnindexed;
console.log(`Unindexed query: ${timeUnindexed.toFixed(2)}ms`);
console.log(`Results: ${unindexedResults.length}`);
// Test with index
const startIndexed = performance.now();
const indexedResults = await database.createQuery(`
SELECT *
FROM products
WHERE category = 'electronics'
`).execute();
const timeIndexed = performance.now() - startIndexed;
console.log(`Indexed query: ${timeIndexed.toFixed(2)}ms`);
console.log(`Results: ${indexedResults.length}`);
console.log(`Speedup: ${(timeUnindexed / timeIndexed).toFixed(1)}x`);
}
Debugging Index Issues
Query Not Using Index:
-
Verify property name matches index exactly
-
Ensure indexed property is used in WHERE clause
-
Check that query condition is compatible with indexing
Slow Queries Despite Indexes:
-
Check index selectivity (too many duplicate values?)
-
Verify result set size (use LIMIT if appropriate)
-
Monitor IndexedDB performance in browser DevTools
-
Consider simplifying complex queries
Index Declaration Errors:
-
Verify indexes declared in DatabaseConfig
-
Check property names for typos
-
Ensure database version increments when changing indexes
-
Verify database successfully reopened with new config
Index Migration Strategy
When adding indexes to existing databases:
async function migrateIndexes() {
// Step 1: Check current version
const currentDb = await Database.open({
name: 'myapp',
version: 1,
collections: {
products: {
indexes: ['name']
}
}
});
console.log('Current version:', currentDb.config.version);
// Step 2: Close database
await currentDb.close();
// Step 3: Reopen with new indexes and incremented version
const migratedDb = await Database.open({
name: 'myapp',
version: 2,
collections: {
products: {
indexes: [
'name',
'price', // New index
'category' // New index
]
}
}
});
console.log('Migrated to version:', migratedDb.config.version);
return migratedDb;
}
await migrateIndexes();