为什么 MySQL 之外还要选 MongoDB

MongoDB 是最流行的文档型数据库。它把数据存成类 JSON 的 BSON 文档,而不是关系表里行和列。当业务模型变化快、字段不固定、文档天然一层嵌套时,用 MySQL 要不断加字段、改表结构,用 MongoDB 则可以直接在文档里塞新字段,开发敏捷很多。内容管理、商品详情、日志、IoT 数据这类半结构化场景,是它的强项。

但它不是万能的。需要多表 join、强事务、复杂报表统计时,关系型数据库仍然更顺手。经验是:用文档数据库建模灵活、读写快的业务实体,用关系型数据库管核心交易与强一致,两者并存,而不是二选一。

本文核心知识点
  • 文档模型一对多且一起读取的关系适合嵌入,否则用引用。
  • 索引单键、复合、多键与文本索引,原则和关系库相通。
  • 聚合管道用 stage 链式处理文档,完成分组统计。
  • 副本集生产必须部署副本集,单点必丢数据。
MongoDB 文档模型示意:蓝色渐变背景上三张层叠的半透明文档卡片,旁边放大镜聚焦,下方漏斗状聚合管道汇聚成一个发光结果块
文档卡片被索引放大、再经聚合漏斗过滤汇聚——这正是"文档存储、索引定位、管道聚合"三步的抽象。

文档模型:嵌入式还是引用

建模时第一个抉择是:关联数据是嵌进同一个文档,还是单独存集合再用 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 字段上有索引,还能直接走索引扫描,而不是全集合读完再过滤。