Skip to content

MongoDB高级

MongoDB 高级部分重点关注索引、聚合、复制集和分片。文档数据库的灵活性很强,但如果索引和文档结构设计不好,也会出现严重性能问题。

为什么高级能力很重要

MongoDB 入门 CRUD 很简单,但真正上线后问题通常出在高级能力:查询不走索引、聚合扫描太多数据、主节点故障后不会切换、分片键选错导致热点。高级能力解决的是“数据量变大、并发变高、机器故障后还能不能稳定运行”的问题。

知识结构

mermaid
flowchart TD
    A["文档设计"] --> B["索引"]
    B --> C["聚合管道"]
    C --> D["复制集"]
    D --> E["分片集群"]
    E --> F["事务"]
    F --> G["备份和监控"]

索引

MongoDB 的索引用于提高查询效率。常见索引类型:

  • 单字段索引。
  • 复合索引。
  • 多键索引,适用于数组字段。
  • TTL 索引,适合自动过期数据。
  • 文本索引,适合简单全文检索。

复合索引也需要关注字段顺序,通常把等值查询字段放前面,范围查询字段放后面。

索引执行过程

mermaid
flowchart TD
    A["find 条件"] --> B{"是否有合适索引"}
    B -- "有" --> C["IXSCAN 扫描索引"]
    C --> D["FETCH 读取文档"]
    D --> E["过滤、排序、返回"]
    B -- "没有" --> F["COLLSCAN 扫描集合"]
    F --> E

索引不是越多越好:

  1. 每个索引都占磁盘和内存。
  2. 写入、更新、删除要维护索引。
  3. 索引字段顺序不匹配查询,可能仍然不好用。
  4. 低选择性字段单独建索引意义有限。

ESR 原则

复合索引常用 ESR 思路:

字母含义示例
EEquality 等值条件hospitalId = H001
SSort 排序字段createdAt desc
RRange 范围条件createdAt >= begin

实际设计要看查询:

javascript
db.assets.find({
  hospitalId: "H001",
  status: "NORMAL",
  createdAt: { $gte: ISODate("2026-07-01") }
}).sort({ createdAt: -1 }).limit(20)

可考虑:

javascript
db.assets.createIndex({
  hospitalId: 1,
  status: 1,
  createdAt: -1
})

hospitalIdstatus 是等值过滤,createdAt 同时服务范围和排序。

多键索引

数组字段建索引会形成多键索引:

javascript
db.assets.createIndex({ tags: 1 })
db.assets.find({ tags: "vip" })

如果一个文档数组很大,索引项也会很多,写入和更新成本会上升。标签数组可以,日志数组不可以无限增长。

TTL 索引

TTL 索引用于自动过期删除文档,适合验证码、临时 token、短期日志。

javascript
db.verify_codes.createIndex(
  { expireAt: 1 },
  { expireAfterSeconds: 0 }
)

注意:

  1. TTL 删除不是精确到秒实时删除。
  2. 不适合核心业务数据自动删除。
  3. 重要数据应先归档再清理。

聚合管道

聚合管道用于数据处理和统计,常见阶段:

  • $match:过滤数据。
  • $project:选择或计算字段。
  • $group:分组统计。
  • $sort:排序。
  • $limit:限制数量。
mermaid
flowchart TD
    A["原始文档"] --> B["$match 过滤"]
    B --> C["$group 分组"]
    C --> D["$sort 排序"]
    D --> E["统计结果"]

核心原理:先减少数据再计算

聚合管道是一段一段处理文档流。越靠前的阶段处理的数据量越大,所以能提前过滤就提前 $match,能提前裁剪字段就 $project。如果先 $group$sort 大量数据,再过滤,内存和 CPU 成本都会明显增加。

聚合优化流程

mermaid
flowchart TD
    A["原始集合"] --> B["$match 尽早过滤"]
    B --> C["$project 裁剪字段"]
    C --> D["$group 分组计算"]
    D --> E["$sort 排序"]
    E --> F["$limit 限制结果"]

错误写法:

