MongoDB 是一个面向文档的 NoSQL 数据库,使用 BSON(Binary JSON)作为数据格式。相比传统关系型数据库的表-行结构,MongoDB 采用集合-文档的层次结构,更适合存储半结构化数据和快速迭代场景。它以水平扩展能力强、查询语言丰富、社区生态活跃而闻名,广泛应用于电商平台、物联网、实时分析以及内容管理等领域。
在生产环境中,MongoDB 通常以副本集(Replica Set)或分片集群(Sharding Cluster)的形式部署,既可以通过本地安装快速启动,也可以直接选用 MongoDB Atlas 云托管服务实现零运维上线。理解其文档模型和核心操作是进阶使用索引、聚合、事务以及性能优化的前提。
1. 核心概念对比
| 关系型数据库 | MongoDB | 说明 |
|---|---|---|
| Database | Database | 逻辑数据库 |
| Table | Collection | 集合(无固定 schema) |
| Row | Document | BSON 文档(最大 16MB) |
| Column | Field | 字段 |
| Index | Index | 索引 |
| Primary Key | _id | 默认 ObjectId |
| Join | $lookup / 嵌入 | 聚合管道或文档内嵌 |
MongoDB 不强制集合内的文档具有统一结构,这种灵活性允许在同一集合中存储不同字段组合的文档。但自由并不意味着无序,合理的 schema 设计仍然是保障查询性能和数据一致性的关键。
2. BSON 数据类型
BSON 是 MongoDB 的数据存储格式,在 JSON 的基础上扩展了二进制安全、类型丰富以及遍历高效等特性。与纯文本 JSON 相比,BSON 对数字和日期等类型有更精确的表达,避免了 JSON 中 “整数 / 浮点 / 大数” 类型模糊的问题。
{
_id: ObjectId("64a1b2c3d4e5f6a7b8c9d0e1"), // 12字节唯一ID
name: "Alice", // String
age: 30, // Int32
salary: 12500.50, // Double
isActive: true, // Boolean
tags: ["developer", "admin"], // Array
address: { // Embedded Document
city: "Beijing",
zip: "100000"
},
createdAt: ISODate("2024-01-15T08:00:00Z"), // Date
metadata: null, // Null
profile: BinData(0, "base64string"), // Binary
config: undefined // Undefined (避免使用)
}
ObjectId 结构:
64a1b2c3 d4e5f6 a7b8c9 d0e1
timestamp machine pid counter
(4B) (3B) (2B) (3B)
ObjectId 的生成规则天然具备大致有序性,常被用于默认主键和按时间排序的查询替代方案。需要注意的是,由于包含机器标识和随机种子,ObjectId 在分布式环境中具备全局唯一性,不应该依赖其精确时间戳做跨毫秒的排序。
3. 数据库与集合操作
// 创建/切换数据库
use ecommerce;
// 查看当前数据库中的集合列表
db.getCollectionNames();
// 创建集合(带选项)
db.createCollection("products", {
capped: true, // 固定大小集合,类似循环日志
size: 5242880, // 5MB
max: 5000 // 最多5000条文档
});
// 删除集合(不可逆,慎用)
db.products.drop();
// 查看集合统计信息,包括文档数、索引大小、存储大小等
db.products.stats();
固定大小集合(Capped Collection)适用于日志流和消息队列场景,写入速度极快且自动覆盖旧数据,但无法单独删除其中某条文档,也不能更新导致文档体积增大。
4. CRUD 操作
4.1 Create(插入)
// 插入单条文档
db.users.insertOne({
name: "Alice",
email: "alice@example.com",
age: 30,
createdAt: new Date()
});
// 插入多条(无序,遇到错误继续)
db.users.insertMany([
{ name: "Bob", age: 25, tags: ["new"] },
{ name: "Charlie", age: 35, tags: ["vip"] }
], { ordered: false });
// 原子递增插入,常用于自增 ID 生成器
db.counters.findOneAndUpdate(
{ _id: "orderId" },
{ $inc: { seq: 1 } },
{ upsert: true, returnDocument: "after" }
);
无序插入在批量导入时效率更高,因为单条失败不会中断整个批次。insertOne 和 insertMany 都会返回 acknowledged、insertedId 等字段,便于业务层确认写入状态。
4.2 Read(查询)
// 精确匹配
db.users.find({ age: 30 });
// 比较操作符
db.products.find({
price: { $gte: 100, $lte: 500 },
stock: { $gt: 0 }
});
// 逻辑组合
db.users.find({
$and: [
{ age: { $gte: 18 } },
{ $or: [
{ status: "active" },
{ vip: true }
]}
]
});
// 数组查询
db.users.find({ tags: { $in: ["developer", "admin"] } }); // 包含任一
db.users.find({ tags: { $all: ["developer", "mongodb"] } }); // 包含全部
db.users.find({ tags: { $size: 2 } }); // 数组长度
// 元素级匹配(数组中的对象)
db.orders.find({ items: { $elemMatch: { sku: "SKU001", qty: { $gt: 5 } } } });
// 投影(只返回指定字段,减少网络传输)
db.users.find({}, { name: 1, email: 1, _id: 0 });
// 排序+分页
db.users.find()
.sort({ createdAt: -1 }) // 降序
.skip(20) // 跳过前20条
.limit(10); // 取10条
比较操作符速查:
| 操作符 | 含义 |
|---|---|
$eq | 等于 |
$ne | 不等于 |
$gt / $gte | 大于 / 大于等于 |
$lt / $lte | 小于 / 小于等于 |
$in / $nin | 在 / 不在数组中 |
$regex | 正则匹配 |
$exists | 字段是否存在 |
$type | 字段类型匹配 |
对于高并发系统,skip 跳过大量文档成本较高,建议改用范围查询(如基于 _id 或时间戳的游标分页)来提升性能。
4.3 Update(更新)
// 更新单条
db.users.updateOne(
{ name: "Alice" },
{ $set: { age: 31, updatedAt: new Date() } }
);
// 更新多条
db.users.updateMany(
{ status: "pending" },
{ $set: { status: "active" }, $inc: { loginCount: 1 } }
);
// 更新操作符速查
{ $set: { field: value } } // 设置字段
{ $unset: { field: "" } } // 删除字段
{ $inc: { counter: 1 } } // 递增
{ $mul: { price: 1.1 } } // 相乘
{ $push: { tags: "new-tag" } } // 数组尾部添加
{ $addToSet: { tags: "new-tag" } } // 数组添加(去重)
{ $pull: { tags: "old-tag" } } // 数组移除
{ $pop: { tags: 1 } } // 数组弹出尾部(-1为头部)
{ $rename: { oldName: "newName" } } // 重命名字段
在更新操作中,$set 只会覆盖指定字段而不会替换整个文档,这是 MongoDB 更新语义与覆盖写入的本质区别。$inc 和 $mul 在并发更新数字字段时可以避免读取-修改-写入的竞争问题。
4.4 Delete(删除)
// 删除单条
db.users.deleteOne({ _id: ObjectId("64a1b2c3d4e5f6a7b8c9d0e1") });
// 删除多条
db.logs.deleteMany({ createdAt: { $lt: new Date(Date.now() - 30*24*60*60*1000) } });
// 清空集合(比逐条 deleteMany 更快,但不会回收磁盘空间)
db.users.deleteMany({});
// 删除集合并回收空间
db.users.drop();
执行删除前务必先运行 find 确认目标文档,避免误删。对于历史数据清理,可以考虑使用 TTL 索引,让 MongoDB 自动按时间删除过期文档。
5. 批量操作与优化
// 有序批量写入(默认): 遇到错误停止
db.users.bulkWrite([
{ insertOne: { document: { name: "User1" } } },
{ updateOne: { filter: { name: "Alice" }, update: { $set: { age: 32 } } } },
{ deleteOne: { filter: { name: "Bob" } } }
]);
// 游标遍历(大数据量避免内存溢出)
const cursor = db.largeCollection.find().batchSize(1000);
cursor.forEach(doc => {
process(doc);
});
// explain 分析查询性能
db.users.find({ email: "alice@example.com" }).explain("executionStats");
explain("executionStats") 是排查慢查询的核心手段。输出中的 executionTimeMillis、totalDocsExamined 和 totalKeysExamined 能直观反映索引是否命中。如果 totalDocsExamined 远大于返回文档数,说明查询正在做全表扫描,亟待添加索引。
6. 文档模型设计原则
MongoDB 的灵活性并不意味着可以随意嵌套。好的文档设计需要在查询性能、更新便利性和数据冗余之间取得平衡。
嵌入 vs 引用
嵌入适合一对少量、一起读写、不会持续增长的数据;引用适合一对多且多的一方会持续增长、需要独立更新的数据。
// 嵌入示例:订单内嵌商品条目(一起读写,条目数量有限)
db.orders.insertOne({
_id: ObjectId(),
customerId: "C001",
items: [
{ sku: "SKU001", name: "T-Shirt", qty: 2, price: 99.0 },
{ sku: "SKU002", name: "Sneakers", qty: 1, price: 599.0 }
],
total: 797.0,
createdAt: new Date()
});
// 引用示例:博客文章引用作者(作者信息独立维护,多篇文章复用)
db.authors.insertOne({ _id: ObjectId(), name: "Leeting", bio: "Backend Engineer" });
db.posts.insertOne({
title: "MongoDB 入门",
authorId: ObjectId("..."), // 引用 authors 集合
content: "...",
createdAt: new Date()
});
Schema 验证
从 MongoDB 3.6 开始,集合支持 JSON Schema 验证规则,可以强制字段类型、边界和必填项。
db.createCollection("users", {
validator: {
$jsonSchema: {
bsonType: "object",
required: ["name", "email", "age"],
properties: {
name: { bsonType: "string", description: "must be a string" },
email: { bsonType: "string", pattern: "^.+@.+$" },
age: { bsonType: "int", minimum: 0, maximum: 150 }
}
}
},
validationLevel: "strict",
validationAction: "error"
});
validationLevel: "moderate" 则仅对新插入和更新的文档生效,不影响存量数据。Schema 验证是防止脏数据进入数据库的最后一道防线。
BSON 设计注意事项
- 单个文档上限 16MB,避免无节制地内嵌数组或大型二进制对象。
- 字段名在每条文档中重复存储,命名应尽量简短。
- 避免在文档中存储冗长的空值字段,这会增加存储和传输开销。
7. CRUD 操作进阶
BulkWrite
bulkWrite 支持在一次网络往返中混合执行插入、更新、替换和删除,适合数据同步和批量迁移场景。
db.products.bulkWrite([
{ insertOne: { document: { sku: "SKU003", name: "Hat", price: 49.9 } } },
{ updateMany: { filter: { category: "clothing" }, update: { $inc: { price: 5 } } } },
{ replaceOne: { filter: { sku: "SKU001" }, replacement: { sku: "SKU001", name: "T-Shirt V2", price: 129.0 } } },
{ deleteMany: { filter: { stock: { $lte: 0 } } } }
], { ordered: false });
FindAndModify
findAndModify 是原子读写操作,适用于秒杀扣减库存、领取优惠券等需要"查后改"且必须串行化的场景。
// 原子扣减库存并返回更新后的文档
db.inventory.findOneAndUpdate(
{ sku: "SKU001", stock: { $gte: 1 } },
{ $inc: { stock: -1 } },
{ returnDocument: "after", upsert: false }
);
Upsert
当查询条件匹配不到文档时,upsert: true 会自动插入新文档,常用于如果不存在则创建、存在则更新的幂等写入。
db.configs.updateOne(
{ key: "maintenance_mode" },
{ $set: { value: true, updatedAt: new Date() } },
{ upsert: true }
);
8. 索引基础
索引是 MongoDB 查询性能的生命线。没有索引的查询会触发全集合扫描(COLLSCAN),在数据量增大时性能迅速恶化。
单字段索引
// 为 email 创建唯一索引
db.users.createIndex({ email: 1 }, { unique: true });
// 为 createdAt 创建降序索引
db.logs.createIndex({ createdAt: -1 });
复合索引
复合索引的字段顺序至关重要。最左前缀原则决定了索引能否被查询利用。
// 复合索引:先按 status 筛选,再按 createdAt 排序
db.orders.createIndex({ status: 1, createdAt: -1 });
// 该索引支持的查询
db.orders.find({ status: "shipped" }).sort({ createdAt: -1 });
db.orders.find({ status: "shipped", createdAt: { $gte: new Date("2024-01-01") } });
文本索引
文本索引用于全文检索,支持按关键词搜索和语言分词。
db.articles.createIndex({ title: "text", content: "text" });
// 搜索包含 MongoDB 或索引的文章
db.articles.find({ $text: { $search: "MongoDB indexing" } });
// 返回相关性得分并按得分排序
db.articles.find(
{ $text: { $search: "database" } },
{ score: { $meta: "textScore" } }
).sort({ score: { $meta: "textScore" } });
地理空间索引
地理空间索引支持基于坐标的位置查询,广泛应用于配送范围查询和附近推荐。
// 创建 2dsphere 索引
db.stores.createIndex({ location: "2dsphere" });
// 插入带坐标的文档
db.stores.insertOne({
name: "Beijing Store",
location: { type: "Point", coordinates: [116.4074, 39.9042] }
});
// 查询距离北京天安门 5000 米内的门店
db.stores.find({
location: {
$near: {
$geometry: { type: "Point", coordinates: [116.4074, 39.9042] },
$maxDistance: 5000
}
}
});
9. 聚合框架入门
聚合框架是 MongoDB 的数据处理管道,通过将多个阶段(Stage)串联起来,实现过滤、分组、排序、关联和重塑等复杂计算,无需将数据拉取到应用层。
// 按月份统计订单金额和数量
db.orders.aggregate([
{ $match: { status: "completed" } },
{ $group: {
_id: { $dateToString: { format: "%Y-%m", date: "$createdAt" } },
totalAmount: { $sum: "$amount" },
orderCount: { $sum: 1 }
}},
{ $sort: { _id: 1 } },
{ $limit: 12 }
]);
常用聚合阶段一览:
| 阶段 | 作用 |
|---|---|
$match | 过滤文档,类似 find 的条件 |
$group | 分组统计,必须使用 _id 指定分组键 |
$sort | 排序,内存限制 100MB,大数据量建议先 $match |
$project | 投影 / 重塑字段,控制输出结构 |
$limit / $skip | 分页截取 |
$lookup | 左外连接,关联其他集合 |
$unwind | 拆分数组,每元素生成一行 |
$addFields | 新增计算字段 |
关联查询示例
db.orders.aggregate([
{ $match: { status: "completed" } },
{ $lookup: {
from: "users",
localField: "userId",
foreignField: "_id",
as: "userInfo"
}},
{ $unwind: "$userInfo" },
{ $project: {
orderId: "$_id",
total: 1,
userName: "$userInfo.name",
userEmail: "$userInfo.email"
}}
]);
聚合管道在分片集群上会自动分发到各个分片执行,结果汇总后再进入后续阶段,具备良好的水平扩展能力。对于超大规模数据聚合,可以结合 $merge 将结果输出到目标集合,避免内存瓶颈。
10. 与主流语言集成
MongoDB 官方为各大主流语言提供了原生驱动,支持连接池、负载均衡、读写分离和事务等高级特性。
Node.js
const { MongoClient } = require('mongodb');
async function main() {
const client = new MongoClient('mongodb://localhost:27017');
await client.connect();
const db = client.db('ecommerce');
const users = db.collection('users');
await users.insertOne({ name: 'Alice', age: 30 });
const result = await users.find({ age: { $gte: 25 } }).toArray();
console.log(result);
await client.close();
}
main();
Java
import com.mongodb.client.*;
import org.bson.Document;
public class MongoExample {
public static void main(String[] args) {
try (MongoClient client = MongoClients.create("mongodb://localhost:27017")) {
MongoDatabase db = client.getDatabase("ecommerce");
MongoCollection<Document> users = db.getCollection("users");
users.insertOne(new Document("name", "Alice").append("age", 30));
users.find(new Document("age", new Document("$gte", 25)))
.forEach(doc -> System.out.println(doc.toJson()));
}
}
}
Python
from pymongo import MongoClient
client = MongoClient("mongodb://localhost:27017")
db = client["ecommerce"]
users = db["users"]
users.insert_one({"name": "Alice", "age": 30})
for doc in users.find({"age": {"$gte": 25}}):
print(doc)
实际项目中,建议配合使用 ODM / ORM 框架,如 Node.js 的 Mongoose、Java 的 Spring Data MongoDB、Python 的 MongoEngine,以获得类型安全、Schema 校验和更优雅的业务层抽象。
11. Atlas 云托管入门与免费集群
MongoDB Atlas 是 MongoDB 官方提供的全托管数据库即服务(DBaaS),支持 AWS、Azure 和 Google Cloud 三大云平台。开发者无需关注服务器选型、副本集配置、备份和版本升级,几分钟内即可创建可生产的集群。
免费集群创建步骤
- 访问 https://cloud.mongodb.com 注册账号。
- 创建新 Project,选择 Shared Cluster(M0 免费级)。
- 选择最接近用户的云区域,如 AWS
ap-east-1(香港)或cn-north-1(如可用)。 - 配置数据库访问身份验证:创建用户名和密码。
- 配置网络访问白名单,建议先添加
0.0.0.0/0方便开发测试,生产环境替换为固定出口 IP。 - 点击 Connect,选择驱动程序类型,复制连接字符串。
连接字符串示例:
mongodb+srv://<user>:<password>@cluster0.xxxxx.mongodb.net/mydb?retryWrites=true&w=majority
Atlas 免费层提供 512MB 存储空间、共享 vCPU 和三节点副本集,足够支持个人项目、原型验证和小型生产应用。付费层支持自动扩展、全局集群、网络隔离(VPC Peering / PrivateLink)以及高级监控告警。
常用 Atlas 功能
- Real-Time Performance Panel:实时查看慢查询和锁竞争。
- Data Explorer:Web 界面直接浏览和修改文档。
- Triggers:基于数据库变更事件触发 Serverless 函数(类似 AWS Lambda)。
- Data Lake:将 Atlas 数据与 AWS S3 / Azure Blob 打通,统一分析查询。
延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。