MongoDB

技术栈
数据库
mongodbnosqldocument-databasebsonjsonopen-source

概览

MongoDB 是全球最流行的NoSQL 文档数据库,由 MongoDB Inc. 于 2009 年发布。它以灵活的 JSON/BSON 文档模型替代传统行列表结构,无需预定义 Schema,特别适合快速迭代的 Web 应用、实时分析、内容管理和微服务架构。支持水平扩展(分片)、高可用副本集、聚合管道和强大的查询语言,是 MEAN/MERN 技术栈的核心组件。

安装

1. 环境准备

  • 操作系统:Linux (Ubuntu 20.04+)、macOS 12+、Windows 10+
  • 硬件要求:最低 512MB RAM(推荐 2GB+),1GB 磁盘空间
  • 端口:默认 27017
  • 依赖:libcurl(Linux)、Apple Silicon Mac 需要 Rosetta(或使用 Docker)

2. 安装命令

Ubuntu/Debian

# 导入公钥
curl -fsSL https://www.mongodb.org/static/pgp/server-7.0.asc | \
  sudo gpg --dearmor -o /usr/share/keyrings/mongodb-server-7.0.gpg

# 添加源
echo "deb [signed-by=/usr/share/keyrings/mongodb-server-7.0.gpg] \
  https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/7.0 multiverse" | \
  sudo tee /etc/apt/sources.list.d/mongodb-org-7.0.list

sudo apt update
sudo apt install mongodb-org -y
sudo systemctl start mongod
sudo systemctl enable mongod

macOS (Homebrew)

brew tap mongodb/brew
brew install mongodb-community@7.0
brew services start mongodb-community@7.0

Windows

choco install mongodb
# 或下载 MSI:https://www.mongodb.com/try/download/community

Docker(推荐开发环境)

docker run --name mongo-dev \
  -e MONGO_INITDB_ROOT_USERNAME=admin \
  -e MONGO_INITDB_ROOT_PASSWORD=admin123 \
  -p 27017:27017 -v ~/mongo-data:/data/db \
  -d mongo:7

验证安装

mongosh
# 或
docker exec -it mongo-dev mongosh -u admin -p admin123

3. 常见安装问题

Q: MongoDB 连接被拒绝

sudo systemctl status mongod
# 检查是否运行。如启动失败查看日志:
sudo journalctl -u mongod --no-pager -n 50

Q: Apple Silicon (M1/M2/M3) 兼容性
推荐使用 Docker:

docker run --platform linux/amd64 -d --name mongo-dev \
  -p 27017:27017 mongo:7

Q: 认证失败
首次安装默认无认证。启用认证需在 /etc/mongod.conf 中:

security:
  authorization: enabled

然后创建管理员用户:

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

示例

目标

通过 mongosh Shell 和 Python/PyMongo 完成 MongoDB 数据库创建、集合操作、文档 CRUD 和聚合管道。

环境准备

mongosh mongodb://localhost:27017
# 或 Docker:
docker exec -it mongo-dev mongosh

第一步:mongosh Shell 方式

// 切换到测试数据库(不存在则自动创建)
use hello_mongo

// 直接插入文档 = 隐式创建集合
db.users.insertMany([
  {
    name: "张三",
    age: 21,
    email: "zhangsan@example.com",
    hobbies: ["编程", "篮球"],
    address: { city: "北京", district: "海淀" }
  },
  {
    name: "李四",
    age: 22,
    email: "lisi@example.com",
    hobbies: ["编程", "摄影", "旅行"],
    address: { city: "上海", district: "浦东" }
  }
])

// 查询
db.users.find({ age: { $gte: 21 } })
db.users.findOne({ "address.city": "北京" })

// 投影(只返回部分字段)
db.users.find({}, { name: 1, email: 1, _id: 0 })

// 更新
db.users.updateOne(
  { name: "张三" },
  { $set: { age: 22 }, $push: { hobbies: "摄影" } }
)

// 条件更新
db.users.updateMany(
  { "address.city": "北京" },
  { $set: { "address.country": "中国" } }
)

// 删除
db.users.deleteOne({ name: "李四" })

第二步:聚合管道

// 统计各城市用户数
db.users.aggregate([
  { $group: { _id: "$address.city", count: { $sum: 1 } } },
  { $sort: { count: -1 } }
])

// 展开数组统计最受欢迎爱好
db.users.aggregate([
  { $unwind: "$hobbies" },
  { $group: { _id: "$hobbies", count: { $sum: 1 } } },
  { $sort: { count: -1 } }
])

第三步:Python PyMongo 方式

pip install pymongo
from pymongo import MongoClient

client = MongoClient("mongodb://localhost:27017")
db = client["hello_mongo"]
col = db["products"]

# 创建索引
col.create_index("name", unique=True)

# 插入
products = [
    {"name": "机械键盘", "price": 299, "tags": ["外设", "办公"], "stock": 50},
    {"name": "无线鼠标", "price": 89, "tags": ["外设", "移动"], "stock": 200},
]
col.insert_many(products)

# 查询
for p in col.find({"price": {"$lt": 300}}).sort("price", -1):
    print(f"{p['name']}: ¥{p['price']} (库存:{p['stock']})")

