POST · 数据库 · 文章

MongoDB 从入门到进阶

2026/11/13 · by ZHANG XING YUN · 阅读 3 次 · 3667 字 · 预计阅读 10 分钟

本文覆盖 MongoDB 的文档模型、CRUD、查询与更新操作符、索引、聚合管道、事务、副本集与读写关注、Schema 设计与运维命令。

  • 环境:MongoDB 7、mongosh
  • 定位:文档型数据库,适合结构灵活、以整体读写为主的数据

一、文档模型与基本概念

关系型 MongoDB 说明
database database 数据库
table collection 集合,无固定结构
row document 文档,JSON 形式(底层 BSON)
column field 字段,可嵌套
primary key _id 默认唯一索引,未指定时自动生成 ObjectId
join $lookup 聚合阶段,能力弱于 SQL JOIN
transaction transaction 4.0+ 副本集、4.2+ 分片支持
{
  "_id": { "$oid": "65f0c3a1e2b4c9d8f7a6b5c4" },
  "title": "第一条笔记",
  "tags": ["demo", "mongodb"],
  "author": { "id": 1001, "name": "张三" },
  "createdAt": { "$date": "2026-01-13T10:00:00.000Z" }
}

文档的特点:一个文档就是一个完整的对象,读取时一次取回,不需要 JOIN;代价是跨文档的关联与事务能力弱于关系库。


二、安装与连接

2.1 Docker

docker run -d --name mongo \
  -p 27017:27017 \
  -e MONGO_INITDB_ROOT_USERNAME=root \
  -e MONGO_INITDB_ROOT_PASSWORD=secret \
  -v mongodata:/data/db \
  mongo:7

带认证的连接串必须指定 authSource=admin:

mongodb://root:secret@localhost:27017/appdb?authSource=admin

2.2 连接

mongosh "mongodb://root:secret@localhost:27017/appdb?authSource=admin"

# 或分步
mongosh
> use admin
> db.auth("root", "secret")
> use appdb

2.3 单机启用认证

mongod --auth --dbpath /data/db

首次需要先用 localhost 例外创建管理员:

use admin
db.createUser({ user: "root", pwd: "secret", roles: [{ role: "root", db: "admin" }] })

三、mongosh 常用命令

命令 作用
show dbs 列出数据库
use appdb 切换数据库(不存在则延迟创建)
show collections 列出集合
db.stats() 数据库统计
db.users.countDocuments() 文档数
db.users.find().limit(5) 查询
db.users.getIndexes() 索引
db.users.explain().find(...) 执行计划
db.currentOp() 当前操作
db.killOp(id) 终止操作
db.setProfilingLevel(2) 开启慢查询分析
exit 退出

集合不存在时,首次写入会自动创建;也可以显式创建并指定选项:

db.createCollection("notes", {
  validator: { $jsonSchema: { bsonType: "object", required: ["title"] } }
})

四、BSON 数据类型

类型 示例 说明
String "abc"
Int32 / Int64 1、NumberLong("1") 整数有范围区分
Double 1.5 浮点
Decimal128 NumberDecimal("19.99") 金额必须用它
Boolean true
Date ISODate("2026-01-13T10:00:00Z") 毫秒精度,UTC
ObjectId ObjectId("...") 12 字节,含时间戳
Array [1, 2] 可混合类型
Object { a: 1 } 嵌套文档
Null null
Binary BinData(...) 二进制
Regex /abc/i 正则

两个容易踩的点:

  1. 金额用 Decimal128,不要用 Number(double 精度问题)。
  2. 同一个字段在不同文档里可能是不同 BSON 类型(如有的存字符串、有的存数字),查询时不会互相匹配,需要统一写入路径。

五、插入文档

db.users.insertOne({ name: "张三", email: "a@example.com", points: 10 })

db.users.insertMany([
  { name: "李四", email: "b@example.com" },
  { name: "王五", email: "c@example.com" }
])

// 指定 _id 与时间
db.notes.insertOne({
  _id: ObjectId(),
  title: "第一条",
  createdAt: new Date()
})

返回结果包含 insertedId / insertedIds。插入时未定义的字段不会占位(与关系表不同),因此同一集合的文档可以结构不同。


