1. NoSQL 数据库概述
1.1 为什么需要 NoSQL
传统关系型数据库(如 MySQL、PostgreSQL、Oracle)在过去数十年间一直是数据存储的基石。它们以严格的结构化表(Table)、预定义 Schema、ACID 事务和强大的 SQL 查询著称。然而,随着互联网规模的爆炸式增长,关系型数据库逐渐暴露出以下局限:
| 局限 | 说明 |
|---|---|
| Schema 僵化 | 每次新增字段都需要执行 ALTER TABLE,在大表上可能锁表数小时 |
| 扩展困难 | 垂直扩展(升级硬件)有物理上限,水平扩展(分库分表)需要中间件且复杂度高 |
| 大数据性能瓶颈 | 多表 JOIN 在海量数据下性能急剧下降 |
| 非结构化数据支持差 | JSON、大文本、地理位置等类型处理不自然 |
| 开发效率低 | ORM 映射增加心智负担,模式变更需要 DBA 介入 |
例如,一个社交平台的海量用户动态、行为日志、实时计数器等场景,用关系型数据库处理会非常痛苦。这正是 NoSQL(Not Only SQL)数据库崛起的背景。
1.2 四类 NoSQL 数据库
NoSQL 并非单一技术,而是涵盖四大类数据模型:
| 类别 | 代表产品 | 数据模型 | 典型场景 |
|---|---|---|---|
| 文档型 | MongoDB, Couchbase | JSON/BSON 文档 | 内容管理、日志、用户资料、电商商品 |
| 键值型 | Redis, DynamoDB, etcd | 键值对 | 缓存、会话管理、配置中心 |
| 列族型 | Cassandra, HBase | 宽列存储 | 时序数据、IoT、推荐引擎、消息队列 |
| 图型 | Neo4j, Neptune | 节点+边 | 社交关系、推荐系统、知识图谱、欺诈检测 |
1.3 CAP 定理与 BASE
理解 NoSQL 必须先理解两个核心理论。
CAP 定理(Brewer's Theorem):分布式系统只能同时满足以下三者中的两个:
- C(一致性):所有节点在同一时刻看到相同数据
- A(可用性):每个请求都能获得非错误的响应
- P(分区容错性):系统在网络分区时仍能继续工作
由于网络分区不可避免,实际上是在 CP 与 AP 之间做选择:
CP 系统(如 HBase、etcd)—— 牺牲可用性保证一致性
AP 系统(如 Cassandra、DynamoDB)—— 牺牲一致性保证可用性
CA 系统(单机 MySQL)—— 实际上无法在网络分区时工作MongoDB 在副本集模式下默认为 CP(主节点写入,从节点异步复制),但可配置写关注(write concern)和读关注(read concern)来调节一致性级别。
BASE 理论:NoSQL 对 ACID 的替代方案
- BA(Basically Available):基本可用
- S(Soft State):允许中间状态
- E(Eventually Consistent):最终一致
"If you have ACID, you sleep well at night. If you have BASE, you ship on Friday."
— NoSQL 领域的笑谈
1.4 MongoDB 在 NoSQL 生态中的位置
MongoDB 是目前最流行的文档型数据库,由 MongoDB Inc.(原 10gen)于 2009 年开源。它在 NoSQL 生态中定位如下:
- 数据模型:BSON(Binary JSON)文档,天然支持嵌套对象和数组
- 查询能力:丰富的查询语言 + 聚合管道(Aggregation Pipeline),媲美 SQL 的灵活度
- 一致性:支持可调一致性(不同读写级别)
- 扩展性:原生分片(Sharding),自动均衡数据
- 生态:MongoDB Atlas 云服务、Mongoose/Spring Data/Mongoid 等驱动
MongoDB 的竞争对手包括 Firestore(Firebase)、Couchbase、Amazon DocumentDB,但 MongoDB 在独立部署和自运维场景中仍居主导地位。
2. MongoDB 基础
2.1 Docker 安装
最快捷的本地实践方式(本文章所有示例均在以下环境验证):
# 拉取 MongoDB 8.0 镜像
docker pull mongo:8.0
# 启动单节点 MongoDB
docker run -d \
--name mongo-local \
-p 27017:27017 \
-v mongo-data:/data/db \
-e MONGO_INITDB_ROOT_USERNAME=admin \
-e MONGO_INITDB_ROOT_PASSWORD=admin123 \
mongo:8.0
# 进入 mongosh 交互 Shell
docker exec -it mongo-local mongosh -u admin -p admin123
# 或者使用连接字符串直连
# mongodb://admin:admin123@localhost:270172.2 核心概念
MongoDB 的数据结构非常直观:
┌────────────────────────────────────────┐
│ Database (数据库) │
│ ┌──────────────────────────────────┐ │
│ │ Collection (集合) │ │
│ │ ┌─────────────────────────────┐ │ │
│ │ │ Document (文档) — JSON 对象 │ │ │
│ │ │ { │ │ │
│ │ │ _id: ObjectId(...), │ │ │
│ │ │ name: "Alice", │ │ │
│ │ │ age: 30, │ │ │
│ │ │ tags: ["dev", "ops"] │ │ │
│ │ │ } │ │ │
│ │ └─────────────────────────────┘ │ │
│ └──────────────────────────────────┘ │
└────────────────────────────────────────┘2.3 与关系型数据库概念对比
| RDBMS(MySQL) | MongoDB | 说明 |
|---|---|---|
| Database | Database | 相同概念 |
| Table | Collection | 集合不强制固定 Schema |
| Row | Document | BSON 文档,可嵌套 |
| Column | Field | 字段可以在不同文档中不同 |
| Primary Key | _id |
自动创建,默认 ObjectId |
| Index | Index | 基本相同 |
| JOIN | $lookup / 嵌入式文档 |
推荐嵌入式避免 JOIN |
| Transactions | 支持(4.0+) | 多文档事务 |
关键差异:
// RDBMS:每张表需要 CREATE TABLE 定义 Schema
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100),
age INT
);
// MongoDB:直接插入文档,无需预定义 Schema
db.users.insertOne({
name: "Alice",
age: 30,
address: { city: "北京", district: "海淀" }, // 嵌套对象
tags: ["开发者", "MongoDB"] // 数组
})
// 第二个文档可以和第一个字段不同!
db.users.insertOne({
name: "Bob",
phone: "13800138000", // 新增字段
meta: { lastLogin: ISODate() }
})2.4 mongosh 基本使用
// 查看所有数据库
show dbs
// 或
db.adminCommand('listDatabases')
// 切换/创建数据库(use 后自动创建)
use myblog
// 查看所有集合
show collections
// 查看数据库信息
db.stats()
// 查看集合信息
db.articles.stats()
// 查看集合中所有文档
db.articles.find().pretty()
// 查看帮助
help
db.help()
db.articles.help()
// 列出所有可用的方法
db.articles.getDB()2.5 BSON 数据类型
MongoDB 使用 BSON(Binary JSON)存储文档,比 JSON 支持更多数据类型:
| BSON 类型 | 数字编号 | 示例 | 说明 |
|---|---|---|---|
| Double | 1 | 3.14159 |
双精度浮点数 |
| String | 2 | "你好" |
UTF-8 字符串 |
| Object | 3 | { a: 1 } |
内嵌文档 |
| Array | 4 | [1,2,3] |
数组 |
| Binary | 5 | BinData(0, ...) |
二进制数据 |
| ObjectId | 7 | ObjectId("...") |
12 字节唯一 ID |
| Boolean | 8 | true/false |
布尔值 |
| Date | 9 | ISODate("2024-09-15") |
64 位整数毫秒时间戳 |
| Null | 10 | null |
空值 |
| Int32 | 16 | NumberInt(42) |
32 位整数 |
| Timestamp | 17 | Timestamp(1726,...) |
64 位时间戳(内部使用) |
| Int64 | 18 | NumberLong(999) |
64 位整数 |
| Decimal128 | 19 | NumberDecimal("10.99") |
高精度十进制(财务场景) |
// 在 mongosh 中测试 BSON 类型
db.types.insertOne({
stringField: "文本",
intField: NumberInt(42),
longField: NumberLong(9999999999),
decimalField: NumberDecimal("99.99"),
doubleField: 3.14159,
dateField: new Date(),
isActive: true,
nested: { key: "value" },
arr: [1, "混合", true, { a: 1 }],
nullField: null,
objectIdField: new ObjectId(),
binaryField: BinData(0, "AAAA"),
timestampField: Timestamp(1726358400, 1)
})3. CRUD 操作
3.1 Insert
insertOne
use shop
db.products.insertOne({
name: "无线蓝牙耳机",
category: "电子产品",
price: NumberDecimal("299.00"),
stock: 150,
specs: {
brand: "SoundPro",
color: "黑色",
battery: "8小时"
},
tags: ["蓝牙", "无线", "降噪"],
createdAt: new Date()
})返回值包含 acknowledged: true 和 insertedId:
{
acknowledged: true,
insertedId: ObjectId("66e5a1b2c3d4e5f6a7b8c9d0")
}insertMany
db.products.insertMany([
{
name: "机械键盘",
category: "电子产品",
price: NumberDecimal("599.00"),
stock: 80,
specs: { brand: "KeyCraft", layout: "87键", switch: "青轴" },
tags: ["键盘", "机械", "办公"]
},
{
name: "瑜伽垫",
category: "运动用品",
price: NumberDecimal("129.00"),
stock: 300,
specs: { brand: "FitLife", thickness: "6mm", material: "TPE" },
tags: ["瑜伽", "健身", "防滑"]
},
{
name: "不锈钢保温杯",
category: "厨具",
price: NumberDecimal("89.00"),
stock: 500,
specs: { brand: "WarmHeld", capacity: "500ml", material: "304不锈钢" },
tags: ["水杯", "保温", "户外"]
}
])有序插入 vs 无序插入:
// ordered: true — 遇到错误停止后续插入(默认)
db.products.insertMany([
{ _id: "a", name: "A" },
{ _id: "a", name: "B" }, // 重复 _id,报错,B 不插入
{ _id: "c", name: "C" } // 因为有序,C 也不插入
], { ordered: true })
// ordered: false — 跳过错误继续插入
db.products.insertMany([
{ _id: "x", name: "X" },
{ _id: "x", name: "Y" }, // 重复 _id,跳过
{ _id: "z", name: "Z" } // 继续插入成功
], { ordered: false })3.2 Query
find 与 findOne
// 查询所有文档
db.products.find()
// 美化输出
db.products.find().pretty()
// 查询一条
db.products.findOne({ name: "机械键盘" })
// 投影 — 只返回指定字段
db.products.find(
{ category: "电子产品" },
{ name: 1, price: 1, _id: 0 } // 1=包含, 0=排除
)查询操作符
比较操作符:
// $eq — 等于
db.products.find({ price: NumberDecimal("299.00") })
// 或 $eq 显式写法
db.products.find({ price: { $eq: NumberDecimal("299.00") } })
// $ne — 不等于
db.products.find({ category: { $ne: "电子产品" } })
// $gt / $gte — 大于 / 大于等于
db.products.find({ price: { $gt: NumberDecimal("100.00") } })
db.products.find({ price: { $gte: NumberDecimal("100.00") } })
// $lt / $lte — 小于 / 小于等于
db.products.find({ stock: { $lt: 100 } })
db.products.find({ stock: { $lte: 150 } })
// $in — 在列表中
db.products.find({ category: { $in: ["电子产品", "运动用品"] } })
// $nin — 不在列表中
db.products.find({ category: { $nin: ["厨具"] } })逻辑操作符:
// $and — 且
db.products.find({
$and: [
{ price: { $gte: NumberDecimal("100.00") } },
{ stock: { $gte: 100 } }
]
})
// 多个条件直接写逗号默认就是 AND
db.products.find({
price: { $gte: NumberDecimal("100.00") },
stock: { $gte: 100 }
})
// $or — 或
db.products.find({
$or: [
{ category: "电子产品" },
{ price: { $lt: NumberDecimal("100.00") } }
]
})
// $nor — 非或(所有条件都不符合)
db.products.find({
$nor: [
{ stock: { $lt: 50 } },
{ category: "厨具" }
]
})
// $not — 取反
db.products.find({ stock: { $not: { $gte: 200 } } })元素与数组操作符:
// $exists — 字段是否存在
db.products.find({ specs.color: { $exists: true } })
// $type — 按 BSON 类型查询
db.products.find({ price: { $type: "decimal" } })
// $regex — 正则匹配
db.products.find({ name: { $regex: /键盘/ } })
db.products.find({ name: { $regex: "^无", $options: "i" } }) // i 忽略大小写
// 数组查询
// 精确匹配数组
db.products.find({ tags: ["蓝牙", "无线", "降噪"] })
// 包含某个元素
db.products.find({ tags: "蓝牙" })
// $all — 包含所有指定元素
db.products.find({ tags: { $all: ["无线", "降噪"] } })
// $size — 数组长度
db.products.find({ tags: { $size: 3 } })
// $elemMatch — 数组内嵌文档匹配
db.products.find({ tags: { $elemMatch: { $eq: "蓝牙" } } })嵌套文档查询:
// 点号访问嵌套字段(必须用引号)
db.products.find({ "specs.brand": "SoundPro" })
// 精确匹配嵌套文档
db.products.find({ specs: { brand: "SoundPro", color: "黑色", battery: "8小时" } })
// 注意:字段顺序和值必须完全一致!3.3 Update
updateOne — 更新单条
// $set — 设置字段
db.products.updateOne(
{ name: "无线蓝牙耳机" },
{ $set: { price: NumberDecimal("279.00"), "specs.color": "白色" } }
)
// $unset — 删除字段
db.products.updateOne(
{ name: "无线蓝牙耳机" },
{ $unset: { "specs.battery": "" } }
)
// $inc — 自增/自减(原子操作)
db.products.updateOne(
{ name: "机械键盘" },
{ $inc: { stock: -1 } } // 减1(下单扣库存)
)
// $mul — 相乘
db.products.updateOne(
{ name: "瑜伽垫" },
{ $mul: { price: NumberDecimal("0.9") } } // 打九折
)
// $min / $max — 条件更新
db.products.updateOne(
{ name: "不锈钢保温杯" },
{ $min: { price: NumberDecimal("79.00") } } // 如果当前 > 79 则更新
)updateMany — 批量更新
// 所有电子产品涨价 10%
db.products.updateMany(
{ category: "电子产品" },
{ $mul: { price: NumberDecimal("1.10") } }
)
// 所有库存不足 100 的商品标记为低库存
db.products.updateMany(
{ stock: { $lt: 100 } },
{ $set: { status: "low_stock" } }
)数组更新操作
// $push — 追加数组元素
db.products.updateOne(
{ name: "无线蓝牙耳机" },
{ $push: { tags: "热卖" } }
)
// $push + $each — 批量追加
db.products.updateOne(
{ name: "无线蓝牙耳机" },
{ $push: { tags: { $each: ["2024新款", "热销"] } } }
)
// $addToSet — 添加不重复元素
db.products.updateOne(
{ name: "无线蓝牙耳机" },
{ $addToSet: { tags: "热卖" } } // 已存在则不添加
)
// $pull — 删除匹配元素
db.products.updateOne(
{ name: "无线蓝牙耳机" },
{ $pull: { tags: "热卖" } }
)
// $pullAll — 删除多个匹配元素
db.products.updateOne(
{ name: "无线蓝牙耳机" },
{ $pullAll: { tags: ["2024新款", "热销"] } }
)
// $pop — 删除首/尾元素
db.products.updateOne(
{ name: "无线蓝牙耳机" },
{ $pop: { tags: 1 } } // 1=删除最后一个, -1=删除第一个
)
// 更新数组中的特定元素(使用位置 $ 或数组索引)
db.products.updateOne(
{ name: "无线蓝牙耳机", "tags": "降噪" },
{ $set: { "tags.$": "主动降噪" } } // $ 代表匹配到的第一个元素位置
)
// 使用数组索引(从 0 开始)
db.products.updateOne(
{ name: "无线蓝牙耳机" },
{ $set: { "tags.0": "蓝牙5.3" } }
)upsert — 不存在则插入
// upsert: true — 文档不存在则创建
db.products.updateOne(
{ name: "智能手表" },
{
$setOnInsert: {
category: "电子产品",
price: NumberDecimal("1299.00"),
stock: 60,
created_at: new Date()
},
$set: { updated_at: new Date() }
},
{ upsert: true }
)3.4 Delete
// deleteOne — 删除单个文档
db.products.deleteOne({ name: "智能手表" })
// deleteMany — 删除多个
db.products.deleteMany({ stock: { $eq: 0 } })
// 删除所有文档(保留集合)
db.products.deleteMany({})
// 删除集合(整个集合及索引)
db.products.drop()
// 删除数据库
db.dropDatabase()3.5 批量操作(bulkWrite)
bulkWrite 允许在一次网络往返中执行多种写入操作:
db.products.bulkWrite([
{
insertOne: {
document: {
name: "USB-C 充电线",
category: "电子产品",
price: NumberDecimal("39.00"),
stock: 1000
}
}
},
{
insertOne: {
document: {
name: "手机支架",
category: "配件",
price: NumberDecimal("25.00"),
stock: 500
}
}
},
{
updateOne: {
filter: { name: "机械键盘" },
update: { $inc: { stock: 10 } }
}
},
{
deleteOne: {
filter: { name: "瑜伽垫" }
}
},
{
updateMany: {
filter: { category: "电子产品" },
update: { $set: { updatedAt: new Date() } }
}
}
])
// 返回值示例
{
acknowledged: true,
insertedCount: 2,
matchedCount: 1,
modifiedCount: 2,
deletedCount: 1,
upsertedCount: 0,
insertedIds: {
'0': ObjectId("..."),
'1': ObjectId("...")
}
}4. 聚合管道
聚合管道(Aggregation Pipeline)是 MongoDB 最强大的数据加工工具。数据依次经过每个阶段(Stage),每个阶段对文档进行变换。
类比 Unix 管道:
cat log | grep ERROR | sort | uniq -c | head -10
4.1 基础阶段
准备示例数据:
db.orders.insertMany([
{ _id: 1, customer: "Alice", items: ["鼠标", "键盘"], total: 898, status: "completed", date: ISODate("2024-01-15") },
{ _id: 2, customer: "Bob", items: ["显示器"], total: 2399, status: "completed", date: ISODate("2024-01-20") },
{ _id: 3, customer: "Alice", items: ["硬盘"], total: 599, status: "pending", date: ISODate("2024-02-10") },
{ _id: 4, customer: "Carol", items: ["耳机", "鼠标垫"], total: 199, status: "completed", date: ISODate("2024-02-15") },
{ _id: 5, customer: "Bob", items: ["键盘", "音箱"], total: 1298, status: "shipped", date: ISODate("2024-03-05") },
{ _id: 6, customer: "Alice", items: ["笔记本"], total: 5999, status: "completed", date: ISODate("2024-03-20") }
])$match — 筛选(类似 WHERE)
db.orders.aggregate([
{ $match: { status: "completed" } }
])$group — 分组聚合(类似 GROUP BY)
// 按客户分组,计算总消费和订单数
db.orders.aggregate([
{ $group: {
_id: "$customer",
totalSpent: { $sum: "$total" },
orderCount: { $sum: 1 },
avgAmount: { $avg: "$total" },
minAmount: { $min: "$total" },
maxAmount: { $max: "$total" }
} }
])$sort — 排序
db.orders.aggregate([
{ $group: {
_id: "$customer",
totalSpent: { $sum: "$total" }
}},
{ $sort: { totalSpent: -1 } } // -1 降序, 1 升序
])$project — 投影/字段变换(类似 SELECT)
db.orders.aggregate([
{ $project: {
customer: 1,
total: 1,
date: { $dateToString: { format: "%Y-%m-%d", date: "$date" } },
status: 1,
itemsCount: { $size: "$items" },
// 创建新计算字段
totalCategory: {
$cond: {
if: { $gte: ["$total", 1000] },
then: "大额订单",
else: "普通订单"
}
},
_id: 0
} }
])组合管道示例
// 找出消费总额最高的客户及其订单统计
db.orders.aggregate([
// 1. 筛选已完成订单
{ $match: { status: { $in: ["completed", "shipped"] } } },
// 2. 按客户分组统计
{ $group: {
_id: "$customer",
totalSpent: { $sum: "$total" },
orderCount: { $sum: 1 },
avgOrderValue: { $avg: "$total" }
}},
// 3. 只保留总消费 >= 1000 的客户
{ $match: { totalSpent: { $gte: 1000 } } },
// 4. 按总消费降序排列
{ $sort: { totalSpent: -1 } },
// 5. 格式化输出
{ $project: {
customer: "$_id",
totalSpent: { $round: ["$totalSpent", 2] },
orderCount: 1,
avgOrderValue: { $round: ["$avgOrderValue", 2] },
_id: 0
}}
])4.2 $lookup — 关联查询(类似 JOIN)
准备关联集合:
db.customers.insertMany([
{ _id: "Alice", email: "alice@example.com", city: "北京", vip: true },
{ _id: "Bob", email: "bob@example.com", city: "上海", vip: false },
{ _id: "Carol", email: "carol@example.com", city: "深圳", vip: true }
])
db.order_details.insertMany([
{ orderId: 1, product: "鼠标", qty: 1, price: 199 },
{ orderId: 1, product: "键盘", qty: 1, price: 699 },
{ orderId: 2, product: "显示器", qty: 1, price: 2399 },
{ orderId: 6, product: "笔记本", qty: 1, price: 5999 }
])// 关联 orders 与 customers
db.orders.aggregate([
{ $lookup: {
from: "customers", // 关联集合名
localField: "customer", // 本地字段
foreignField: "_id", // 对方字段
as: "customerInfo" // 输出数组字段名
}},
// $lookup 结果是一个数组,用 $unwind 展开
{ $unwind: {
path: "$customerInfo",
preserveNullAndEmptyArrays: true // 保留没有客户信息的订单
}},
{ $project: {
orderId: "$_id",
customer: 1,
total: 1,
city: "$customerInfo.city",
isVip: "$customerInfo.vip",
email: "$customerInfo.email",
_id: 0
}}
])多表关联 + 聚合:
// 查询每个VIP客户的订单详情
db.orders.aggregate([
// 关联客户信息
{ $lookup: {
from: "customers",
localField: "customer",
foreignField: "_id",
as: "customer"
}},
{ $unwind: "$customer" },
// 只保留 VIP 客户
{ $match: { "customer.vip": true } },
// 关联订单详情
{ $lookup: {
from: "order_details",
localField: "_id",
foreignField: "orderId",
as: "details"
}},
// 按客户分组统计
{ $group: {
_id: "$customer._id",
customerEmail: { $first: "$customer.email" },
totalOrders: { $sum: 1 },
totalSpent: { $sum: "$total" },
products: { $push: "$details.product" }
}},
// 展开产品列表(合并嵌套数组)
{ $project: {
customerEmail: 1,
totalOrders: 1,
totalSpent: 1,
products: {
$reduce: {
input: "$products",
initialValue: [],
in: { $concatArrays: ["$$value", "$$this"] }
}
}
}}
])4.3 高级阶段
$unwind — 展开数组
// 将每个订单按商品展开为多行
db.orders.aggregate([
{ $unwind: "$items" },
{ $project: {
customer: 1,
item: "$items",
total: 1,
_id: 0
}}
])
// 结果:
// { customer: "Alice", item: "鼠标", total: 898 }
// { customer: "Alice", item: "键盘", total: 898 }
// { customer: "Bob", item: "显示器", total: 2399 }
// ...// 统计哪个商品被购买最多
db.orders.aggregate([
{ $unwind: "$items" },
{ $group: {
_id: "$items",
purchaseCount: { $sum: 1 }
}},
{ $sort: { purchaseCount: -1 } }
])$bucket — 分桶
// 将订单按金额分桶
db.orders.aggregate([
{ $bucket: {
groupBy: "$total",
boundaries: [0, 200, 500, 1000, 5000, 10000],
default: "其他",
output: {
count: { $sum: 1 },
orders: { $push: { id: "$_id", customer: "$customer" } },
avgTotal: { $avg: "$total" }
}
}}
])$facet — 多面聚合
// 在一个管道中同时执行多种聚合
db.orders.aggregate([
{ $facet: {
// 面1: 按状态统计订单数
"byStatus": [
{ $group: { _id: "$status", count: { $sum: 1 } } }
],
// 面2: 按月份统计销售额
"byMonth": [
{ $group: {
_id: { $dateToString: { format: "%Y-%m", date: "$date" } },
totalRevenue: { $sum: "$total" },
orderCount: { $sum: 1 }
}},
{ $sort: { _id: 1 } }
],
// 面3: 总统计
"summary": [
{ $group: {
_id: null,
totalOrders: { $sum: 1 },
totalRevenue: { $sum: "$total" },
avgOrderValue: { $avg: "$total" }
}}
],
// 面4: 最热卖商品(先展开数组)
"topItems": [
{ $unwind: "$items" },
{ $group: { _id: "$items", count: { $sum: 1 } } },
{ $sort: { count: -1 } },
{ $limit: 3 }
]
}}
])4.4 完整聚合示例:电商报表
// 月度销售报表
db.orders.aggregate([
{ $match: { status: { $ne: "cancelled" } } },
{ $lookup: {
from: "customers",
localField: "customer",
foreignField: "_id",
as: "customerInfo"
}},
{ $unwind: { path: "$customerInfo", preserveNullAndEmptyArrays: true } },
{ $group: {
_id: {
year: { $year: "$date" },
month: { $month: "$date" },
city: "$customerInfo.city"
},
revenue: { $sum: "$total" },
orders: { $sum: 1 },
customers: { $addToSet: "$customer" }
}},
{ $project: {
period: {
$concat: [
{ $toString: "$_id.year" }, "-",
{ $cond: {
if: { $lt: ["$_id.month", 10] },
then: { $concat: ["0", { $toString: "$_id.month" }] },
else: { $toString: "$_id.month" }
}}
]
},
city: "$_id.city",
revenue: { $round: ["$revenue", 2] },
orderCount: "$orders",
customerCount: { $size: "$customers" },
_id: 0
}},
{ $sort: { period: 1, revenue: -1 } }
])5. 索引
没有索引的查询就是"全表扫描"(Collection Scan),在数据量大时性能灾难。MongoDB 使用 B-Tree 索引结构。
5.1 单字段索引
// 创建单字段升序索引
db.products.createIndex({ price: 1 }) // 1=升序, -1=降序
// 查看集合索引
db.products.getIndexes()
// 结果示例
[
{ v: 2, key: { _id: 1 }, name: "_id_" },
{ v: 2, key: { price: 1 }, name: "price_1" }
]
// 唯一索引
db.products.createIndex({ name: 1 }, { unique: true })
// 稀疏索引(只索引包含该字段的文档)
db.products.createIndex({ email: 1 }, { sparse: true })5.2 复合索引
// 复合索引:先按 category 排序,再按 price 排序
db.products.createIndex({ category: 1, price: -1 })
// 复合索引的"最左前缀"原则:
// 以下查询可以使用该索引:
db.products.find({ category: "电子产品" })
db.products.find({ category: "电子产品", price: { $gt: 100 } })
// 以下查询不能使用该索引(没有 category 条件):
db.products.find({ price: { $gt: 100 } })
// 复合索引设计建议
// 1. 等值条件字段放前面
// 2. 排序字段放后面
// 3. 范围条件字段放最后5.3 多键索引(数组字段)
当被索引的字段是数组时,MongoDB 自动创建多键索引(Multikey Index):
// 自动创建多键索引
db.products.createIndex({ tags: 1 })
// 多键索引对以下查询有效
db.products.find({ tags: "蓝牙" })
db.products.find({ tags: { $all: ["无线", "降噪"] } })
// 注意:一个复合索引中只能有一个数组字段
// 下面的创建会报错(两个数组字段)
// db.products.createIndex({ tags: 1, items: 1 }) // ❌5.4 文本索引
用于全文搜索:
// 在 name 和 specs.brand 字段上创建文本索引
db.products.createIndex(
{ name: "text", "specs.brand": "text" },
{ weights: { name: 10, "specs.brand": 5 } } // name 权重更高
)
// $text 查询
db.products.find({ $text: { $search: "蓝牙耳机" } })
// 按文本相关性排序
db.products.find(
{ $text: { $search: "键盘机械" } },
{ score: { $meta: "textScore" } }
).sort({ score: { $meta: "textScore" } })
// 中文分词 — 需要额外配置(mongosh 中需安装 ICU 或 Elasticsearch 配合)
// MongoDB 文本索引默认支持的语言不包括中文5.5 Geospatial 索引(2dsphere)
// 准备地理数据(GeoJSON 格式)
db.places.insertMany([
{
name: "望京 SOHO",
location: {
type: "Point",
coordinates: [116.4741, 39.9952] // [经度, 纬度]
},
category: "写字楼"
},
{
name: "朝阳公园",
location: {
type: "Point",
coordinates: [116.4824, 39.9347]
},
category: "公园"
},
{
name: "天安门广场",
location: {
type: "Point",
coordinates: [116.3972, 39.9087]
},
category: "景点"
}
])
// 创建 2dsphere 索引
db.places.createIndex({ location: "2dsphere" })
// 查询附近的地点(距离中心点 5km 内)
db.places.find({
location: {
$near: {
$geometry: {
type: "Point",
coordinates: [116.4600, 39.9800]
},
$maxDistance: 5000, // 5km
$minDistance: 0
}
}
})
// $geoWithin — 查询在指定多边形内的地点
db.places.find({
location: {
$geoWithin: {
$geometry: {
type: "Polygon",
coordinates: [[
[116.35, 39.90],
[116.50, 39.90],
[116.50, 40.00],
[116.35, 40.00],
[116.35, 39.90]
]]
}
}
}
})5.6 TTL 索引(自动过期)
// 创建会话集合,30分钟后自动删除
db.sessions.createIndex(
{ createdAt: 1 },
{ expireAfterSeconds: 1800 } // 1800秒 = 30分钟
)
// 插入数据(必须包含 createdAt 日期字段)
db.sessions.insertOne({
sessionId: "abc123",
userId: "user_001",
createdAt: new Date() // TTL 索引基于此字段
})
// 自定义过期时间(使用 expireAfterSeconds: 0)
// 文档中指定 expireAt 字段
db.coupons.createIndex(
{ expireAt: 1 },
{ expireAfterSeconds: 0 }
)
db.coupons.insertOne({
code: "DISCOUNT50",
discount: 50,
expireAt: new Date("2024-12-31T23:59:59Z") // 过期时间精确到秒
})
// 说明:TTL 索引每隔 60 秒运行一次后台清理任务5.7 索引优化与 explain()
// 使用 explain() 分析查询计划
db.products.find({ price: { $gt: 100 } }).explain("executionStats")
// 输出关键指标解读:
{
queryPlanner: {
winningPlan: {
stage: "IXSCAN", // 索引扫描(良好),COLLSCAN 表示全表扫描(差)
indexName: "price_1", // 使用的索引
...
}
},
executionStats: {
nReturned: 5, // 返回文档数
totalDocsExamined: 5, // 检查的文档数(应接近 nReturned)
totalKeysExamined: 5, // 检查的索引键数
executionTimeMillis: 1, // 执行时间(ms)
...
}
}
// 最佳实践:totalDocsExamined ≈ nReturned 表示索引高效
// 如果 totalDocsExamined >> nReturned,需要考虑优化索引
// 强制使用指定索引
db.products.find({ price: { $gt: 100 } }).hint({ price: 1 })
// 删除索引
db.products.dropIndex("price_1") // 按名称删除
db.products.dropIndex({ price: 1 }) // 按定义删除
db.products.dropIndexes() // 删除所有非 _id 索引索引使用总结:
| 索引类型 | 创建命令 | 适用场景 |
|---|---|---|
| 单字段索引 | { field: 1 } |
等值/范围查询 |
| 复合索引 | { a:1, b:-1 } |
多条件查询 |
| 多键索引 | { arrayField: 1 } |
数组字段查询 |
| 文本索引 | { field: "text" } |
全文搜索 |
| 地理索引 | { loc: "2dsphere" } |
位置查询 |
| TTL 索引 | { t: 1 }, { expireAfterSeconds: N } |
自动过期数据 |
| 唯一索引 | { f: 1 }, { unique: true } |
约束唯一性 |
| 稀疏索引 | { f: 1 }, { sparse: true } |
只索引含该字段的文档 |
| 哈希索引 | { f: "hashed" } |
分片键(均衡分布) |
| 部分索引 | { f: 1 }, { partialFilterExpression: { ... } } |
只索引部分文档 |
6. 复制集与分片
6.1 复制集(Replica Set)
复制集是 MongoDB 的高可用方案,由一主多从组成。
复制集架构
┌─────────────┐ 选举 ┌─────────────┐
│ Primary │◄────────────│ Secondary │
│ (主节点) │ │ (从节点1) │
│ 读写操作 │ ───────────►│ 异步复制 │
└──────┬──────┘ oplog └─────────────┘
│ ┌─────────────┐
│ │ Secondary │
└────────────────────►│ (从节点2) │
异步复制 │ 异步复制 │
└─────────────┘主从切换与读写分离
// ======== Docker 部署 3 节点复制集 ========
// 1. 启动三个 MongoDB 容器
docker run -d --name mongo-rs1 -p 27017:27017 \
mongo:8.0 --replSet rs0 --bind_ip_all
docker run -d --name mongo-rs2 -p 27018:27017 \
mongo:8.0 --replSet rs0 --bind_ip_all
docker run -d --name mongo-rs3 -p 27019:27017 \
mongo:8.0 --replSet rs0 --bind_ip_all
// 2. 连接到主节点并初始化复制集
docker exec -it mongo-rs1 mongosh
// 在 mongosh 中:
config = {
_id: "rs0",
members: [
{ _id: 0, host: "mongo-rs1:27017", priority: 2 },
{ _id: 1, host: "mongo-rs2:27017", priority: 1 },
{ _id: 2, host: "mongo-rs3:27017", priority: 1 }
]
}
rs.initiate(config)
// 查看复制集状态
rs.status()
// 查看当前谁是主节点
rs.isMaster()
// 3. 在驱动连接串中指定多个节点,实现自动故障转移
// mongodb://mongo-rs1:27017,mongo-rs2:27017,mongo-rs3:27017/mydb?replicaSet=rs0读写分离配置
// 主节点写入,从节点读取(需要设置 readPreference)
// 在 mongosh 中:
rs.secondaryOk() // 允许从从节点读取
// 在应用代码驱动中:
// readPreference=secondary // 始终读从节点
// readPreference=primaryPreferred // 优先读主节点,主节点不可用读从节点
// 写关注(Write Concern)
// w: "majority" — 等待大多数节点确认写入
db.orders.insertOne(
{ item: "商品", qty: 100 },
{ writeConcern: { w: "majority", wtimeout: 5000 } }
)
// 读关注(Read Concern)
// "majority" — 只读取已被大多数节点确认的数据
db.orders.find().readConcern("majority")
// "local" — 默认,可能读到尚未复制到从节点的数据
db.orders.find().readConcern("local")6.2 分片(Sharding)
当单机无法承载数据量或吞吐量时,使用分片将数据分布到多个节点。
分片架构组件
┌──────────────────────────────────────────────┐
│ mongos (路由层) │
│ 应用程序通过 mongos 访问整个集群 │
└──────┬────────────────────────┬───────────────┘
│ │
┌──────▼──────┐ ┌─────────▼────────┐
│ Shard 1 │ │ Shard 2 │
│ (复制集 A) │ │ (复制集 B) │
│ 数据范围: │ │ 数据范围: │
│ 0 ~ 10000 │ │ 10001 ~ 20000 │
└─────────────┘ └──────────────────┘
│ │
└────────┬───────────┘
│
┌────────────▼────────────┐
│ Config Server │
│ (配置服务器,存储元数据) │
│ 必须为复制集 │
└─────────────────────────┘分片键选择
分片键(Shard Key)是分片的核心决策,直接影响性能:
// ======== 启用分片 ========
// 1. 连接到 mongos
mongos> sh.enableSharding("shop")
// 2. 在集合上启用分片,选择分片键
// 方式一:基于 _id 的哈希分片(最常用,数据分布均匀)
mongos> sh.shardCollection("shop.orders", { _id: "hashed" })
// 方式二:基于范围分片
mongos> sh.shardCollection("shop.orders", { customer: 1 })
// 方式三:基于复合分片键(MongoDB 5.0+)
mongos> sh.shardCollection("shop.orders", { customer: 1, date: 1 })分片键选择原则:
| 原则 | 说明 | 好坏示例 |
|---|---|---|
| 高基数 | 分片键的值应足够多 | ✅ userId ❌ gender(只有男女) |
| 写分布均衡 | 写入应均匀分布到各分片 | ✅ 哈希 _id ❌ 单调递增 createdAt |
| 查询高效 | 尽量将查询路由到少量分片 | ✅ 按 userId 查询 ❌ 按 status 查询 |
分片策略对比:
// 1. 哈希分片(Ranged Hash)
// 优点:写入分布最均匀
// 缺点:范围查询会广播到所有分片
sh.shardCollection("shop.logs", { _id: "hashed" })
// 2. 范围分片
// 优点:范围查询高效(只需访问相关分片)
// 缺点:数据倾斜风险(如 hot 分片)
sh.shardCollection("shop.orders", { customer: 1 })
// 3. 区域分片(Zone Sharding,按地域)
// 适用于数据本地化场景
sh.addShardToZone("shard01", "华东")
sh.addShardToZone("shard02", "华北")
sh.updateZoneKeyRange(
"shop.orders",
{ region: "华东" },
{ region: "华东" },
"华东"
)分片管理命令
// 查看分片集群状态
sh.status()
// 查看数据分布
db.orders.getShardDistribution()
// 手动合并块(Chunk)
// 在分片间数据不均衡时手动触发
sh.mergeChunks(
"shop.orders",
{ customer: "A" },
{ customer: "M" }
)
// 查看分片键范围
db.orders.getShardKey()7. 其他 NoSQL 数据库简介
7.1 Cassandra(列族数据库)
Apache Cassandra 是 Facebook 开源的宽列存储数据库,设计哲学来自 Amazon Dynamo 和 Google Bigtable。
核心特性:
- 去中心化架构:所有节点对等,无单点故障
- 最终一致性:可调节一致性级别(ONE、QUORUM、ALL)
- 线性扩展:增加节点即可线性提升吞吐量
- 近乎零宕机:滚动升级、节点替换不影响服务
数据模型:
-- Cassandra 使用 CQL(Cassandra Query Language),类 SQL
CREATE KEYSPACE myapp
WITH replication = {
'class': 'SimpleStrategy',
'replication_factor': 3
};
CREATE TABLE myapp.users_by_email (
email text PRIMARY KEY,
name text,
age int,
city text
);
CREATE TABLE myapp.users_by_city (
city text,
email text,
name text,
age int,
PRIMARY KEY (city, email) -- 复合主键:分区键 + 聚类键
);
-- 插入
INSERT INTO myapp.users_by_email (email, name, age, city)
VALUES ('alice@example.com', 'Alice', 30, '北京');
-- 查询(必须带分区键)
SELECT * FROM myapp.users_by_city WHERE city = '北京';适用场景:
- IoT 设备数据采集
- 时序指标和监控数据
- 消息系统(如 DynamoDB 风格)
- 需要写入吞吐量极高的场景
不适用场景:
- 多表关联(无 JOIN 支持)
- 事务性强的业务(无 ACID)
- 聚合分析(不支持 GROUP BY、SUM 等)
7.2 Neo4j(图数据库)
Neo4j 是最流行的图数据库,使用属性图模型(节点+关系+属性)。
核心概念:
(Alice) —[:FOLLOWS]→ (Bob) —[:LIKES]→ (Post1)
│ │
└────—[:COMMENTED]→ (Comment1) ←————─┘Cypher 查询语言:
// 创建节点和关系
CREATE (alice:User {name: 'Alice', age: 30})
CREATE (bob:User {name: 'Bob', age: 28})
CREATE (post:Post {title: 'MongoDB 入门教程', content: '...'})
CREATE (alice)-[:FOLLOWS]->(bob)
CREATE (bob)-[:LIKES]->(post)
CREATE (alice)-[:COMMENTED {text: '好文章!'}]->(post)
// 查询推荐的社交关系
// 找到 Bob 关注的人中 Alice 也认识的人
MATCH (alice:User {name: 'Alice'})-[:FOLLOWS]->(common:User)<-[:FOLLOWS]-(bob:User {name: 'Bob'})
RETURN common.name
// 找到 Alice 可能感兴趣的内容(好友喜欢的内容)
MATCH (alice:User {name: 'Alice'})-[:FOLLOWS]->(friend:User)
MATCH (friend)-[:LIKES]->(post:Post)
WHERE NOT (alice)-[:LIKES]->(post)
RETURN post.title, count(friend) AS recommendationScore
ORDER BY recommendationScore DESC适用场景:
- 社交网络(好友推荐、关系发现)
- 反欺诈(资金流链路分析)
- 知识图谱(实体关系检索)
- 推荐引擎(协同过滤)
- 权限管理(组织层级、角色继承)
7.3 InfluxDB(时序数据库)
InfluxDB 是专门为时序数据设计的数据库,由 InfluxData 开发。
核心概念:
| 概念 | 说明 | 示例 |
|---|---|---|
| Measurement | 类似于关系型的表 | cpu_usage, sensor_temperature |
| Tag | 索引标签(键值对) | host=server01, region=cn-beijing |
| Field | 实际指标值 | value=83.5, load=0.75 |
| Timestamp | 时间戳(纳秒精度) | 2024-09-15T10:00:00Z |
InfluxDB 操作示例:
-- InfluxQL(类 SQL)
-- 插入时序数据
INSERT cpu_usage,host=server01,region=cn-beijing value=83.5,load=0.75 1726408800000000000
-- 查询
SELECT mean("value") AS avg_cpu
FROM "cpu_usage"
WHERE time > now() - 1h
AND "region" = 'cn-beijing'
GROUP BY time(5m), "host"适用场景:
- 服务器/容器监控(CPU、内存、网络)
- 应用性能监控(APM)
- 物联网传感器数据
- 金融行情数据
- 实时仪表盘
7.4 各选型场景对比
| 场景 | 推荐数据库 | 原因 |
|---|---|---|
| 内容管理/博客/CMS | MongoDB | 文档结构灵活,支持嵌套,Schema 变化无痛 |
| 电商商品目录 | MongoDB | 商品属性多变,支持嵌套规格和动态字段 |
| 实时排行榜/计数器 | Redis | 毫秒级响应,支持 ZSET/Sorted Set |
| 用户会话/缓存 | Redis | 键值快速读写,TTL 自动过期 |
| IoT 传感器数据 | Cassandra / InfluxDB | 写入吞吐极高,时序聚合函数强大 |
| 社交关系推荐 | Neo4j | 图遍历效率比 SQL JOIN 高数个数量级 |
| 日志/审计 | MongoDB / Elasticsearch | 文档灵活,支持全文检索 |
| 金融核心交易 | 关系型数据库 | 需要严格 ACID 事务和审计合规 |
| 规则引擎/权限 | Neo4j | 树形/图结构最适合角色继承 |
总结
MongoDB 作为 NoSQL 文档数据库的代表,凭借其灵活的文档模型、丰富的查询语言、强大的聚合管道和原生的扩展能力,在现代 Web 应用、内容管理、大数据分析等场景中得到了广泛应用。
选择建议:
- 如果你的数据结构频繁变化,或需要处理嵌套/不规则数据 → MongoDB
- 如果你要求强 ACID、复杂的多表关联 → 关系型数据库
- 如果你需要高吞吐的时序数据存储 → InfluxDB / TimescaleDB
- 如果你需要处理复杂的关系和连接 → Neo4j
- 如果你需要全球分布式、零宕机的系统 → Cassandra / DynamoDB
NoSQL 与关系型数据库并非对立关系,而是互为补充。现代架构中经常混合使用多种数据库,每种解决自己最擅长的问题。这正是 Persistence Polyglot(多语言持久化)的精髓。
"The right tool for the right job." — 技术选型的永恒真理。
本文所有 MongoDB 示例均在 MongoDB 8.0 中验证通过。其他数据库版本可能存在细微差异。