MongoDB 面试题
一、MongoDB 基础
1. MongoDB 是什么?
MongoDB 是一个开源的、面向文档的 NoSQL 数据库。
核心特性:
- ✅ 文档存储 - 使用 BSON(Binary JSON)格式存储数据
- ✅ 模式灵活 - 无需预定义表结构(Schema-less)
- ✅ 水平扩展 - 支持分片(Sharding)
- ✅ 高可用 - 支持复制集(Replica Set)
- ✅ 丰富查询 - 支持复杂查询和聚合
2. MongoDB vs 关系型数据库
| 特性 | MongoDB | MySQL |
|---|---|---|
| 数据模型 | 文档(Document) | 行(Row) |
| 表结构 | 集合(Collection) | 表(Table) |
| 数据库结构 | 数据库(Database) | 数据库(Database) |
| 模式 | 无模式(Schema-less) | 固定模式(Schema) |
| 事务 | 支持(4.0+) | 完全支持 |
| JOIN | 有限支持($lookup) | 完全支持 |
| 水平扩展 | 原生支持 | 需中间件 |
| ACID | 支持(4.0+) | 完全支持 |
3. MongoDB 的优势和劣势
优势:
- ✅ 灵活的文档模型
- ✅ 水平扩展能力强
- ✅ 高性能读写
- ✅ 丰富的查询功能
- ✅ 易于开发和迭代
劣势:
- ❌ 内存占用较大
- ❌ 不支持复杂 JOIN
- ❌ 事务支持相对较晚(4.0+)
- ❌ 数据一致性相对较弱(最终一致性)
二、核心概念
1. 文档(Document)
文档结构:
json
{
"_id": ObjectId("507f1f77bcf86cd799439011"),
"name": "Alice",
"age": 30,
"email": "alice@example.com",
"address": {
"city": "Beijing",
"street": "Chang'an Street"
},
"tags": ["developer", "mongodb"],
"created_at": ISODate("2025-01-01T00:00:00Z")
}字段类型:
- String、Number、Boolean
- Array、Object(嵌套文档)
- Date、ObjectId、Null
- Binary Data、Regular Expression
2. 集合(Collection)
集合特点:
- 类似关系型数据库的"表"
- 无需预定义结构
- 可以存储不同结构的文档
创建集合:
javascript
// 显式创建
db.createCollection("users");
// 隐式创建(插入文档时自动创建)
db.users.insertOne({ name: "Alice" });3. 索引(Index)
索引类型:
单字段索引:
javascript
// 创建单字段索引
db.users.createIndex({ email: 1 }); // 1 表示升序,-1 表示降序
// 唯一索引
db.users.createIndex({ email: 1 }, { unique: true });
// 稀疏索引(只索引有该字段的文档)
db.users.createIndex({ phone: 1 }, { sparse: true });复合索引:
javascript
// 创建复合索引
db.users.createIndex({ name: 1, age: -1 });
// 查询顺序必须遵循最左前缀原则
// ✅ 可以使用索引
db.users.find({ name: "Alice" });
db.users.find({ name: "Alice", age: 30 });
// ❌ 不能使用索引
db.users.find({ age: 30 });文本索引:
javascript
// 创建文本索引
db.articles.createIndex({ title: "text", content: "text" });
// 文本搜索
db.articles.find({ $text: { $search: "mongodb tutorial" } });地理空间索引:
javascript
// 2dsphere 索引(地球表面)
db.places.createIndex({ location: "2dsphere" });
// 查询附近的位置
db.places.find({
location: {
$near: {
$geometry: { type: "Point", coordinates: [116.3974, 39.9093] },
$maxDistance: 1000 // 1000 米
}
}
});TTL 索引(自动删除过期文档):
javascript
// 创建 TTL 索引(30 秒后自动删除)
db.logs.createIndex({ created_at: 1 }, { expireAfterSeconds: 30 });4. 查询操作
基本查询:
javascript
// 查询所有
db.users.find();
// 条件查询
db.users.find({ age: 30 });
db.users.find({ age: { $gte: 18, $lte: 65 } });
db.users.find({ name: { $in: ["Alice", "Bob"] } });
// 逻辑查询
db.users.find({ $or: [{ age: 30 }, { city: "Beijing" }] });
db.users.find({ $and: [{ age: { $gte: 18 } }, { age: { $lte: 65 } }] });
// 模糊查询
db.users.find({ name: /^A/ }); // 以 A 开头
db.users.find({ name: /Alice$/ }); // 以 Alice 结尾
// 存在性查询
db.users.find({ email: { $exists: true } });
db.users.find({ phone: { $exists: false } });
// 空值查询
db.users.find({ email: null });
db.users.find({ email: { $in: [null], $exists: true } }); // null 且存在投影(只返回指定字段):
javascript
// 只返回 name 和 email
db.users.find({}, { name: 1, email: 1, _id: 0 });
// 排除 email
db.users.find({}, { email: 0 });排序、限制、跳过:
javascript
// 排序
db.users.find().sort({ age: 1 }); // 升序
db.users.find().sort({ age: -1 }); // 降序
// 限制结果数量
db.users.find().limit(10);
// 跳过
db.users.find().skip(10).limit(10); // 分页
// 组合使用
db.users.find().sort({ created_at: -1 }).skip(20).limit(10);5. 更新操作
更新方法:
javascript
// updateOne - 更新一条
db.users.updateOne(
{ email: "alice@example.com" },
{ $set: { age: 31 } }
);
// updateMany - 更新多条
db.users.updateMany(
{ city: "Beijing" },
{ $set: { region: "North" } }
);
// replaceOne - 替换整个文档
db.users.replaceOne(
{ email: "alice@example.com" },
{ name: "Alice", age: 31, email: "alice@example.com" }
);更新操作符:
javascript
// $set - 设置字段
db.users.updateOne({ _id: 1 }, { $set: { age: 31 } });
// $unset - 删除字段
db.users.updateOne({ _id: 1 }, { $unset: { phone: "" } });
// $inc - 增加数值
db.users.updateOne({ _id: 1 }, { $inc: { age: 1 } });
// $push - 添加数组元素
db.users.updateOne({ _id: 1 }, { $push: { tags: "new" } });
// $pull - 删除数组元素
db.users.updateOne({ _id: 1 }, { $pull: { tags: "old" } });
// $addToSet - 添加唯一元素
db.users.updateOne({ _id: 1 }, { $addToSet: { tags: "new" } });
// $rename - 重命名字段
db.users.updateOne({ _id: 1 }, { $rename: { "old_field": "new_field" } });三、聚合(Aggregation)
1. 聚合管道(Aggregation Pipeline)
管道操作符:
javascript
// $match - 过滤
db.orders.aggregate([
{ $match: { status: "completed" } }
]);
// $group - 分组
db.orders.aggregate([
{ $group: {
_id: "$customer_id",
total: { $sum: "$amount" },
count: { $sum: 1 },
avg: { $avg: "$amount" }
}}
]);
// $project - 投影
db.orders.aggregate([
{ $project: {
customer_id: 1,
amount: 1,
date: { $dateToString: { format: "%Y-%m-%d", date: "$created_at" } }
}}
]);
// $sort - 排序
db.orders.aggregate([
{ $sort: { amount: -1 } }
]);
// $limit - 限制
db.orders.aggregate([
{ $limit: 10 }
]);
// $skip - 跳过
db.orders.aggregate([
{ $skip: 10 }
]);
// $lookup - 关联(类似 JOIN)
db.orders.aggregate([
{
$lookup: {
from: "users",
localField: "customer_id",
foreignField: "_id",
as: "customer"
}
}
]);复杂聚合示例:
javascript
// 统计每个客户的订单总额和平均金额
db.orders.aggregate([
{ $match: { status: "completed" } }, // 过滤已完成订单
{ $group: {
_id: "$customer_id",
total: { $sum: "$amount" },
avg: { $avg: "$amount" },
count: { $sum: 1 }
}},
{ $sort: { total: -1 } }, // 按总额降序
{ $limit: 10 } // 取前 10
]);2. Map-Reduce(已废弃)
注意: Map-Reduce 在 MongoDB 5.0+ 中已废弃,推荐使用聚合管道。
四、复制集(Replica Set)
1. 复制集概念
复制集组成:
- Primary(主节点) - 处理所有写操作
- Secondary(从节点) - 复制主节点数据
- Arbiter(仲裁节点) - 不存储数据,只参与选举
复制集优势:
- ✅ 高可用(自动故障转移)
- ✅ 数据冗余(多份副本)
- ✅ 读写分离(从节点可以读)
2. 复制集配置
javascript
// 初始化复制集
rs.initiate({
_id: "rs0",
members: [
{ _id: 0, host: "localhost:27017" },
{ _id: 1, host: "localhost:27018" },
{ _id: 2, host: "localhost:27019" }
]
});
// 查看状态
rs.status();
// 添加节点
rs.add("localhost:27020");
// 删除节点
rs.remove("localhost:27020");3. 读写分离
从节点读取:
javascript
// 连接到从节点
var db = connect("localhost:27017/rs0");
// 设置读偏好
db.setReadPreference("secondary"); // 从从节点读
// 或在查询时指定
db.users.find({}).readPref("secondary");读偏好选项:
primary- 只从主节点读(默认)primaryPreferred- 优先主节点,不可用时从从节点读secondary- 只从从节点读secondaryPreferred- 优先从节点,不可用时从主节点读nearest- 从延迟最低的节点读
五、分片(Sharding)
1. 分片概念
分片组件:
- Shard(分片) - 存储数据
- Config Server(配置服务器) - 存储元数据
- Mongos(路由) - 路由查询请求
分片优势:
- ✅ 水平扩展(突破单机限制)
- ✅ 负载均衡(数据分散到多个分片)
- ✅ 高可用(分片故障不影响整体)
2. 分片键(Shard Key)
选择分片键的原则:
- ✅ 高基数(唯一值多)
- ✅ 低频率(分布均匀)
- ✅ 单调递增(避免热点)
分片策略:
javascript
// 启用分片
sh.enableSharding("mydb");
// 创建分片集合
sh.shardCollection("mydb.users", { user_id: 1 }); // 单字段分片键
// 复合分片键
sh.shardCollection("mydb.orders", { customer_id: 1, order_date: 1 });哈希分片:
javascript
// 使用哈希分片键(适合范围查询少的场景)
sh.shardCollection("mydb.logs", { _id: "hashed" });六、.NET Core 集成
1. 安装驱动
bash
dotnet add package MongoDB.Driver2. 连接 MongoDB
csharp
using MongoDB.Driver;
var client = new MongoClient("mongodb://localhost:27017");
var database = client.GetDatabase("mydb");
var collection = database.GetCollection<BsonDocument>("users");3. 基本操作
插入文档:
csharp
var user = new BsonDocument
{
{ "name", "Alice" },
{ "age", 30 },
{ "email", "alice@example.com" }
};
await collection.InsertOneAsync(user);查询文档:
csharp
// 查询单条
var filter = Builders<BsonDocument>.Filter.Eq("email", "alice@example.com");
var user = await collection.Find(filter).FirstOrDefaultAsync();
// 查询多条
var users = await collection.Find(_ => true).ToListAsync();
// 条件查询
var filter = Builders<BsonDocument>.Filter.And(
Builders<BsonDocument>.Filter.Gte("age", 18),
Builders<BsonDocument>.Filter.Lte("age", 65)
);
var users = await collection.Find(filter).ToListAsync();更新文档:
csharp
var filter = Builders<BsonDocument>.Filter.Eq("email", "alice@example.com");
var update = Builders<BsonDocument>.Update.Set("age", 31);
await collection.UpdateOneAsync(filter, update);删除文档:
csharp
var filter = Builders<BsonDocument>.Filter.Eq("email", "alice@example.com");
await collection.DeleteOneAsync(filter);4. 使用强类型模型
csharp
public class User
{
public ObjectId Id { get; set; }
public string Name { get; set; }
public int Age { get; set; }
public string Email { get; set; }
}
var collection = database.GetCollection<User>("users");
// 插入
var user = new User { Name = "Alice", Age = 30, Email = "alice@example.com" };
await collection.InsertOneAsync(user);
// 查询
var filter = Builders<User>.Filter.Eq(u => u.Email, "alice@example.com");
var user = await collection.Find(filter).FirstOrDefaultAsync();七、常见面试题
Q1: MongoDB 为什么使用 B+ 树索引?
MongoDB 使用 B 树索引(不是 B+ 树),因为:
- 范围查询 - B 树适合范围查询
- 随机访问 - B 树节点可以存储数据
- 写入性能 - B 树写入性能较好
注意: MongoDB 使用的是 B 树,不是 B+ 树。
Q2: MongoDB 如何保证数据一致性?
最终一致性:
- 主从复制是异步的
- 写入主节点后,从节点可能还没同步
- 读取从节点可能读到旧数据
强一致性:
- 使用写关注(Write Concern)
- 使用读关注(Read Concern)
javascript
// 写关注
db.users.insertOne(
{ name: "Alice" },
{ writeConcern: { w: "majority", wtimeout: 5000 } }
);
// 读关注
db.users.find({}).readConcern("majority");Q3: MongoDB 事务支持?
MongoDB 4.0+ 支持事务:
csharp
using var session = await client.StartSessionAsync();
session.StartTransaction();
try
{
await collection1.InsertOneAsync(session, doc1);
await collection2.InsertOneAsync(session, doc2);
await session.CommitTransactionAsync();
}
catch
{
await session.AbortTransactionAsync();
throw;
}限制:
- 事务中的操作必须在同一个分片上(分片集群)
- 事务时间不能太长(默认 60 秒)
Q4: MongoDB 如何优化查询性能?
创建索引
javascriptdb.users.createIndex({ email: 1 });使用投影
javascriptdb.users.find({}, { name: 1, email: 1 });使用限制和排序
javascriptdb.users.find().sort({ created_at: -1 }).limit(10);使用覆盖索引
javascript// 索引包含查询所需的所有字段 db.users.createIndex({ email: 1, name: 1 });避免全表扫描
- 使用索引字段查询
- 避免正则表达式(除非前缀匹配)
Q5: MongoDB 和关系型数据库的选择?
选择 MongoDB:
- ✅ 数据结构灵活,经常变化
- ✅ 水平扩展需求高
- ✅ 大量非结构化数据
- ✅ 高并发读写
选择关系型数据库:
- ✅ 数据结构稳定
- ✅ 需要复杂 JOIN
- ✅ 强一致性要求
- ✅ 事务复杂
八、最佳实践
- ✅ 合理使用索引 - 提升查询性能
- ✅ 使用投影 - 只返回需要的字段
- ✅ 避免全表扫描 - 使用索引字段查询
- ✅ 合理设计文档结构 - 避免过度嵌套
- ✅ 使用复制集 - 实现高可用
- ✅ 监控慢查询 - 使用 profiler
- ✅ 定期备份 - 防止数据丢失
- ✅ 设置合理的写关注 - 平衡性能和数据安全
- ❌ 避免大文档 - 单个文档不超过 16MB
- ❌ 不要在生产环境使用 Map-Reduce - 使用聚合管道