为什么需要索引?
在深入代码之前,我们必须理解索引的重要性,索引是 MongoDB 中用于查询优化的一种数据结构,可以极大地提高数据检索速度。
- 无索引的情况:当执行一个查询(如
db.users.find({"name": "张三"}))时,MongoDB 必须扫描整个users集合中的所有文档,这个过程称为全表扫描,如果集合中有数百万个文档,查询会非常慢。 - 有索引的情况:如果在
name字段上创建了索引,MongoDB 会维护一个按照name字段值排序的 B-Tree 索引结构,当查询name时,MongoDB 可以快速定位到目标文档,而无需扫描整个集合,就像在书的目录中快速找到页码一样。
索引的代价:
- 写入开销:每次插入、更新或删除文档时,MongoDB 都需要同时更新索引,这会降低写入性能。
- 存储空间:索引需要占用额外的磁盘空间。
索引是一把双刃剑,需要根据查询模式来合理创建。
Java 驱动中的核心 API
在 Java 中,我们使用 MongoCollection 对象来操作索引,主要的 API 都在这个对象上。
1 创建索引
最常用的方法是 createIndex()。
// 创建一个在 "name" 字段上的升序索引
collection.createIndex(new Document("name", 1));
Document: 用于定义索引的字段和排序方向。1: 表示升序。-1: 表示降序。
2 创建复合索引
复合索引是建立在多个字段上的索引,对于多字段查询非常有效。
// 创建一个复合索引,先按 "category" 升序,再按 "price" 降序
collection.createIndex(new Document("category", 1).append("price", -1));
3 创建唯一索引
唯一索引确保集合中在索引字段上的值是唯一的,类似于关系型数据库中的 UNIQUE 约束。
// 确保 "email" 字段的值是唯一的
collection.createIndex(new Document("email", 1), new IndexOptions().unique(true));
4 创建稀疏索引
稀疏索引只会为包含索引字段的文档创建索引条目,这对于那些大部分文档都缺少某个字段(如 optional_field)的场景非常有用,可以节省大量空间。
// 只为包含 "optional_field" 的文档创建索引
collection.createIndex(new Document("optional_field", 1), new IndexOptions().sparse(true));
5 创建 TTL (Time-To-Live) 索引
TTL 索引用于在指定时间后自动删除文档,常用于实现缓存、会话日志等场景。
- 注意:TTL 索引只能用于日期类型(
Date)的字段。
// 在 "createdAt" 字段上创建 TTL 索引,300秒(5分钟)后自动删除文档
collection.createIndex(
new Document("createdAt", 1),
new IndexOptions().expireAfter(300L, TimeUnit.SECONDS)
);
6 获取索引信息
你可以查看集合上所有已创建的索引信息。
// 获取集合的所有索引
ListIndexesIterable<Document> indexes = collection.listIndexes();
// 遍历并打印索引信息
for (Document index : indexes) {
System.out.println(index.toJson());
}
7 删除索引
可以根据索引名称或索引键来删除索引。
// 方法1: 通过索引名称删除
// 注意:默认创建的 _id 索引名称是 "_id_"
collection.dropIndex("name_1");
// 方法2: 通过索引键删除
collection.dropIndex(new Document("name", 1));
8 重命名索引
MongoDB 5.0+ 支持重命名索引。
// 将 "name_1" 重命名为 "user_name_index"
collection.renameIndex("name_1", "user_name_index");
完整的 Java 示例
下面是一个完整的 Java 程序,演示了如何连接到 MongoDB 并执行上述各种索引操作。
1 准备工作
- 启动 MongoDB:确保你的 MongoDB 服务正在运行。
- 添加 Java 驱动依赖:如果你使用 Maven,在
pom.xml中添加:<dependency> <groupId>org.mongodb</groupId> <artifactId>mongodb-driver-sync</artifactId> <version>4.11.1</version> <!-- 使用最新稳定版本 --> </dependency>
2 示例代码
import com.mongodb.client.*;
import com.mongodb.client.model.Indexes;
import com.mongodb.client.model.IndexOptions;
import org.bson.Document;
import java.util.Arrays;
public class MongoDBIndexExample {
public static void main(String[] args) {
// 1. 创建 MongoDB 客户端并连接到服务器
String uri = "mongodb://localhost:27017";
try (MongoClient mongoClient = MongoClients.create(uri)) {
// 2. 获取数据库和集合
MongoDatabase database = mongoClient.getDatabase("testdb");
MongoCollection<Document> collection = database.getCollection("users");
// 为了演示,先清空集合
collection.drop();
// 3. 插入一些示例数据
collection.insertMany(Arrays.asList(
new Document("name", "Alice").append("age", 30).append("city", "New York").append("email", "alice@example.com"),
new Document("name", "Bob").append("age", 25).append("city", "London"),
new Document("name", "Charlie").append("age", 35).append("city", "New York").append("email", "charlie@example.com"),
new Document("name", "David").append("age", 40).append("city", "Paris")
));
System.out.println("插入示例数据完成。");
// --- 索引操作 ---
// 4. 创建单字段索引
System.out.println("\n--- 创建单字段索引 ---");
collection.createIndex(Indexes.ascending("name"));
System.out.println("在 'name' 字段上创建升序索引成功。");
// 5. 创建复合索引
System.out.println("\n--- 创建复合索引 ---");
collection.createIndex(Indexes.compoundIndex(Indexes.ascending("city"), Indexes.descending("age")));
System.out.println("在 'city' (升序) 和 'age' (降序) 上创建复合索引成功。");
// 6. 创建唯一索引
System.out.println("\n--- 创建唯一索引 ---");
collection.createIndex(Indexes.ascending("email"), new IndexOptions().unique(true));
System.out.println("在 'email' 字段上创建唯一索引成功。");
// 尝试插入重复 email 的文档会报错
try {
collection.insertOne(new Document("name", "Eve").append("email", "alice@example.com"));
} catch (Exception e) {
System.out.println("插入重复 email 失败,这是预期的行为: " + e.getMessage());
}
// 7. 获取并打印所有索引信息
System.out.println("\n--- 集合中的所有索引 ---");
collection.listIndexes().forEach(doc -> System.out.println(doc.toJson()));
// 8. 删除索引
System.out.println("\n--- 删除索引 ---");
// 默认创建的 _id 索引名称是 "_id_"
collection.dropIndex("_id_"); // 通常不建议删除 _id 索引
collection.dropIndex("name_1"); // 通过名称删除
System.out.println("删除 'name_1' 索引成功。");
// 9. 再次查看索引,确认删除
System.out.println("\n--- 删除后的索引列表 ---");
collection.listIndexes().forEach(doc -> System.out.println(doc.toJson()));
} catch (Exception e) {
e.printStackTrace();
}
}
}
最佳实践
- 为常用查询字段创建索引:分析你的应用程序,找出
find(),sort(),skip(),limit()操作中频繁使用的字段,并为它们创建索引。 - 复合索引的顺序很重要:在
{"fieldA": 1, "fieldB": 1}这样的复合索引中,它能够高效支持:{"fieldA": ...}{"fieldA": ..., "fieldB": ...}{"fieldB": ...}(效率较低,因为索引不是为这个查询优化的)- 最佳实践:将高选择性(区分度高)的字段放在索引的前面。
- 避免过多索引:不要为所有字段都创建索引,索引会增加写入开销和存储空间,只创建真正需要的索引。
- 使用
explain()分析查询:在开发或调试阶段,使用explain()方法来检查你的查询是否有效地使用了索引。// 在 Java 中使用 explain Document query = new Document("city", "New York"); Document sort = new Document("age", -1); collection.find(query).sort(sort).explain().forEach(doc -> System.out.println(doc.toJson()));查看
explain()输出中的winningPlan.executionStats,特别是executionTimeMillis和totalDocsExamined,一个好的查询应该有很低的totalDocsExamined值。 - 考虑后台创建索引:对于大型集合,创建索引可能会阻塞数据库的读写操作,可以使用
background: true选项在后台创建索引,但这会减慢索引创建的速度。collection.createIndex(Indexes.ascending("large_field"), new IndexOptions().background(true));
索引是 MongoDB 性能优化的核心,通过 Java 驱动,你可以灵活地创建、管理和删除索引来满足你的应用需求,关键在于分析查询模式,并有策略地创建索引,以在查询性能和写入性能之间取得平衡。
