MongoDB 教程

MongoDB 是最流行的文档型 NoSQL 数据库。数据以 BSON(Binary JSON)格式存储,没有固定表结构(schema-free),同一集合中的文档可以拥有不同的字段。适合快速迭代、结构多变的业务场景。


一、定位与概念映射

从关系型数据库迁移过来,最大的门槛是术语体系。一张表对照:

关系型数据库MongoDB说明
databasedatabase数据库,概念一致
table(表)collection(集合)无固定结构的”表”
row(行)document(文档)一条 BSON 记录
column(列)field(字段)文档内的键
primary key_id自动生成的唯一主键(默认 ObjectId)
JOIN$lookup(聚合)不鼓励跨集合关联,倾向内嵌
索引index概念一致

核心思想差异:关系库追求范式化拆表,MongoDB 鼓励把相关数据内嵌进一个文档,用一次读取换掉多次 JOIN。


二、安装与 mongosh 连接

# Docker 方式(推荐)
docker run -d --name mongo -p 27017:27017 \
    -e MONGO_INITDB_ROOT_USERNAME=admin \
    -e MONGO_INITDB_ROOT_PASSWORD=mongo123 \
    mongo:7

Linux 也可通过 apt 官方仓库安装 mongodb-org 包后 systemctl enable --now mongod 启动。

mongosh 是官方新版 Shell(替代旧版 mongo):

// 本机连接
mongosh
 
// 带认证连接
mongosh "mongodb://admin:mongo123@localhost:27017"
 
// 进入后常用命令
show dbs              // 列出数据库
use shop              // 切换/创建数据库(插入数据后才真正落盘)
show collections      // 列出集合
db.stats()            // 当前库状态

注意:use 创建的库在写入第一条文档前是看不见的,这是惰性创建。


三、CRUD 全套

以下示例假设已 use shop,操作 users 集合。

3.1 插入:insertOne / insertMany

// 单条插入
db.users.insertOne({
    name: "张三",
    age: 25,
    tags: ["vip", "beta"],
    address: { city: "北京", zip: "100000" }
})
 
// 批量插入(返回插入的 _id 数组)
db.users.insertMany([
    { name: "李四", age: 30, tags: ["vip"] },
    { name: "王五", age: 22, tags: [] },
    { name: "赵六", age: 35, tags: ["svip"], balance: 500 }
])

3.2 查询:find 条件与投影

// 全部文档
db.users.find()
 
// 格式化输出
db.users.find().pretty()
 
// 条件查询
db.users.find({ name: "张三" })
 
// 多条件(隐式 AND)
db.users.find({ name: "张三", age: 25 })
 
// 投影:只显示 name 和 age(_id 默认总显示)
db.users.find({}, { name: 1, age: 1 })
 
// 排除某些字段
db.users.find({}, { address: 0, tags: 0 })
 
// 只取一条
db.users.findOne({ name: "张三" })
 
// 计数 / 排序 / 截断
db.users.find({ age: { $gt: 20 } }).count()
db.users.find().sort({ age: -1 }).limit(5).skip(10)

3.3 更新:updateOne / updateMany

更新必须使用更新操作符,直接传文档会整体替换(危险):

// 更新第一条匹配文档
db.users.updateOne(
    { name: "张三" },          // 过滤条件
    { $set: { age: 26 } }      // 更新操作
)
 
// 更新所有匹配文档
db.users.updateMany(
    { age: { $lt: 18 } },
    { $set: { status: "minor" } }
)
 
// upsert:不存在则插入
db.users.updateOne({ name: "新用户" }, { $set: { age: 40 } }, { upsert: true })

3.4 删除:deleteOne / deleteMany

db.users.deleteOne({ name: "赵六" })
 
db.users.deleteMany({ age: { $gt: 60 } })
 
db.users.deleteMany({})   // 清空集合(保留索引定义)

四、查询操作符大全

