Skip to content

MongoDB 数据库

MongoDB 核心概念、数据模型、聚合查询与最佳实践

MongoDB 概述

特点

  • 文档数据库(BSON 格式)
  • 模式灵活(Schema-less)
  • 支持索引
  • 分布式设计
  • 丰富的查询语言

应用场景

  • 内容管理系统
  • 用户数据存储
  • 日志存储
  • 实时分析
  • 移动应用后端

数据模型

文档结构

javascript
// 灵活的文档结构
{
  _id: ObjectId("..."),
  name: "Alice",
  age: 25,
  email: "alice@example.com",
  tags: ["developer", "frontend"],
  address: {
    street: "123 Main St",
    city: "Beijing",
    country: "China"
  },
  createdAt: new Date(),
  updatedAt: new Date()
}

// 数组内嵌文档
{
  orders: [
    { orderId: 1, amount: 100, status: "paid" },
    { orderId: 2, amount: 200, status: "shipped" }
  ]
}

模式设计原则

javascript
// 1. 内嵌(Embed)- 一对一关系
// 适合经常一起查询的数据
{
  user: {
    name: "Alice",
    profile: {
      bio: "Developer",
      avatar: "url",
      website: "https://alice.dev"
    }
  }
}

// 2. 引用(Reference)- 一对多/多对多
// 适合数据量大的场景

// users 集合
{
  _id: ObjectId("..."),
  name: "Alice",
  email: "alice@example.com"
}

// posts 集合(引用 user)
{
  _id: ObjectId("..."),
  userId: ObjectId("user_id"),
  title: "My Post",
  content: "..."
}

// 3. 子集模式(Subset)- 只存储常用字段
{
  _id: ObjectId("..."),
  userId: ObjectId("user_id"),
  title: "Post title",
  // 只存储前10条评论的摘要
  recentComments: [
    { text: "Great!", author: "Bob" }
  ]
}

CRUD 操作

插入

javascript
// 单文档插入
await db.collection('users').insertOne({
  name: 'Alice',
  age: 25,
  createdAt: new Date()
})

// 多文档插入
await db.collection('users').insertMany([
  { name: 'Bob', age: 30 },
  { name: 'Charlie', age: 35 }
])

// 插入或更新(Upsert)
await db.collection('users').updateOne(
  { email: 'alice@example.com' },
  {
    $set: { name: 'Alice Updated' },
    $setOnInsert: { createdAt: new Date() }
  },
  { upsert: true }
)

查询

javascript
// 基本查询
await db.collection('users').findOne({ name: 'Alice' })
await db.collection('users').find({ age: { $gte: 25 } }).toArray()

// 比较操作符
{
  age: { $gt: 25 },           // 大于
  age: { $gte: 25 },          // 大于等于
  age: { $lt: 30 },           // 小于
  age: { $lte: 30 },          // 小于等于
  age: { $ne: 25 },           // 不等于
  age: { $in: [25, 30, 35] }, // 在数组中
  age: { $nin: [20, 40] }     // 不在数组中
}

// 逻辑操作符
{
  $and: [{ age: { $gte: 25 } }, { age: { $lte: 35 } }],
  $or: [{ age: { $lt: 25 } }, { age: { $gt: 35 } }],
  $not: { age: { $gt: 30 } }
}

// 元素操作符
{
  name: { $exists: true },
  age: { $type: 'number' }
}

// 数组操作符
{
  tags: { $in: ['developer', 'frontend'] },
  tags: { $all: ['developer', 'frontend'] },  // 包含所有
  scores: { $elemMatch: { $gte: 90 } }        // 至少一个元素满足
}

// 投影(只返回指定字段)
await db.collection('users').find(
  { age: { $gte: 25 } },
  { projection: { name: 1, age: 1, _id: 0 } }  // 1-返回,0-不返回
)

// 分页
const page = 1
const pageSize = 10
await db.collection('users')
  .find({})
  .skip((page - 1) * pageSize)
  .limit(pageSize)
  .toArray()

// 排序
await db.collection('users')
  .find({})
  .sort({ age: 1, name: -1 })  // 1-升序,-1-降序
  .toArray()

// 统计
const count = await db.collection('users').countDocuments({ age: { $gte: 25 } })

更新

javascript
// 更新单个
await db.collection('users').updateOne(
  { name: 'Alice' },
  {
    $set: { age: 26 },           // 设置字段
    $inc: { views: 1 },          // 递增
    $push: { tags: 'senior' },   // 数组添加
    $pull: { tags: 'junior' },   // 数组删除
    $addToSet: { tags: 'expert' } // 数组去重添加
  }
)

// 更新多个
await db.collection('users').updateMany(
  { age: { $lt: 30 } },
  { $set: { status: 'young' } }
)