javascript
db.asset_collect_logs.aggregate([
  { $group: { _id: "$assetNo", total: { $sum: 1 } } },
  { $match: { total: { $gt: 100 } } }
])

如果集合很大,这会先处理全量日志。

更合理的写法:

javascript
db.asset_collect_logs.aggregate([
  {
    $match: {
      collectTime: {
        $gte: ISODate("2026-07-01"),
        $lt: ISODate("2026-07-02")
      }
    }
  },
  { $group: { _id: "$assetNo", total: { $sum: 1 } } },
  { $match: { total: { $gt: 100 } } },
  { $sort: { total: -1 } }
])

$lookup 要谨慎

MongoDB 支持 $lookup 做集合关联,但不能把它当成关系库 Join 的完全替代。

javascript
db.assets.aggregate([
  { $match: { hospitalId: "H001" } },
  {
    $lookup: {
      from: "departments",
      localField: "deptId",
      foreignField: "_id",
      as: "dept"
    }
  }
])

注意:

  1. 关联字段要有索引。
  2. $lookup 前要先 $match 缩小主集合。
  3. 高频复杂关联说明文档模型可能设计错了。
  4. 大报表更适合预聚合或离线计算。

复制集

复制集提供高可用。一个主节点负责写入,多个从节点复制数据。主节点故障后,集群会选举新的主节点。

mermaid
sequenceDiagram
    participant App as 应用
    participant P as Primary
    participant O as Oplog
    participant S as Secondary
    App->>P: 写入
    P->>O: 记录 oplog
    S->>O: 拉取 oplog
    S->>S: 重放操作

Oplog 是什么

Oplog 是复制集用于同步的操作日志。Primary 写入后把操作记录到 oplog,Secondary 按顺序拉取并重放。

重点:

  1. Oplog 是有大小限制的循环日志。
  2. Secondary 落后太久,oplog 已覆盖,就需要重新同步。
  3. Oplog 延迟会影响读从库的实时性。
  4. 复制集提高可用性,但不能替代备份。

选举和写关注

Primary 故障后,复制集会选举新的 Primary。写入确认由 writeConcern 控制。

writeConcern含义适用
{ w: 1 }Primary 写成功即返回性能高,风险较高
{ w: "majority" }多数节点确认核心数据更稳
{ w: "majority", j: true }多数确认并写 journal更强持久性

如果业务是订单、资产主数据、审计日志,通常不能只追求低延迟,要考虑写入可靠性。

读偏好

读可以配置 readPreference,例如读 Primary 或 Secondary。但读 Secondary 可能读到旧数据。

readPreference特点
primary读主,数据最新
secondary读从,可能延迟
primaryPreferred优先主,主不可用读从

不要把从库读当成无成本扩容。复制延迟会让用户刚写完看不到最新数据。

分片

当单机容量或吞吐不足时,可以使用分片集群。分片的关键是选择合适的 shard key。错误的 shard key 会导致数据倾斜或热点写入。

mermaid
flowchart TD
    A["应用请求"] --> B["mongos 路由"]
    B --> C["Config Server 路由元数据"]
    B --> D["Shard 1"]
    B --> E["Shard 2"]
    B --> F["Shard 3"]

shard key 怎么选

好的 shard key 要同时考虑:

  1. 写入是否均匀。
  2. 查询是否能带上 shard key。
  3. 数据是否容易按 shard key 分布。
  4. 是否会出现单点热点。
  5. 是否会导致大量跨分片查询。
shard key风险
自增时间字段新写入集中到一个分片,形成热点
低基数字段,如 status数据分布严重倾斜
随机值写入均匀,但范围查询差
tenantId + hash多租户较常见,但要看查询模式

分片前要问的问题

  1. 单机容量真的不够了吗?
  2. 是否先优化索引、模型、冷热分离?
  3. 查询是否大多能带 shard key?
  4. 运维是否具备分片集群能力?
  5. 备份、恢复、监控、扩容方案是否准备好?

分片能解决水平扩展,但会显著增加复杂度。不要把分片当成性能问题的第一解。

事务