六、查询

6.1 基本查询

db.users.find({ name: "张三" })
db.users.findOne({ email: "a@example.com" })
db.users.find({}, { name: 1, email: 1, _id: 0 })   // 投影:1 保留,0 排除

投影规则:_id 默认返回,排除需显式 _id: 0;除 _id 外不能混用 1 与 0。

6.2 查询操作符

类别 操作符 示例
比较 $eq $ne $gt $gte $lt $lte $in $nin { age: { $gte: 18 } }
逻辑 $and $or $nor $not { $or: [{ a: 1 }, { b: 2 }] }
存在 $exists { email: { $exists: true } }
类型 $type { age: { $type: "int" } }
数组 $all $size $elemMatch { tags: { $all: ["a", "b"] } }
正则 $regex { name: { $regex: "^张" } }
空值 null { email: null } 同时匹配 null 与缺失字段
// 数组包含某个值
db.notes.find({ tags: "mongodb" })

// 数组元素同时满足多个条件
db.notes.find({ tags: { $elemMatch: { $regex: "^demo" } } })

// 嵌套字段
db.users.find({ "author.id": 1001 })

// 组合条件
db.users.find({
  $and: [
    { points: { $gte: 10 } },
    { $or: [{ status: "active" }, { status: { $exists: false } }] }
  ]
})

null 查询的陷阱:{ email: null } 会匹配"值为 null"和"字段不存在"两种情况;只想匹配显式 null 时用 { email: { $type: "null" } }。

6.3 排序与分页

db.users.find().sort({ createdAt: -1 }).skip(20).limit(10)

skip 越大越慢,深分页改用游标:

db.users.find({ createdAt: { $lt: lastCreatedAt } })
  .sort({ createdAt: -1 }).limit(20)

6.4 计数与去重

db.users.countDocuments({ status: "active" })
db.users.distinct("status")

countDocuments 是准确计数(会扫描);estimatedDocumentCount 用元数据,快但只是近似。


七、更新

// 更新一条
db.users.updateOne({ email: "a@example.com" }, { $set: { points: 20 } })

// 更新多条
db.users.updateMany({ status: "active" }, { $inc: { points: 1 } })

// 替换整份文档(只保留 _id 与新内容)
db.users.replaceOne({ email: "a@example.com" }, { name: "张三", email: "a@example.com" })

// upsert:不存在则插入
db.users.updateOne(
  { email: "d@example.com" },
  { $set: { name: "赵六" }, $setOnInsert: { createdAt: new Date() } },
  { upsert: true }
)

7.1 更新操作符

操作符 作用
$set 设置字段
$unset 删除字段
$inc 数值增减
$mul 数值相乘
$min / $max 取较小/较大值
$push 数组追加
$addToSet 数组追加(去重)
$pull 数组移除匹配元素
$pop 移除首/尾元素
$rename 重命名字段
$currentDate 设为当前时间
// 数组操作
db.notes.updateOne(
  { _id: noteId },
  {
    $addToSet: { tags: "new" },
    $push: { comments: { user: "u1", text: "好" } }
  }
)

// 删除数组中的元素
db.notes.updateOne({ _id: noteId }, { $pull: { tags: "new" } })

7.2 原子性与返回

  • 单文档更新是原子的;多文档更新不是。
  • findOneAndUpdate 可以在同一次操作中返回更新前后文档:
db.users.findOneAndUpdate(
  { email: "a@example.com" },
  { $inc: { points: 1 } },
  { returnDocument: "after" }
)

7.3 管道更新(4.2+)

db.users.updateMany(
  { points: { $lt: 0 } },
  [{ $set: { points: 0, updatedAt: "$$NOW" } }]
)

八、删除

db.users.deleteOne({ email: "a@example.com" })
db.users.deleteMany({ status: "disabled" })
db.users.drop()                 // 删除集合
db.dropDatabase()               // 删除数据库

deleteMany({}) 会清空集合但保留索引,drop() 会连同索引一起删除。


九、索引

db.users.createIndex({ email: 1 }, { unique: true })
db.orders.createIndex({ userId: 1, createdAt: -1 })

