====== 第四部分:索引(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 进行范围查询
* 索引设计需要平衡查询性能和存储开销
继续阅读 [[part05-crud|第五部分:CRUD 操作详解]]。