操作符含义示例
$eq / $ne等于 / 不等于{ age: { $ne: 25 } }
$gt / $gte大于 / 大于等于{ age: { $gt: 18 } }
$lt / $lte小于 / 小于等于{ price: { $lte: 100 } }
$in在数组列表内{ tag: { $in: ["a", "b"] } }
$nin不在数组列表内{ status: { $nin: ["off"] } }
$or{ $or: [{ age: { $lt: 18 } }, { vip: true }] }
$and与(显式写法){ $and: [{ age: { $gt: 18 } }, { age: { $lt: 60 } }] }
$not取反{ age: { $not: { $gt: 30 } } }
$exists字段是否存在{ balance: { $exists: true } }
$regex正则匹配{ name: { $regex: "^张", $options: "i" } }
$elemMatch数组元素同时满足多条件{ scores: { $elemMatch: { $gt: 80, $lt: 90 } } }
$type按类型过滤{ age: { $type: "int" } }

组合示例:

// 年龄在 18-60 之间 且 (北京 或 上海)
db.users.find({
    age: { $gte: 18, $lte: 60 },
    $or: [
        { "address.city": "北京" },
        { "address.city": "上海" }
    ]
})

五、嵌套文档与数组查询

这是 MongoDB 区别于 SQL 的重点能力:

// 示例数据
db.students.insertMany([
    { name: "小明", address: { province: "浙江", city: "杭州" }, scores: [85, 92, 78],
      courses: [{ title: "C语言", grade: "A" }, { title: "数据结构", grade: "B" }] },
    { name: "小红", address: { province: "江苏", city: "南京" }, scores: [95, 88],
      courses: [{ title: "C++", grade: "A" }, { title: "算法", grade: "S" }] }
])
// 点号访问嵌套字段(注意加引号)
db.students.find({ "address.city": "杭州" })
 
// 整个嵌套对象精确匹配(顺序敏感,很少用)
db.students.find({ address: { province: "浙江", city: "杭州" } })
 
// 数组:包含某元素即命中
db.students.find({ scores: 85 })
 
// 数组按索引取值
db.students.find({ "scores.0": { $gte: 90 } })
 
// 数组大小
db.students.find({ scores: { $size: 3 } })
 
// 数组元素同时满足多个条件
db.students.find({ scores: { $elemMatch: { $gt: 80, $lt: 90 } } })
 
// 对象数组中按内部字段查
db.students.find({ "courses.title": "算法" })
 
// $slice 投影:只返回数组前 n 个元素
db.students.find({}, { scores: { $slice: 2 } })

六、聚合管道 pipeline

聚合是 MongoDB 的数据分析核心,数据像流水线一样依次经过各个阶段(stage),每个阶段的输出是下一个阶段的输入——和 Unix 管道 | 的思想完全一致。

6.1 各阶段速览

阶段作用SQL 类比
$match过滤文档WHERE
$group分组统计GROUP BY
$sort排序ORDER BY
$project字段筛选与计算列SELECT 列表
$limit / $skip截断 / 跳过LIMIT / OFFSET
$unwind把数组拆成多条文档行转列展开

6.2 完整示例

示例数据(订单集合):

db.orders.insertMany([
    { user: "张三", amount: 200, status: "paid",   items: ["键盘", "鼠标"] },
    { user: "张三", amount: 350, status: "paid",   items: ["显示器"] },
    { user: "李四", amount: 120, status: "pending", items: ["鼠标垫"] },
    { user: "李四", amount: 800, status: "paid",   items: ["主机", "电源", "风扇"] },
    { user: "王五", amount: 50,  status: "cancelled", items: ["数据线"] }
])

需求:统计已支付订单中每个用户的总金额与订单数,只看总额超过 400 的用户,按总额倒序取前两名:

db.orders.aggregate([
    // 1. 过滤:等价 WHERE status = 'paid'
    { $match: { status: "paid" } },
 
    // 2. 分组:等价 SELECT user, SUM(amount), COUNT(*) ... GROUP BY user
    { $group: {
        _id: "$user",
        total:  { $sum: "$amount" },
        orders: { $sum: 1 },
        avgAmt: { $avg: "$amount" }
    }},
 
    // 3. HAVING:过滤分组结果
    { $match: { total: { $gt: 400 } } },
 
    // 4. 排序
    { $sort: { total: -1 } },
 
    // 5. 取前 2 名
    { $limit: 2 },
 
    // 6. 整理输出格式(_id 改名为 user)
    { $project: {
        _id: 0,
        user: "$_id",
        total: 1,
        orders: 1,
        avgAmt: { $round: ["$avgAmt", 2] }
    }}
])

6.3 $unwind 展开

// 每个 item 拆成一条文档
db.orders.aggregate([
    { $match: { status: "paid" } },
    { $unwind: "$items" },          // 5 条订单 → 6 条商品记录
    { $group: {
        _id: "$items",
        soldCount: { $sum: 1 }
    }},
    { $sort: { soldCount: -1 } }
])

七、索引

没有索引时每个查询都是全集合扫描(类比 O(n) 遍历链表),索引把查找降到近似 O(log n)(底层为 B-Tree)。

// 单字段索引
db.users.createIndex({ age: 1 })            // 1 升序,-1 降序
 
// 复合索引(遵循最左前缀原则,同 MySQL)
db.users.createIndex({ age: 1, name: 1 })
 
// 唯一索引(重复插入报错)
db.users.createIndex({ email: 1 }, { unique: true })
 
// 稀疏索引:只为存在该字段的文档建索引
db.users.createIndex({ phone: 1 }, { sparse: true })
 
// TTL 索引:expireAfterSeconds 秒后自动删除文档(会话/日志场景神器)
db.sessions.createIndex({ createdAt: 1 }, { expireAfterSeconds: 3600 })
 
// 查看集合上的所有索引
db.users.getIndexes()
 
// 删除索引
db.users.dropIndex("age_1")

7.1 explain 分析查询计划

db.users.find({ age: { $gt: 20 } }).explain("executionStats")

关注输出中的关键字段:

字段含义
winningPlan.stageCOLLSCAN=全表扫(坏);IXSCAN=走索引(好)
totalDocsExamined实际扫描的文档数
nReturned返回的文档数
executionTimeMillis耗时毫秒

nReturned ≈ totalDocsExamined 说明索引效率高;两者差距悬殊则需要优化。


八、副本集与分片

特性一句话说明
副本集(Replica Set)一主多从自动故障转移:主节点写入,从节点异步复制,主挂了从节点自动选举上位
分片(Sharding)水平拆分:按分片键把海量数据分散到多台机器,解决单机容量与写入瓶颈

生产环境标配是”分片集群 + 每个分片内部是副本集”,学习阶段单机即可。


九、适用场景

场景是否适合原因
商品目录 / CMS 内容适合字段不固定,嵌套结构一次读全
用户画像 / 标签系统适合数组字段天然支持标签
日志 / 埋点分析适合写入吞吐高 + 聚合管道分析 + TTL 自动过期
移动端 Feed 流适合文档模型贴合 JSON API
强事务的多表关联业务(如账务核心)不适合跨文档事务支持弱且代价高,选关系库
需要大量复杂 JOIN 的报表不适合$lookup 性能远不如关系库 JOIN

十、速查卡

分类关键命令
库与集合show dbs / use db / show collections
插入insertOne / insertMany
查询find(filter, projection) / findOne
更新updateOne / updateMany + $set
删除deleteOne / deleteMany
比较$gt $gte $lt $lte $in $nin
逻辑$and $or $not
结构$exists $type $elemMatch
正则$regex
聚合$match$group$sort$project
展开数组$unwind
索引createIndex / getIndexes / dropIndex
分析.explain("executionStats"),拒绝 COLLSCAN
高可用副本集(复制+选主)、分片(水平扩展)

返回 数据库目录