索引是 IndexedDB 中实现高效查询的关键机制。没有索引,你只能通过主键查找数据;有了索引,你可以根据任意属性快速定位记录。
想象一个包含百万条用户记录的数据库:
// 没有索引:只能遍历所有记录 function findUserByEmail(email) { return new Promise((resolve) => { const users = []; const cursor = store.openCursor(); cursor.onsuccess = (event) => { const result = event.target.result; if (result) { if (result.value.email === email) { users.push(result.value); } result.continue(); } else { resolve(users); } }; }); } // 时间复杂度: O(n) // 有索引:直接定位 function findUserByEmailFast(email) { const index = store.index('emailIndex'); return index.get(email); } // 时间复杂度: O(log n) 或 O(1)
索引本质上是一个有序的键值映射:
当创建索引时,IndexedDB 会:
1. 遍历对象存储空间中的所有记录 2. 提取索引属性的值 3. 构建 B-Tree 结构(具体实现因浏览器而异) 4. 维护索引与数据的同步
// 基本语法 const index = store.createIndex(indexName, keyPath, options); // options { unique: boolean, // 是否唯一 multiEntry: boolean // 是否多入口(用于数组值) }
普通索引:
// 允许重复值 store.createIndex('ageIndex', 'age', { unique: false }); // 可以获取所有 25 岁的用户 const request = index.getAll(25);
唯一索引:
// 不允许重复值 store.createIndex('emailIndex', 'email', { unique: true }); // 尝试插入重复值会报错 store.add({ id: 2, email: 'existing@example.com' }); // ConstraintError: 该 email 已存在
多入口索引(MultiEntry):
// 用于数组类型的属性 store.createIndex('tagIndex', 'tags', { multiEntry: true }); // 数据 store.add({ id: 1, title: 'IndexedDB 教程', tags: ['javascript', 'database', 'browser'] }); // 查询 const request = index.getAll('javascript'); // 返回所有包含 'javascript' 标签的文章
复合索引:
// 在多个属性上创建索引 store.createIndex('nameAgeIndex', ['lastName', 'firstName', 'age']); // 查询方式 index.get(['张', '三', 25]);
const index = store.index('ageIndex'); // 获取第一条匹配记录 index.get(25).onsuccess = (e) => { console.log('第一个25岁用户:', e.target.result); }; // 获取所有匹配记录 index.getAll(25).onsuccess = (e) => { console.log('所有25岁用户:', e.target.result); }; // 获取匹配记录的数量 index.count(25).onsuccess = (e) => { console.log('25岁用户数量:', e.target.result); }; // 只获取键(不包含完整对象) index.getKey(25).onsuccess = (e) => { console.log('主键:', e.target.result); };
使用 IDBKeyRange 进行范围查询:
// 创建范围 const range = IDBKeyRange.bound(lower, upper, lowerOpen, upperOpen); // 范围类型 IDBKeyRange.only(value); // 等于 value IDBKeyRange.lowerBound(value); // >= value IDBKeyRange.upperBound(value); // <= value IDBKeyRange.bound(lower, upper); // lower <= x <= upper IDBKeyRange.bound(lower, upper, true); // lower < x <= upper IDBKeyRange.bound(lower, upper, false, true); // lower <= x < upper // 示例:查询年龄在 20 到 30 之间的用户 const ageIndex = store.index('ageIndex'); const range = IDBKeyRange.bound(20, 30); const request = ageIndex.getAll(range); // 示例:查询名字在字典序 "A" 到 "M" 之间的用户 const nameIndex = store.index('nameIndex'); const nameRange = IDBKeyRange.bound('A', 'M', false, true); const request2 = nameIndex.openCursor(nameRange);
request.onupgradeneeded = function(event) { const db = event.target.result; const transaction = event.target.transaction; // 获取对象存储空间 const store = transaction.objectStore('users'); // 删除索引 if (store.indexNames.contains('oldIndex')) { store.deleteIndex('oldIndex'); console.log('索引已删除'); } };
const index = store.index('ageIndex'); // 索引名称 console.log(index.name); // 'ageIndex' // 索引的 keyPath console.log(index.keyPath); // 'age' // 是否多入口 console.log(index.multiEntry); // false // 是否唯一 console.log(index.unique); // false // 关联的对象存储空间 console.log(index.objectStore); // IDBObjectStore
应该创建索引的属性:
不应该创建索引的属性:
// 推荐命名格式:[属性名]Index store.createIndex('emailIndex', 'email', { unique: true }); store.createIndex('createdAtIndex', 'createdAt', { unique: false }); // 复合索引命名 store.createIndex('nameAgeIndex', ['lastName', 'age']); store.createIndex('categoryPriceIndex', ['category', 'price']);
索引不是越多越好:
本章深入介绍了 IndexedDB 的索引机制:
继续阅读 第五部分:CRUD 操作详解。