# 聚合:各标签商品数
pipeline = [
    {"$unwind": "$tags"},
    {"$group": {"_id": "$tags", "count": {"$sum": 1}}}
]
for r in col.aggregate(pipeline):
    print(f"标签[{r['_id']}]: {r['count']}件商品")

# 更新
col.update_one(
    {"name": "机械键盘"},
    {"$set": {"price": 259}, "$inc": {"stock": -1}}
)

client.close()

预期输出

# mongosh find
{
  _id: ObjectId("..."),
  name: '张三',
  age: 22,
  email: 'zhangsan@example.com',
  hobbies: ['编程', '篮球', '摄影']
}

# Python
机械键盘: ¥299 (库存:50)
无线鼠标: ¥89 (库存:200)
标签[外设]: 2件商品
标签[办公]: 1件商品
标签[移动]: 1件商品

教程

1. 什么是 MongoDB?

MongoDB 是一个面向文档(Document-Oriented)的 NoSQL 数据库。不同于 MySQL 的表格行,MongoDB 的数据单元是 BSON 文档(Binary JSON),最接近 JavaScript 对象。

核心概念对照

RDBMS MongoDB
Database Database
Table Collection(集合)
Row Document(文档)
Column Field(字段)
Primary Key _id (自动生成 ObjectId)
JOIN $lookup(聚合管道)

2. 文档模型设计

⚠️ MongoDB 的核心挑战:Embed vs Reference

// 方案 A:内嵌(Embed)- 适合"包含"关系
{
  _id: ObjectId("..."),
  title: "MongoDB 入门",
  author: { name: "张三", email: "z@s.com" },  // 内嵌作者
  tags: ["NoSQL", "数据库"],
  comments: [
    { user: "alice", text: "很棒!", at: ISODate("...") },
    { user: "bob", text: "学习了", at: ISODate("...") }
  ]
}

// 方案 B:引用(Reference)- 适合"多对多"关系
// courses 集合
{ _id: 1, name: "数据库原理", teacher: "王教授" }
// students 集合
{ _id: 1001, name: "小明", course_ids: [1, 2, 3] }

设计原则

  • 一起读的一起存:高频一起查询的用 Embed
  • 独立实体用引用:独立的实体(用户/课程)分开存
  • 数组不要无限增长:内嵌数组有 16MB 文档限制

3. 实战:学生管理系统

use school

// === 1. 集合与索引 ===
db.students.createIndex({ email: 1 }, { unique: true })
db.students.createIndex({ name: 1 })
db.courses.createIndex({ name: 1 }, { unique: true })
db.enrollments.createIndex({ student_id: 1, course_id: 1 }, { unique: true })

// === 2. 插入数据 ===
db.students.insertMany([
  { name: "张三", email: "zs@school.edu", age: 20, gender: "M" },
  { name: "李四", email: "ls@school.edu", age: 21, gender: "M" },
  { name: "王五", email: "ww@school.edu", age: 19, gender: "F" },
])

db.courses.insertMany([
  { name: "数据库原理", credits: 3, teacher: "王教授", capacity: 60 },
  { name: "数据结构", credits: 4, teacher: "李教授", capacity: 50 },
  { name: "操作系统", credits: 3, teacher: "张教授", capacity: 55 },
])

// === 3. 选课(事务 - MongoDB 4.0+) ===
const session = db.getMongo().startSession()
session.startTransaction()
try {
  const enrollments = session.getDatabase("school").enrollments

  enrollments.insertMany([
    { student_id: ObjectId("..."), course_id: ObjectId("..."), enrolled_at: new Date() },
    { student_id: ObjectId("..."), course_id: ObjectId("..."), enrolled_at: new Date() },
  ])

  // 更新课程已选人数
  db.courses.updateOne({ _id: ObjectId("...") }, { $inc: { enrolled: 2 } })

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

// === 4. 聚合:学生成绩单 ===
// 先添加成绩嵌入到 enrollments
db.enrollments.updateMany({}, { $set: { score: Math.floor(Math.random() * 40) + 60 } })

db.enrollments.aggregate([
  {
    $lookup: {
      from: "students",
      localField: "student_id",
      foreignField: "_id",
      as: "student"
    }
  },
  { $unwind: "$student" },
  {
    $lookup: {
      from: "courses",
      localField: "course_id",
      foreignField: "_id",
      as: "course"
    }
  },
  { $unwind: "$course" },
  {
    $group: {
      _id: "$student._id",
      name: { $first: "$student.name" },
      avg_score: { $avg: "$score" },
      courses: { $push: { name: "$course.name", score: "$score" } }
    }
  },
  { $sort: { avg_score: -1 } }
])

4. 索引策略

// 单字段索引
db.students.createIndex({ age: 1 })

// 复合索引
db.enrollments.createIndex({ student_id: 1, course_id: 1 })

// 文本索引(全文搜索)
db.courses.createIndex({ name: "text", teacher: "text" })
db.courses.find({ $text: { $search: "数据库" } })

// 分析查询性能
db.students.find({ age: { $gte: 20 } }).explain("executionStats")

5. MongoDB Compass

除了命令行,强烈推荐安装 MongoDB Compass(GUI 工具):

思考题

  1. 什么情况下 Embed 会比 Reference 性能更差?(提示:文档大小 vs 读写模式)
  2. $lookup 的局限性是什么?为什么 MongoDB 不建议大量使用它?
  3. 学生选课系统如果用纯 Embed 方式设计,会遇到什么问题?