为什么 MySQL 之外还要选 MongoDB
MongoDB 是最流行的文档型数据库。它把数据存成类 JSON 的 BSON 文档,而不是关系表里行和列。当业务模型变化快、字段不固定、文档天然一层嵌套时,用 MySQL 要不断加字段、改表结构,用 MongoDB 则可以直接在文档里塞新字段,开发敏捷很多。内容管理、商品详情、日志、IoT 数据这类半结构化场景,是它的强项。
但它不是万能的。需要多表 join、强事务、复杂报表统计时,关系型数据库仍然更顺手。经验是:用文档数据库建模灵活、读写快的业务实体,用关系型数据库管核心交易与强一致,两者并存,而不是二选一。
- 文档模型一对多且一起读取的关系适合嵌入,否则用引用。
- 索引单键、复合、多键与文本索引,原则和关系库相通。
- 聚合管道用 stage 链式处理文档,完成分组统计。
- 副本集生产必须部署副本集,单点必丢数据。
文档模型:嵌入式还是引用
建模时第一个抉择是:关联数据是嵌进同一个文档,还是单独存集合再用 id 引用。判断标准很朴素:一起读、一起写、"一"的那方不会太大,就嵌入;多对多、一对巨大的多、需要独立更新的,就引用。
// 嵌入式:文章与其评论一起读,评论嵌在文档里
{
_id: ObjectId("64b2f1a3c8d9e0a1b2c3d4e5"),
title: "MongoDB 实战",
author: "极码匠人",
comments: [
{ user: "张三", text: "讲得清楚", at: ISODate("2026-10-14") },
{ user: "李四", text: "求聚合示例", at: ISODate("2026-10-14") }
]
}
嵌入式减少了 join,一次查询就能拿到完整对象,但单文档有 16MB 上限,且评论无限增长会撑爆文档。所以评论这类"会一直长下去"的子项,更适合独立成一个 comments 集合,用 post_id 引用。把一起展示的数据嵌入,把会独立增长的数据拆出去,是文档建模最实用的一条经验。
集合设计与常用类型
MongoDB 的"集合"相当于关系库的"表",但它不强制 schema,同一集合里的文档结构可以不同。实际项目里仍建议在应用层约束字段,避免随意塞字段导致数据腐化。常用类型有几个要注意:id 默认用 ObjectId,自带时间戳且全局唯一;时间一律用 Date 类型而不是字符串,否则没法做范围查询和排序;金额用整数分存储,避免浮点。
// 插入一条商品文档
db.product.insertOne({
sku: "SKU-10086",
title: "机械键盘",
priceCent: 39900,
tags: ["键盘", "青轴"],
createdAt: new Date()
});
// 按条件查询并只取需要的字段
db.product.find(
{ priceCent: { $lt: 50000 } },
{ sku: 1, title: 1, _id: 0 }
);
索引:单键、复合与多键
和 MySQL 一样,MongoDB 不建索引就全集合扫描。它的索引类型更丰富:单键索引、复合索引、数组字段上的多键索引、文本索引,以及地理空间索引。
| 索引类型 | 用途 | 示例 |
|---|---|---|
| 单键 | 单个字段等值或范围查询 | createIndex({ sku: 1 }) |
| 复合 | 多字段组合查询 | createIndex({ category: 1, priceCent: -1 }) |
| 多键 | 数组字段 | createIndex({ tags: 1 }) |
| 文本 | 全文检索 | createIndex({ title: "text" }) |
复合索引同样遵循最左前缀:{ category:1, priceCent:-1 } 能服务"先按类目再按价格"的查询,但直接按 priceCent 排序就用不上。1 与 -1 表示升降序方向,多字段排序方向要和索引方向匹配,否则照样走全集合排序。
explain 与索引失效的常见写法
排查慢查询用 explain("executionStats"),重点看 winningPlan 是不是走了索引、totalDocsExamined 是不是远大于返回行数。两者相差巨大,说明在大量扫描后才筛出结果,索引没建好或条件写法有问题。
// 反例:对索引字段做运算,索引失效
db.log.find({ year: { $expr: { $year: "$ts" } } });
// 正解:直接按时间范围查询,走 ts 上的索引
db.log.find({ ts: { $gte: new Date("2026-01-01"), $lt: new Date("2026-02-01") } });
常见失效写法还有:在查询条件里对索引字段用正则且以 ^ 开头之外的模糊、隐式把字符串当数字比较、以及 $or 里有一个分支无索引。和关系库一样,让索引字段干干净净地出现在等值或范围条件里,是最稳的原则。
聚合管道:分组、排序与投影
聚合管道是 MongoDB 做统计分析的核心。它把文档依次经过一串 stage:$match 过滤、$group 分组统计、$sort 排序、$project 投影字段。管道就像一条流水线,每个 stage 的输出是下一个 stage 的输入。
// 统计每个类目的商品数量与平均价,按数量降序取前五
db.product.aggregate([
{ $match: { onSale: true } },
{ $group: {
_id: "$category",
count: { $sum: 1 },
avgPrice: { $avg: "$priceCent" }
} },
{ $sort: { count: -1 } },
{ $limit: 5 }
]);
性能上有个关键经验:把 $match 尽量放最前面,先缩小数据量再分组排序,能显著减少后续 stage 的处理规模。如果 $match 字段上有索引,还能直接走索引扫描,而不是全集合读完再过滤。
事务、副本集与生产配置
早期 MongoDB 只支持单文档原子操作,从 4.0 开始支持多文档事务、4.2 后支持分布式事务。这意味着在副本集环境下,你可以像关系库一样对多个文档做"要么全成、要么全回滚"。但要记住,跨集合事务性能开销不小,能靠单文档原子操作解决的(比如用 $inc 原子自增计数)就不要上事务。
生产部署上,单机 MongoDB 是不可接受的。必须部署副本集:一个主节点负责读写,多个从节点同步数据并做故障转移,主节点挂了自动选举新主。再大的业务还可以按分片键把数据分散到多个分片,水平扩展存储与吞吐。磁盘上务必开 journal 日志,掉电后才能重放恢复。
选型清单与常见坑
把经验固化成一份清单:
- 建模先想读路径:界面一次要显示什么,就把那些数据嵌进同一个文档。
- 数组不要无限增长,评论、动态这类拆出去用引用。
- 高频查询字段都建索引,复合索引注意最左前缀与排序方向。
- 聚合管道把过滤放最前,并为 $match 建索引。
- 时间用 Date、金额用整数分、id 用 ObjectId。
- 生产一律副本集,别在单机上跑业务。
MongoDB 最大的价值是开发敏捷:模型变了不用改表,文档随手就能存。但灵活不等于随意,真正让它稳定快速的,仍然是想清楚读路径、建好索引、配好副本集这几件朴素的事。