// 数组更新
await db.collection('users').updateOne(
  { name: 'Alice', 'orders.orderId': 1 },
  {
    $set: { 'orders.$.status': 'delivered' }  // $ 定位符
  }
)

// 更新嵌套字段
await db.collection('users').updateOne(
  { name: 'Alice' },
  { $set: { 'address.city': 'Shanghai' } }
)

// 更新数组内所有元素
await db.collection('users').updateOne(
  { name: 'Alice' },
  { $inc: { 'scores.$[]': 1 } }  // $[] 更新所有元素
)

// 条件数组更新
await db.collection('users').updateOne(
  { name: 'Alice', 'scores': { $gte: 90 } },
  { $set: { 'scores.$[elem]': 100 } },
  { arrayFilters: [{ elem: { $gte: 90 } }] }
)

删除

javascript
// 删除单个
await db.collection('users').deleteOne({ name: 'Alice' })

// 删除多个
await db.collection('users').deleteMany({ status: 'inactive' })

// 删除集合
await db.collection('users').drop()

聚合框架

管道阶段

javascript
// 聚合管道
const pipeline = [
  // 1. 过滤
  { $match: { status: 'active' } },

  // 2. 展开数组
  { $unwind: '$orders' },

  // 3. 添加计算字段
  {
    $addFields: {
      orderYear: { $year: '$orders.date' }
    }
  },

  // 4. 分组
  {
    $group: {
      _id: '$orderYear',
      totalRevenue: { $sum: '$orders.amount' },
      orderCount: { $sum: 1 },
      avgOrderValue: { $avg: '$orders.amount' }
    }
  },

  // 5. 排序
  { $sort: { totalRevenue: -1 } },

  // 6. 限制
  { $limit: 10 },

  // 7. 投影
  {
    $project: {
      _id: 0,
      year: '$_id',
      revenue: '$totalRevenue',
      orders: '$orderCount'
    }
  }
]

const results = await db.collection('users').aggregate(pipeline).toArray()

聚合操作符

javascript
// 字符串操作
{
  $project: {
    fullName: { $concat: ['$firstName', ' ', '$lastName'] },
    emailDomain: { $arrayElemAt: [{ $split: ['$email', '@'] }, 1] }
  }
}

// 条件判断
{
  $project: {
    status: {
      $switch: {
        branches: [
          { case: { $gte: ['$score', 90] }, then: 'A' },
          { case: { $gte: ['$score', 80] }, then: 'B' }
        ],
        default: 'C'
      }
    }
  }
}

// 日期操作
{
  $project: {
    year: { $year: '$createdAt' },
    month: { $month: '$createdAt' },
    day: { $dayOfMonth: '$createdAt' },
    dayOfWeek: { $dayOfWeek: '$createdAt' }
  }
}

// 数组操作
{
  $project: {
    firstTag: { $arrayElemAt: ['$tags', 0] },
    tagCount: { $size: '$tags' },
    hasFrontend: { $in: ['frontend', '$tags'] }
  }
}

索引

javascript
// 单字段索引
await db.collection('users').createIndex({ email: 1 })

// 复合索引
await db.collection('users').createIndex({ status: 1, createdAt: -1 })

// 唯一索引
await db.collection('users').createIndex({ email: 1 }, { unique: true })

// 多键索引(数组字段)
await db.collection('products').createIndex({ tags: 1 })

// 文本索引
await db.collection('articles').createIndex({ title: 'text', content: 'text' })

// 部分索引
await db.collection('users').createIndex(
  { email: 1 },
  { partialFilterExpression: { email: { $exists: true } } }
)

// 过期索引(TTL)
await db.collection('sessions').createIndex(
  { createdAt: 1 },
  { expireAfterSeconds: 3600 }
)

// 查看索引
await db.collection('users').getIndexes()

// 删除索引
await db.collection('users').dropIndex('email_1')

事务

javascript
// 4.0+ 单集合事务
const session = db.startSession()
session.startTransaction()