// 部分索引:只为满足条件的文档建索引
db.orders.createIndex({ status: 1 }, { partialFilterExpression: { status: "pending" } })

// TTL 索引:到期自动删除(如会话、验证码)
db.sessions.createIndex({ createdAt: 1 }, { expireAfterSeconds: 3600 })

// 文本索引
db.notes.createIndex({ title: "text", body: "text" })

// 唯一 + 稀疏:只对存在的字段生效
db.users.createIndex({ phone: 1 }, { unique: true, sparse: true })

db.users.getIndexes()
db.users.dropIndex("email_1")

索引类型:

类型 用途
单键 常规查询
复合 多字段条件,遵循最左前缀
多键(数组) 数组元素查询
文本 全文检索(一个集合只能一个)
哈希 分片键,等值查询
TTL 过期自动清理
部分 仅对部分文档建索引,节省空间

复合索引的最左前缀原则:{ a: 1, b: 1 } 能支持 a、a + b 的查询,不能单独支持 b。

查看索引使用情况:

db.users.aggregate([{ $indexStats: {} }]).forEach(i => print(i.name, i.accesses.ops))

长期 ops 为 0 的索引可以考虑删除。


十、聚合管道

聚合是把文档依次经过多个阶段处理,类似 Unix 管道。

db.orders.aggregate([
  { $match: { status: "paid" } },
  { $group: { _id: "$userId", total: { $sum: "$amount" }, count: { $sum: 1 } } },
  { $sort: { total: -1 } },
  { $limit: 10 }
])

常用阶段:

阶段 作用
$match 过滤,尽量放在最前并命中索引
$project 选择/计算字段
$group 分组聚合
$sort 排序
$limit / $skip 分页
$unwind 展开数组
$lookup 关联其他集合
$facet 同一批数据多路聚合
$bucket 按区间分桶
$addFields 增加计算字段
$replaceRoot 替换文档根

10.1 分组与累计

db.orders.aggregate([
  { $match: { createdAt: { $gte: ISODate("2026-01-01") } } },
  {
    $group: {
      _id: { day: { $dateToString: { format: "%Y-%m-%d", date: "$createdAt" } } },
      total: { $sum: "$amount" },
      avg: { $avg: "$amount" }
    }
  },
  { $sort: { "_id.day": 1 } }
])

10.2 关联($lookup)

db.orders.aggregate([
  { $match: { status: "paid" } },
  {
    $lookup: {
      from: "users",
      localField: "userId",
      foreignField: "_id",
      as: "user"
    }
  },
  { $unwind: { path: "$user", preserveNullAndEmptyArrays: true } },
  { $project: { orderId: "$_id", amount: 1, "user.name": 1 } }
])

$lookup 不会自动走索引,foreignField 必须有索引,否则大集合会非常慢。另一个常见问题是两边字段类型必须一致:如果 orders.userId 存的是字符串而 users._id 是 ObjectId,关联结果永远是空数组。

10.3 分面查询

db.products.aggregate([
  { $match: { category: "book" } },
  {
    $facet: {
      byPrice: [{ $group: { _id: null, min: { $min: "$price" }, max: { $max: "$price" } } }],
      topTags: [{ $unwind: "$tags" }, { $group: { _id: "$tags", cnt: { $sum: 1 } } }, { $sort: { cnt: -1 } }, { $limit: 5 }]
    }
  }
])

十一、事务与一致性

MongoDB 4.0+(副本集)/ 4.2+(分片)支持多文档事务。

const session = db.getMongo().startSession()
session.startTransaction()
try {
  const users = session.getDatabase("appdb").users
  const orders = session.getDatabase("appdb").orders
  users.updateOne({ _id: userId }, { $inc: { points: -100 } }, { session })
  orders.insertOne({ userId, amount: 100, createdAt: new Date() }, { session })
  session.commitTransaction()
} catch (error) {
  session.abortTransaction()
  throw error
} finally {
  session.endSession()
}

约束与代价:

  • 事务默认 60 秒超时,操作数有上限。
  • 跨分片事务性能明显下降。
  • 事务内不要做远程调用或长时间计算。

MongoDB 的设计哲学是"单文档原子",能用一个文档表达的数据尽量放一个文档,减少事务需求。