MongoDB 支持多文档事务,但不应该滥用。它的文档模型本来就是为了把强一致修改尽量收敛在一个文档内。

适合事务:

  1. 少量文档。
  2. 短事务。
  3. 明确业务边界。
  4. 需要同时修改主数据和审计数据。

不适合事务:

  1. 批量长事务。
  2. 跨大量分片。
  3. 大规模报表更新。
  4. 本该用关系库表达的复杂强关系。

事务示意:

javascript
const session = db.getMongo().startSession()
session.startTransaction()

try {
  const assets = session.getDatabase("app").assets
  const audits = session.getDatabase("app").asset_audits

  assets.updateOne(
    { assetNo: "A-001", status: "WAIT_CHECK" },
    { $set: { status: "CHECKED", checkedAt: new Date() } }
  )

  audits.insertOne({
    assetNo: "A-001",
    action: "CHECK",
    createdAt: new Date()
  })

  session.commitTransaction()
} catch (e) {
  session.abortTransaction()
  throw e
}

备份和监控

备份不是“有副本集就够了”。副本集会同步误删和误更新,所以仍然要备份。

能力解决什么不能解决什么
副本集节点故障、高可用误删恢复
分片容量和吞吐扩展模型和索引问题
备份误删、灾难恢复实时高可用
监控发现慢查询、延迟、容量自动修复设计错误

监控重点:

  1. 慢查询。
  2. 复制延迟。
  3. Oplog 窗口。
  4. 连接数。
  5. 内存和磁盘 IO。
  6. 索引命中和集合扫描。
  7. 分片数据倾斜。

使用建议

  1. 根据查询设计索引,不要只根据字段设计索引。
  2. 聚合前尽量先 $match 减少数据量。
  3. 大集合必须关注慢查询和索引命中。
  4. 分片前先确认业务真的需要,分片会增加运维复杂度。
  5. 重要数据要设计备份和恢复方案。

命令 Demo:索引和聚合

创建复合索引:

javascript
db.article.createIndex({ category: 1, createdAt: -1 })

查询并查看执行计划:

javascript
db.article.find({
  category: "database"
}).sort({
  createdAt: -1
}).limit(10).explain("executionStats")

按分类统计文章数:

javascript
db.article.aggregate([
  { $match: { status: "published" } },
  { $group: { _id: "$category", total: { $sum: 1 } } },
  { $sort: { total: -1 } }
])

$match 尽量放在前面,可以减少后续管道要处理的数据量。

商业场景:资产采集平台怎么用 MongoDB

场景:资产主数据放 MongoDB,采集日志持续增长。

设计:

javascript
db.assets.createIndex({ assetNo: 1 }, { unique: true })
db.assets.createIndex({ hospitalId: 1, status: 1, createdAt: -1 })
db.asset_collect_logs.createIndex({ assetNo: 1, collectTime: -1 })
db.asset_collect_logs.createIndex({ collectTime: 1 }, { expireAfterSeconds: 60 * 60 * 24 * 180 })

解释:

  1. 资产主数据按 assetNo 唯一。
  2. 资产列表按医院、状态、时间查询。
  3. 采集日志按资产和时间查询最近记录。
  4. 采集日志保留半年可用 TTL,但核心审计不能随便 TTL 删除。
  5. 如果日志量继续增长,优先考虑冷热拆分和归档,再考虑分片。

面试标准回答

text
MongoDB 高级能力主要包括索引、聚合管道、复制集、分片、事务、备份和监控。索引要按查询模式设计,复合索引要关注等值、排序和范围字段顺序,并用 explain 验证 totalKeysExamined、totalDocsExamined 和 nReturned。聚合管道要尽早 $match、$project,避免全量 $group、$sort。复制集通过 Primary 写入、oplog 同步和选举提供高可用;分片通过 shard key 把数据分布到多个 shard,但 shard key 选错会导致热点、倾斜和广播查询。MongoDB 支持多文档事务,但应优先通过文档建模减少事务需求。副本集不能替代备份,生产还要监控慢查询、复制延迟、oplog 窗口、连接数和分片倾斜。