try {
  await db.collection('accounts').updateOne(
    { accountId: 'A' },
    { $inc: { balance: -100 } },
    { session }
  )

  await db.collection('accounts').updateOne(
    { accountId: 'B' },
    { $inc: { balance: 100 } },
    { session }
  )

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

Node.js 集成

mongoose

javascript
import mongoose from 'mongoose'

// 连接
await mongoose.connect('mongodb://localhost:27017/myapp', {
  maxPoolSize: 10,
  serverSelectionTimeoutMS: 5000
})

// 定义 Schema
const userSchema = new mongoose.Schema({
  name: {
    type: String,
    required: true,
    trim: true,
    minlength: 2,
    maxlength: 50
  },
  email: {
    type: String,
    required: true,
    unique: true,
    lowercase: true
  },
  age: {
    type: Number,
    min: 0,
    max: 150
  },
  tags: [String],
  status: {
    type: String,
    enum: ['active', 'inactive', 'pending'],
    default: 'pending'
  },
  createdAt: {
    type: Date,
    default: Date.now
  }
}, {
  timestamps: true  // 自动管理 createdAt 和 updatedAt
})

// 索引
userSchema.index({ email: 1 })
userSchema.index({ status: 1, createdAt: -1 })

// 虚拟字段
userSchema.virtual('fullInfo').get(function() {
  return `${this.name} (${this.email})`
})

// 实例方法
userSchema.methods.greet = function() {
  return `Hello, I'm ${this.name}`
}

// 静态方法
userSchema.statics.findByEmail = function(email) {
  return this.findOne({ email })
}

// 钩子
userSchema.pre('save', function(next) {
  if (this.isModified('email')) {
    this.email = this.email.toLowerCase()
  }
  next()
})

// 模型
export const User = mongoose.model('User', userSchema)

// 使用
const user = new User({ name: 'Alice', email: 'ALICE@EXAMPLE.COM' })
await user.save()

const users = await User.find({ status: 'active' })
  .select('name email')
  .sort({ createdAt: -1 })
  .limit(10)

const user = await User.findByEmail('alice@example.com')

原生驱动

javascript
import { MongoClient } from 'mongodb'

const client = new MongoClient('mongodb://localhost:27017', {
  maxPoolSize: 10,
  minPoolSize: 2
})

await client.connect()
const db = client.db('myapp')

// 集合操作
const users = db.collection('users')

// CRUD
await users.insertOne({ name: 'Alice', age: 25 })
await users.find({ age: { $gte: 25 } }).toArray()
await users.updateOne({ name: 'Alice' }, { $set: { age: 26 } })
await users.deleteOne({ name: 'Alice' })

// 事务
const session = client.startSession()
await session.withTransaction(async () => {
  await users.updateOne({ _id: id1 }, { $inc: { balance: -100 } }, { session })
  await users.updateOne({ _id: id2 }, { $inc: { balance: 100 } }, { session })
})

最佳实践

javascript
// 1. 连接池配置
const client = new MongoClient(uri, {
  maxPoolSize: 100,
  minPoolSize: 10,
  maxIdleTimeMS: 60000,
  waitQueueTimeoutMS: 30000
})

// 2. 批量操作
const bulkOps = users.map(user => ({
  updateOne: {
    filter: { _id: user._id },
    update: { $set: user }
  }
}))
await users.bulkWrite(bulkOps, { ordered: false })

// 3. 游标处理大结果集
const cursor = users.find({})
for await (const doc of cursor) {
  // 处理每个文档
}

// 4. 投影只取需要的字段
await users.find({}, {
  projection: { name: 1, email: 1 },
  limit: 100
})

// 5. 使用 explain 分析查询
const explain = await users.find({ status: 'active' }).explain('executionStats')

// 6. 软删除
userSchema.add({
  deletedAt: Date,
  deleted: { type: Boolean, default: false }
})

userSchema.pre('find', function() {
  this.where({ deleted: { $ne: true } })
})

// 7. 分片键选择
// 原则:分散写入、查询局部性、避免热点
sh.shardCollection('myapp.orders', { userId: 1, orderDate: 1 })

面试高频题

Q1: MongoDB 和 MySQL 的区别?

特性MongoDBMySQL
类型文档数据库关系数据库
模式灵活(Schema-less)固定
查询语言MongoDB 查询语法SQL
事务4.0+ 支持多文档原生支持
扩展水平扩展(分片)垂直扩展/读写分离
关联引用/聚合JOIN

Q2: MongoDB 索引类型有哪些?

答案:

  • 单字段索引
  • 复合索引
  • 多键索引(数组字段)
  • 文本索引
  • 哈希索引
  • 地理空间索引
  • 部分索引
  • TTL 索引

Q3: 如何设计 MongoDB 的数据模型?

答案:

  • 内嵌:一对一、经常联合查询的数据
  • 引用:一对多、数据量大、需要独立更新的数据
  • 子集:大集合中只存储常用字段

Q4: 聚合管道和 MapReduce 的区别?

特性聚合管道MapReduce
性能更优较低
灵活性更高
实时性实时批量处理
使用场景复杂查询超大规模数据

Q5: MongoDB 如何实现分片?

答案:

  1. 配置分片(Config Servers)
  2. 路由分片(Mongos)
  3. 数据分片(Shard Servers)
  4. 选择分片键(Shard Key)
  5. 数据自动均衡

前端面试知识库