十二、副本集与写关注、读关注

副本集:一主多从,主节点接收写,从节点异步复制。

mongod --replSet rs0 --dbpath /data/db1 --port 27017
mongosh --eval 'rs.initiate({ _id: "rs0", members: [{ _id: 0, host: "localhost:27017" }] })'
rs.status()

写关注(writeConcern):

值 含义
w: 1 主节点写入成功即返回(默认)
w: "majority" 多数节点确认,抗主节点宕机
w: 0 不等待确认,可能丢数据
j: true 写入 journal 后才确认

读关注(readConcern):

值 含义
local 默认,读取本地最新数据
majority 读取多数节点确认的数据,避免回滚
snapshot 事务内一致快照

读偏好(readPreference):primary(默认)、primaryPreferred、secondary、nearest。

关键数据(账务、订单)建议 writeConcern: { w: "majority", j: true } + readConcern: "majority",代价是延迟增加。


十三、Schema 设计:嵌入还是引用

方式 写法 适用
嵌入 子文档放在父文档内 一对一、一对少、总是整体读取
引用 存对方 _id 一对多、数据量大、独立更新
// 嵌入:订单与其明细,总是整体读取
{ _id: 1, items: [{ sku: "A", qty: 2 }, { sku: "B", qty: 1 }], total: 100 }

// 引用:文章与评论,评论量大且独立查询
{ _id: 1, title: "..." }
{ _id: 2, postId: 1, text: "..." }

判断标准:

  • 子数据是否总是与父数据一起读?是 → 嵌入。
  • 子数据是否会无限增长?是 → 引用(文档有 16MB 上限)。
  • 子数据是否需要独立查询与更新?是 → 引用。

16MB 文档上限是硬约束,日志、消息、评论这类会不断追加的数据不要嵌在同一个文档里。


十四、数据校验

db.createCollection("users", {
  validator: {
    $jsonSchema: {
      bsonType: "object",
      required: ["email", "name"],
      properties: {
        email: { bsonType: "string", pattern: "^.+@.+$" },
        name: { bsonType: "string", maxLength: 64 },
        points: { bsonType: "int", minimum: 0 }
      },
      additionalProperties: true
    }
  },
  validationLevel: "strict",   // strict | moderate
  validationAction: "error"    // error | warn
})

已有集合可以用 collMod 添加校验:

db.runCommand({
  collMod: "users",
  validator: { $jsonSchema: { bsonType: "object", required: ["email"] } }
})

校验只能保证"写入符合规则",历史数据不会被自动修正。


十五、用户与权限

use appdb
db.createUser({
  user: "app",
  pwd: "secret",
  roles: [
    { role: "readWrite", db: "appdb" },
    { role: "read", db: "reporting" }
  ]
})

db.getUsers()
db.updateUser("app", { roles: [{ role: "read", db: "appdb" }] })
db.dropUser("app")

角色粒度:read、readWrite、dbAdmin、userAdmin、clusterAdmin、root。应用账号不要给 root 或 clusterAdmin。


十六、备份与恢复

# 逻辑备份
mongodump --uri="mongodb://root:secret@localhost:27017/appdb?authSource=admin" --out=/backup/20260113

# 恢复
mongorestore --uri="mongodb://root:secret@localhost:27017/appdb?authSource=admin" /backup/20260113

# 单集合导出为 JSON / CSV
mongoexport --db=appdb --collection=users --out=users.json
mongoimport --db=appdb --collection=users --file=users.json

生产环境备份策略:

  • 副本集可直接用 mongodump(对主节点压力较大时从从节点备份)。
  • 大库用文件系统快照或云厂商的持续备份。
  • 定期验证恢复流程。

十七、性能与排查

// 执行计划
db.users.find({ email: "a@example.com" }).explain("executionStats")

// 关注字段
// executionStats.totalDocsExamined 与 nReturned 的比值:接近 1 说明索引有效
// executionStats.executionTimeMillis
// winningPlan.stage:COLLSCAN 表示全表扫描

慢查询分析:

db.setProfilingLevel(1, { slowms: 100 })
db.system.profile.find().sort({ ts: -1 }).limit(5)
db.setProfilingLevel(0)

