MongoDB 是当下最主流的文档型数据库。它用 BSON 文档取代行与列,让”一个订单带着它的商品列表、收货地址一起存”成为自然的事。本文从 Docker 一键起实例讲起,覆盖文档模型设计、聚合管道(Aggregation Pipeline)与索引优化三条主线,并给出生产避坑清单,帮你把 MongoDB 用对、用好。
一、为什么选 MongoDB:文档模型的核心优势
关系型数据库要求先把数据”切”成多张表,再用 JOIN 拼回来。文档型数据库反其道而行:把业务实体作为一个整体存成一份文档。这对”结构多变、嵌套层级深、读多写少”的场景格外友好。当你已经在用 PostgreSQL 与 SQL Server 对比 这类关系型方案时,引入 MongoDB 不是为了取代,而是补足半结构化数据的短板。
| 维度 | 关系型(PostgreSQL/MySQL) | 文档型(MongoDB) |
|---|---|---|
| 数据形态 | 严格的行列、预定义 Schema | 灵活的 BSON 文档、Schema 可演进 |
| 典型查询 | 多表 JOIN、聚合 SQL | 单文档读取、嵌套查询、聚合管道 |
| 扩展方式 | 垂直升级、读写分离 | 原生分片(Sharding)水平扩展 |
| 擅长场景 | 强一致事务、报表分析 | 内容/配置/物联网/用户画像 |
二、用 Docker 快速起一个 MongoDB 实例
本地验证最快的方式是 Docker。如果你还没装 Docker,可参考 Docker 实战 先把环境拉起来。下面用 docker-compose 起一个带账号密码的单节点实例:
version: "3.8"
services:
mongo:
image: mongo:7.0
container_name: mongo-demo
restart: unless-stopped
environment:
MONGO_INITDB_ROOT_USERNAME: admin
MONGO_INITDB_ROOT_PASSWORD: secret123
ports:
- "27017:27017"
volumes:
- mongo_data:/data/db
volumes:
mongo_data:
进入交互式 shell(mongosh)验证连通性:
docker exec -it mongo-demo mongosh -u admin -p secret123 --authenticationDatabase admin
# 切换到业务库并插入一条测试文档
use shop
db.products.insertOne({ name: "机械键盘", price: 299, tags: ["外设", "键盘"], stock: 120 })
db.products.find().pretty()
三、文档模型设计:嵌入还是引用
3.1 嵌入(Embedding)把关联数据放进同一文档
订单和商品明细是一对多、且明细不独立复用的关系,适合嵌入:
db.orders.insertOne({
orderNo: "SO-2026-001",
userId: 1001,
createdAt: new Date(),
items: [
{ sku: "KBD-87", name: "机械键盘", qty: 1, price: 299 },
{ sku: "CAB-01", name: "数据线", qty: 2, price: 19 }
],
total: 337,
status: "paid"
})
3.2 引用(Reference)处理多对多与独立实体
当用户、店铺是独立实体、会被多处引用时,存 ID 更合适,读取时用 $lookup 关联:
db.orders.insertOne({
orderNo: "SO-2026-002",
userId: ObjectId("650f..."),
shopId: ObjectId("651a..."),
items: [ { sku: "KBD-87", qty: 1, price: 299 } ]
})
| 判断点 | 选嵌入 | 选引用 |
|---|---|---|
| 数据是否被多方复用 | 否 | 是 |
| 子文档是否持续增长变大 | 否(16MB 文档上限) | 是 |
| 是否需要独立更新子实体 | 否 | 是 |
| 读取是否总是整体取出 | 是 | 否 |
四、CRUD 实战:从写入到更新
MongoDB 的增删改查语法贴近 JSON 直觉。下面是常见写法:
# 批量插入
db.products.insertMany([
{ name: "鼠标", price: 89, tags: ["外设"], stock: 200 },
{ name: "显示器", price: 1299, tags: ["外设"], stock: 60 }
])
# 条件查询:价格小于 300 且标签包含"外设"
db.products.find({ price: { $lt: 300 }, tags: "外设" })
# 更新:给库存小于 100 的商品加 50
db.products.updateMany(
{ stock: { $lt: 100 } },
{ $inc: { stock: 50 } }
)
# 删除:下架名称为"数据线"的商品
db.products.deleteMany({ name: "数据线" })
五、聚合管道:分组、统计与多阶段处理
聚合管道是 MongoDB 最强大的特性。它把数据像流水线一样依次经过 $match、$group、$sort 等阶段。比如统计每个标签下的商品数量与平均价格:
db.products.aggregate([
{ $match: { stock: { $gt: 0 } } },
{ $unwind: "$tags" },
{ $group: {
_id: "$tags",
count: { $sum: 1 },
avgPrice: { $avg: "$price" },
totalStock: { $sum: "$stock" }
}},
{ $sort: { count: -1 } },
{ $limit: 5 }
])
| 阶段 | 作用 | 类比 SQL |
|---|---|---|
| $match | 过滤文档 | WHERE |
| $group | 分组聚合 | GROUP BY |
| $project | 字段裁剪/重命名 | SELECT 列 |
| $lookup | 关联另一集合 | LEFT JOIN |
| $sort / $limit | 排序与截取 | ORDER BY / LIMIT |
六、索引优化与执行计划
没有索引时,MongoDB 会做全集合扫描(COLLSCAN)。给高频查询字段建索引后,查询会走 IXSCAN。下面创建一个复合索引并查看执行计划:
# 给 tags + price 建复合索引(注意顺序:等值在前、范围在后)
db.products.createIndex({ tags: 1, price: -1 })
# 查看执行计划,确认是否用到索引
db.products.find({ tags: "外设", price: { $lt: 500 } }).explain("executionStats")
# 查看现有索引与大小
db.products.getIndexes()
db.products.aggregate([{ $indexStats: {} }])
| 索引类型 | 创建方式 | 适用 |
|---|---|---|
| 单字段 | createIndex({ price: 1 }) | 单一条件查询 |
| 复合 | createIndex({ a:1, b:-1 }) | 多条件组合 |
| 唯一 | createIndex({ sku:1 }, { unique:true }) | 防重复(如订单号) |
| 文本 | createIndex({ name:”text” }) | 关键词模糊搜索 |
当查询变慢时,关系型侧常用执行计划分析(见 PostgreSQL 慢查询排查),MongoDB 侧则靠 explain() 的 executionStats 找出 COLLSCAN 与低效排序。注意:索引不是越多越好,每个索引都会拖慢写入并占用内存。
七、生产避坑清单
| 坑 | 现象 | 对策 |
|---|---|---|
| 大文档嵌入无限增长 | 超过 16MB 写入失败 | 改引用模型,或做历史归档 |
| 索引缺失导致全表扫 | CPU 飙高、响应变慢 | explain 定位,补复合索引 |
| 误用 $where / 全量 JS | 阻塞主线程 | 用原生查询操作符替代 |
| 没设唯一索引 | 订单号重复 | 业务键加 unique 索引 |
| 无副本集 | 单点故障丢数据 | 至少 3 节点副本集 + 定期备份 |
八、与关系型数据库的边界
MongoDB 不是银弹。强一致的事务、复杂多表关联、严格报表,仍然是关系型数据库的领地;而结构不稳定、嵌套深、需要快速迭代 schema 的场景,文档模型优势明显。务实的做法是”按数据特性选型”:核心账务留在 PostgreSQL,商品画像与日志分析交给 MongoDB。在 VS Code 里装一个 MongoDB 插件,就能边写聚合管道边看结果,调试效率很高。
九、聚合管道进阶:时间分桶与多面统计
基础管道解决分组,进阶管道则能直接产出报表。比如按天统计订单金额,并对价格做区间分桶,再用 $facet 一次性拿到”分页数据 + 总数”:
db.orders.aggregate([
{ $match: { status: "paid", createdAt: { $gte: ISODate("2026-10-01") } } },
{ $group: {
_id: { $dateToString: { format: "%Y-%m-%d", date: "$createdAt" } },
gmv: { $sum: "$total" },
orders: { $sum: 1 }
}},
{ $sort: { _id: 1 } }
])
# 用 $bucket 做价格区间分布,用 $facet 同时分页与计数
db.products.aggregate([
{ $facet: {
page: [ { $skip: 0 }, { $limit: 10 } ],
total: [ { $count: "count" } ],
buckets: [ { $bucket: {
groupBy: "$price",
boundaries: [0, 100, 500, 1000, 100000],
default: "other",
output: { count: { $sum: 1 } }
}}]
}}
])
| 进阶阶段 | 用途 | 典型算子 |
|---|---|---|
| 时间维度 | 按天/月汇总指标 | $dateToString、$year、$month |
| 分布分析 | 价格/分位数区间 | $bucket、$bucketAuto |
| 一查多果 | 分页 + 总数 + 分组 | $facet |
| 关联补全 | 跨集合补齐字段 | $lookup + $unwind |
十、副本集、事务与高可用
单节点只是玩具,生产要用副本集(一主多从 + 选举)。在副本集之上,MongoDB 支持多文档 ACID 事务,适合”扣库存 + 写订单”必须同时成功的场景:
const session = db.getMongo().startSession();
session.startTransaction();
try {
session.getDatabase("shop").orders.insertOne(
{ orderNo: "SO-2026-009", userId: 1001, total: 299, status: "paid" },
{ session }
);
session.getDatabase("shop").products.updateOne(
{ sku: "KBD-87" },
{ $inc: { stock: -1 } },
{ session }
);
session.commitTransaction();
print("事务提交成功");
} catch (e) {
session.abortTransaction();
print("事务回滚:", e);
}
| 能力 | 单文档写入 | 多文档事务 |
|---|---|---|
| 原子性 | 天然保证 | 显式 startTransaction |
| 性能开销 | 极低 | 较高,需谨慎使用 |
| 适用 | 绝大多数写操作 | 跨集合/跨文档强一致 |
经验法则:能用单文档原子操作就别上事务。事务会把相关文档锁在一段会话里,滥用会拖垮并发。
十一、备份与恢复
再好的索引也救不了误删。定期备份是底线。逻辑备份用 mongodump,恢复用 mongorestore,二者按连接串工作:
# 全量备份到本地目录
mongodump --uri="mongodb://admin:secret123@localhost:27017" --out=/backup/$(date +%F)
# 只备份某个库
mongodump --uri="mongodb://admin:secret123@localhost:27017/shop" --out=/backup/shop
# 从备份恢复
mongorestore --uri="mongodb://admin:secret123@localhost:27017" --drop /backup/2026-10-18
| 方式 | 优点 | 缺点 |
|---|---|---|
| mongodump 逻辑备份 | 可单库单集合恢复、可读 | 大数据量慢、有锁 |
| 文件系统快照 | 快、一致性强 | 依赖底层存储(如 LVM/云盘) |
| oplog 增量 | 可点到点恢复 | 配置复杂 |
至此,你已经掌握了 MongoDB 从起步到生产的完整主链路:Docker 起实例、嵌入/引用取舍、CRUD、聚合管道(含时间分桶与多面统计)、索引与执行计划、副本集事务、备份恢复,以及一份可直接落地的避坑清单。下一步可以结合分片(Sharding)把单库扩展到真正的水平分布式规模。




