第四部分:索引(Index)详解
4.1 索引概述
索引是 IndexedDB 中实现高效查询的关键机制。没有索引,你只能通过主键查找数据;有了索引,你可以根据任意属性快速定位记录。
4.1.1 为什么需要索引
想象一个包含百万条用户记录的数据库:
// 没有索引:只能遍历所有记录 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)
4.1.2 索引的工作原理
索引本质上是一个有序的键值映射:
- 键(Key):索引属性的值
- 值(Value):对应的主键引用
当创建索引时,IndexedDB 会:
1. 遍历对象存储空间中的所有记录 2. 提取索引属性的值 3. 构建 B-Tree 结构(具体实现因浏览器而异) 4. 维护索引与数据的同步
4.2 创建索引
4.2.1 createIndex 方法
// 基本语法 const index = store.createIndex(indexName, keyPath, options); // options { unique: boolean, // 是否唯一 multiEntry: boolean // 是否多入口(用于数组值) }
4.2.2 索引类型详解
普通索引:
// 允许重复值 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]);
4.3 索引查询
4.3.1 基础查询方法
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); };
4.3.2 范围查询
使用 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);
4.4 索引管理
4.4.1 删除索引
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('索引已删除'); } };
4.4.2 获取索引信息
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
4.5 索引设计最佳实践
4.5.1 选择合适的索引属性
应该创建索引的属性:
- 经常用于查询条件的属性
- 需要排序的属性
- 外键关联的属性
不应该创建索引的属性:
- 很少用于查询的属性
- 值高度唯一的属性(如果已经是主键)
- 大文本字段
- 频繁更新的属性(索引维护有开销)
4.5.2 索引命名规范
// 推荐命名格式:[属性名]Index store.createIndex('emailIndex', 'email', { unique: true }); store.createIndex('createdAtIndex', 'createdAt', { unique: false }); // 复合索引命名 store.createIndex('nameAgeIndex', ['lastName', 'age']); store.createIndex('categoryPriceIndex', ['category', 'price']);
4.5.3 索引数量控制
索引不是越多越好:
- 每个索引都会占用额外存储空间
- 写入操作需要维护所有相关索引
- 建议每个对象存储空间索引数不超过 10 个
4.6 本章小结
本章深入介绍了 IndexedDB 的索引机制:
- 索引用于加速非主键属性的查询
- 支持普通索引、唯一索引、多入口索引和复合索引
- 使用 IDBKeyRange 进行范围查询
- 索引设计需要平衡查询性能和存储开销
继续阅读 第五部分:CRUD 操作详解。