常用诊断命令:

db.currentOp({ "secs_running": { $gte: 5 } })
db.serverStatus().connections
db.stats()
db.users.stats()

优化方向:

  • 为查询条件与排序字段建复合索引,注意最左前缀。
  • 避免全表扫描(COLLSCAN)。
  • 只投影需要的字段,减少网络与内存开销。
  • 控制文档大小,避免文档膨胀导致迁移。
  • 大结果集用分页或游标,不要一次 toArray()。

十八、常见错误

错误 原因 处理
Authentication failed 连接串缺少 authSource=admin 补上参数
E11000 duplicate key error 唯一索引冲突 先查询再更新,或用 upsert
BSONObj size ... is invalid 单文档超过 16MB 拆分文档或改引用结构
not authorized on appdb to execute command 权限不足 授予对应 role
MongoServerSelectionError 连不上副本集/网络不通 检查连接串、DNS、防火墙、副本集状态
查询慢、COLLSCAN 缺索引或索引不匹配 explain 后补复合索引
Transaction numbers are only allowed on a replica set member or mongos 单机实例开了事务 使用副本集
Date 显示差 8 小时 存储为 UTC,展示未转换 前端/服务端按本地时区格式化

十九、FAQ

Q1:MongoDB 适合什么场景?

结构不固定或嵌套明显的数据:内容、表单、日志、会话、商品属性。需要强事务与多表关联的核心业务建议用关系库。

Q2:_id 可以用业务字段吗?

可以,但要求全局唯一且不变。用业务键做主键后,一旦业务键变化就要迁移文档;默认 ObjectId 更省心。

Q3:skip 分页为什么慢?

skip(n) 需要扫描并丢弃前 n 条。深分页用"上次最后一条的排序值"作为游标。

Q4:$lookup 性能差怎么办?

确认 foreignField 有索引;能嵌入就不要关联;高频关联场景考虑在写入时冗余必要字段。

Q5:数组字段怎么建索引?

直接对数组字段建普通索引即为多键索引,MongoDB 会自动为每个元素建条目。注意一个复合索引里最多一个数组字段。

Q6:事务能跨集合吗?

可以(同一副本集内)。跨分片事务也支持,但性能代价大,应尽量避免。

Q7:如何保证不重复插入?

用唯一索引 + updateOne(..., { upsert: true }),或插入时捕获 E11000 并转为幂等成功。

Q8:金额用什么类型?

Decimal128(NumberDecimal("19.99"))。用 double 会出现精度误差。

Q9:如何清理过期数据?

TTL 索引:createIndex({ createdAt: 1 }, { expireAfterSeconds: 3600 })。后台任务每分钟扫描一次,不保证精确到秒。

Q10:单机能不能用于生产?

不建议。单机没有冗余,主节点故障即不可用,且无法使用事务。生产至少三节点副本集。


附录:命令速查

// 库与集合
show dbs
use appdb
show collections
db.createCollection("c")
db.c.drop()
db.dropDatabase()

// 增
db.c.insertOne({...})
db.c.insertMany([{...}, {...}])

// 查
db.c.find({ field: value })
db.c.findOne({ _id: ObjectId("...") })
db.c.find({ age: { $gte: 18 } }).sort({ age: -1 }).limit(10)
db.c.countDocuments({})
db.c.distinct("field")

// 改
db.c.updateOne({ _id }, { $set: { a: 1 } })
db.c.updateMany({}, { $inc: { n: 1 } })
db.c.replaceOne({ _id }, {...})
db.c.findOneAndUpdate({ _id }, { $set: {...} }, { returnDocument: "after" })

// 删
db.c.deleteOne({ _id })
db.c.deleteMany({ status: "x" })

// 索引
db.c.createIndex({ a: 1, b: -1 }, { unique: true })
db.c.getIndexes()
db.c.dropIndex("a_1")

// 聚合
db.c.aggregate([{ $match: {} }, { $group: { _id: "$f", n: { $sum: 1 } } }])

// 管理
db.c.explain("executionStats").find({...})
db.setProfilingLevel(1, { slowms: 100 })
db.stats()
db.serverStatus().connections
← 返回帖子 更多数据库内容 →