36.MongoDB总结
36.1 核心要点速览
SQL 和 NoSQL
- 选型时除了「有没有表结构」,还要看数据结构化程度、一致性要求、查询形态、扩展方式、典型业务是否匹配;下面总表与开篇对比维度(数据结构、一致性、查询、扩展、典型场景)对齐,可一起记。
- 扩展:SQL 常见路径是垂直扩展(更强单机);NoSQL 体系更常按水平扩展(加节点分片)来设计容量。
- 一致性叙事:账务、支付等多用强一致 + 事务说话;时间线、统计类场景往往接受最终一致,换吞吐与可用性。
| 对比维度 | SQL | NoSQL |
|---|---|---|
| 数据模型 | - 表格(行×列) - 固定模式(Schema) |
- 文档(JSON/BSON)、键值、列族、图 - 动态模式,灵活 |
| 查询语言 | - 标准化 SQL - 支持复杂查询、聚合、联表 |
- 各自接口(如 MongoDB 查询、Redis GET) - 依赖具体实现 |
| 事务支持 | - 完整 ACID(原子性、一致性、隔离性、持久性) | - 通常“最终一致” - 部分产品支持限域事务 |
| 性能 | - 优化后复杂联表与事务性能优 - 适合结构化查询 |
- 高并发、低延迟 - 大规模水平扩展 |
| 适用场景 | - 金融、电商等高一致性需求 - 复杂关系建模 |
- 日志、社交、物联网等半/非结构化数据 - 高吞吐缓存 |
| 优势 | - 生态成熟、工具丰富 - 数据完整性高 |
- 扩展性强、灵活性高 - 支持多样数据模型 |
| 局限 | - 模式固定、扩展复杂 - 海量并发场景受限 |
- 标准化查询缺失 - 复杂事务与关联查询能力弱 |
NoSQL 类型对比
- 本篇对比键值、列式、文档三类:键值极简单、延迟低;列式偏读少列、分析型;文档型在半结构化业务数据与灵活查询之间折中,MongoDB 属于这一类。
- 没有「万能 NoSQL」:先问数据是不是键值就够、是不是分析扫列、是不是文档嵌套更合适,再选代表产品。
| 类型 | 代表工具 | 主要优点 | 主要缺点 |
|---|---|---|---|
| 键值存储 | Redis、Memcached、DynamoDB | - 超高性能 - 简单模型 - 易水平扩展 |
- 仅键值对 - 无复杂查询 - 一致性风险 |
| 列式存储 | HBase、Cassandra、Bigtable | - 列级压缩高效 - OLAP 分析优 |
- 写入与更新慢 - 全表/多列查询低效 |
| 文档存储 | MongoDB、CouchDB、Couchbase | - 结构灵活 - 水平扩展 - 强查询功能 |
- 事务与一致性弱 - 复杂建模难 |
MongoDB下载安装
步骤
获取安装包
- 官方下载页获取 Community Edition
- 可选格式:
msi(向导式安装)、zip(便携解压)、Docker 镜像
安装流程
- 对于
msi:一路“Next”完成 - 对于
zip:解压后手动配置环境变量 - 对于 Docker:拉取镜像并运行容器
- 对于
服务运行账户
- Network Service user:Windows 内置低权限账户,安全风险最小
- Local User / Domain User:自定义账户,可精细控制文件与网络访问
- 仅影响操作系统层面权限,不改动数据库内部用户认证
可选组件
- MongoDB Compass:官方 GUI 可视化管理工具,命令行不熟悉者推荐
- 可根据需求勾选,无需强制安装
安装后验证
- 打开“服务”窗口,确认 “MongoDB” 服务存在且设为“自动”启动
形态选择
msi适合本机快速装服务;zip适合自定义目录与脚本化;Docker 适合隔离环境与版本对齐,三者数据目录与配置仍要落在安全、可备份的磁盘上。
安装选项对比表
| 安装环节 | 可选项 | 适用场景与说明 |
|---|---|---|
| 安装包类型 | msizipDocker |
向导安装 / 便携版 / 容器化部署 |
| 服务账户 | Network Service user Local/Domain User |
内置低权限 / 自定义权限,适合不同安全与企业环境需求 |
| GUI 管理工具 | MongoDB Compass | 图形界面管理,适合不熟悉命令行的用户 |
| 安装验证 | Windows 服务管理 | 确认 “MongoDB” 服务已安装、启动类型为“自动” |
MongoDB配置文件
配置文件类型
mongod.cfg(Windows 安装向导生成)mongod.conf(Linux/macOS 原生,Windows 亦可)
文件格式对比
- Key-Value:安装程序生成,格式简单
- YAML:官方推荐,结构清晰,便于维护
常用配置项
- systemLog:日志路径与方式
- storage:数据目录与存储引擎
- net:绑定 IP 与端口
- security:开启授权认证
- processManagement:守护进程与 PID 文件
生效方式
- 配置文件:
mongod --config /path/to/mongod.conf - 命令行:
--dbpath,--logpath,--bind_ip,--port,--auth等
- 配置文件:
网络与安全
- 示例里常见
bindIp: 127.0.0.1,表示仅本机可连;若要远程访问,须同时改绑定地址、系统防火墙、并启用认证(否则等于把库暴露在公网)。
- 示例里常见
| 配置文件 | 默认平台 | 格式 | 适用场景 |
|---|---|---|---|
| mongod.cfg | Windows | Key-Value | 安装向导生成的简易配置 |
| mongod.conf | Linux/macOS(*) | YAML | 官方推荐,易于扩展维护 |
| 配置项 | 作用 | 常见字段示例 |
|---|---|---|
| systemLog | 日志输出位置与模式 | destination, path |
| storage | 数据目录与存储引擎选项 | dbPath |
| net | 网络绑定地址与监听端口 | bindIp, port |
| security | 用户认证与授权 | authorization: enabled |
| processManagement | 守护进程模式与 PID 文件 | fork, pidFilePath |
MongoDB相关工具
官方工具
- mongosh:命令行交互,脚本与自动化管理
- Compass:GUI 可视化,拖拽式查询与分析
第三方工具示例
- Robo 3T / Studio 3T:增强型界面与导出功能
- DbGate:跨 DB 多引擎统一管理
CLI 使用流程
- 下载并解压或安装
- 启动工具后输入连接字符串
- 常用命令:
show dbs、use <db>、show collections、db.collection.insertOne()
| 工具名称 | 类型 | 主要用途 |
|---|---|---|
| mongosh | CLI | 脚本化管理,自动化操作 |
| Compass | GUI | 可视化数据浏览、查询与性能分析 |
| Robo 3T | GUI | 轻量级图形界面,支持多种导出与插件 |
| DbGate | GUI/多引擎 | 多数据库统一管理,支持自定义查询与插件安装 |
MongoDB连接和启用认证
连接字符串格式
mongodb://<user>:<pwd>@<host>:<port>/<db>?options- 特殊字符需 URL 编码(
@ → %40,/ → %2F,: → %3A,? → %3F)
常用选项
- authSource:在哪个库验票(管理员常在
admin)。 - retryWrites:是否自动重试可重试类写入失败;默认值随驱动版本而变,连接串里写清楚最省心。
- w / journal:写入确认级别,与副本集、一致性要求相关。
- readPreference:副本集读路由偏好(如
primary、secondary),和w、读写分离一起设计。 - ssl、connectTimeoutMS、socketTimeoutMS、maxPoolSize 等:链路加密、超时与连接池上限。
- authSource:在哪个库验票(管理员常在
启用认证步骤
- 在配置文件或启动参数中开启
authorization: enabled或--auth - 用命令行连接到 admin 库,
db.createUser(...)创建管理员 - 优雅停服:
db.adminCommand({ shutdown: 1 })或系统服务命令 - 重启带认证的服务,使用带凭证的连接字符串重新连接
- 在配置文件或启动参数中开启
| 步骤 | 方式 | 示例命令/字段 |
|---|---|---|
| 开启认证 | 配置文件 / 参数 | security.authorization: enabled--auth |
| 创建管理员 | mongosh | use admindb.createUser({...}) |
| 优雅关闭 | mongosh / 系统服务 | db.adminCommand({ shutdown: 1 })net stop MongoDB |
| 重启服务并连接 | 配置文件 / 参数 + URI | mongod --config ...mongodb://user:pwd@... |
MongoDB的ObjectID
定义与作用
- 默认主键类型,12 字节 BSON,用于全局唯一标识文档
- 十六进制字符串(24 字符)表现
组成结构
- 时间戳(4 字节):秒级 Unix 时间,保证排序性
- 机器标识(3 字节):主机唯一哈希,区分节点
- 进程 ID(2 字节):区分同机多进程生成
- 随机计数器(3 字节):同时间内递增,防止冲突
核心特性
- 全局唯一性:组合信息确保不同环境无重复
- 时间可提取:
getTimestamp()可还原创建时刻 - 存储高效:仅 12 字节,比 UUID 更节省空间
常见用法
- 默认生成:插入文档时自动赋值
- 查询筛选:
find({ _id: ObjectId(...) }) - 手动生成:
var id = ObjectId() - 获取创建时间:
ObjectId("…").getTimestamp() - 转十六进制字符串:现行 mongosh 常用
ObjectId("…").toString()(与 shell 版本一致即可)
| 组成部分 | 字节数 | 主要用途 |
|---|---|---|
| 时间戳 | 4 | 保证生成顺序与时间关联 |
| 机器标识 | 3 | 区分不同主机 |
| 进程 ID | 2 | 区分同机不同进程 |
| 随机计数器 | 3 | 同秒内生成唯一性 |
MongoDB插入
insertOne(单条插入)- 语法:
db.collection.insertOne(document, options) - 自动生成
_id(若未指定),或使用自定义_id - 可选
writeConcern、bypassDocumentValidation等 - 返回
{ acknowledged: true, insertedId: <_id> }
- 语法:
insertMany(批量插入)- 语法:
db.collection.insertMany([doc1, doc2, …], options) - 一次插入多条文档
- 返回
{ acknowledged: true, insertedIds: […] }
- 语法:
冲突处理
- 若指定的
_id已存在,抛出MongoServerError: E11000 duplicate key error - 可在应用层捕获并处理,或改用
upsert等方式
- 若指定的
insertOne vs. insertMany 对比
| 操作 | 场景 | 参数 | 返回值 | 注意事项 |
|---|---|---|---|---|
| insertOne | 单文档写入 | document, options |
{ acknowledged, insertedId } |
_id 自动/手动,冲突报错 |
| insertMany | 批量写入 | Array<document>, options |
{ acknowledged, insertedIds } |
_id 冲突报 E11000;批量是否「一条失败就停」由 insertMany 的选项控制,以当前版本文档为准 |
MongoDB查询
- 入口:
find(filter, projection);filter为{}时可匹配全部文档,生产仍应带条件并配合索引。 - 投影:除
_id外,同一 projection 不能混用「只返回若干字段的1」与「排除若干字段的0」;_id: 0可与 inclusion 搭配。 - 正则:
$regex易触发全表扫描,热点检索要考虑索引或换搜索方案。 - 排序与分页:字符串按 Unicode,数字存成字符串时
"10"会排在"2"前;深分页避免超大skip,大集合优先用基于_id或稳定排序键的游标翻页。
MongoDB基础查询
方法:
db.collection.find(query, projection)常见操作:
- 等于
$eq、不等$ne、大于$gt、小于$lt、… - 字段存在
$exists、类型匹配$type
- 等于
示例:
db.users.find({ age: { $gte: 25 } }, { name: 1 })
| 操作 | 语法 | 说明 |
|---|---|---|
| 等于 | { age: 25 } |
age 精确匹配 25 |
| 范围查询 | { age: { $gt:25, $lt:30 } } |
age 在 (25,30) 之间 |
| 字段存在 | { score: { $exists:true } } |
仅返回含有 score 字段的 |
| 类型匹配 | { age: { $type:"int" } } |
age 必须为整数 |
MongoDB数组查询和嵌套字段查询
数组查询
$in/$nin:数组中是否出现列表里任一值;$nin对无该字段的文档也会匹配,语义上要心里有数。- 标量数组上「某个元素满足比较」:直接写
{ arr: { $gt: 2 } }等,服务器按元素比对。 $elemMatch:用于元素为子文档的数组,且要求同一个元素同时满足多条条件(子文档内 AND)。
嵌套字段
- 点语法
"bag.index": value或范围{ "bag.index": { $gt: 0 } }
- 点语法
db.users.find({ lessons:{ $in:[1,4] } })
db.users.find({ lessons:{ $gt:2 } })
db.users.find({ "bag.index":1 })
| 功能 | 操作符 / 写法 | 作用 |
|---|---|---|
| 包含任一元素 | { lessons:{ $in:[…] }} |
数组出现过列表中任一项 |
| 排除列表 | { lessons:{ $nin:[…] }} |
无字段或数组不含列表中任一项(注意缺字段语义) |
| 标量元素比较 | { lessons:{ $gt:2 } } |
至少一个元素满足该比较 |
| 文档元素多条件 | { arr:{ $elemMatch:{ a:1,b:2 } } } |
存在一条子文档同时满足子条件 |
| 嵌套字段精确 | { "bag.index":1 } |
子文档字段精确匹配 |
MongoDB多条件查询
AND(隐式):多个字段同级条件- OR:
{ $or:[cond1,cond2] } - 复合:AND +
$or
db.users.find({ age:25, $or:[{name:"T1"},{name:"T2"}] })
| 查询类型 | 语法 | 示例 |
|---|---|---|
| AND | { a:1, b:2 } |
同时满足 a=1 且 b=2 |
| OR | { $or:[{a:1},{b:2}] } |
a=1 或 b=2 |
| 组合查询 | { c:3, $or:[…] } |
c=3 且 (a=1 或 b=2) |
MongoDB指定字段查询
- Projection:第二参数
{ field:1 }包含,{ field:0 }排除 - **默认返回
_id**,可显式{ _id:0 } - 支持嵌套字段:
{ "bag.index":1 }
db.users.find({}, { name:1, age:1, _id:0 })
| 方式 | 语法 | 说明 |
|---|---|---|
| 包含字段 | { name:1, age:1 } |
仅返回 name 和 age |
| 排除字段 | { age:0 } |
返回所有字段但不包括 age |
排除 _id |
{ _id:0 } |
不返回 _id |
MongoDB模糊查询
- **正则
$regex**:字符串模式匹配 - 忽略大小写:
$options:"i" - 常用模式:
^、$、.、*、[]、|
db.users.find({ name:{ $regex:"^T", $options:"i" } })
| 场景 | 语法 | 说明 |
|---|---|---|
| 包含子串 | { name:{ $regex:"Ta" }} |
匹配包含 “Ta” |
| 开头匹配 | { name:{ $regex:"^T" }} |
以 T 开头 |
| 结尾匹配 | { name:{ $regex:"2$" }} |
以 2 结尾 |
| 忽略大小写 | { name:{ $regex:"t", $options:"i" }} |
不区分大小写 |
MongoDB排序查询
.sort({ field:1|-1 }):1 升序,-1 降序- 支持多字段:按字段顺序依次排序
db.users.find().sort({ age:1, name:-1 })
| 排序方式 | 语法 | 说明 |
|---|---|---|
| 单字段升序 | { age:1 } |
age 从小到大 |
| 单字段降序 | { age:-1 } |
age 从大到小 |
| 多字段排序 | { age:1, name:-1 } |
age 再 name |
MongoDB分页查询
skip(n).limit(m):跳过 n,取 m 条- 游标分页:
{ _id:{ $gt:lastId }}+limit - 排序分页:结合
.sort()+ skip/limit
db.users.find().skip(10).limit(5)
db.users.find({ _id:{ $gt:objId } }).limit(5)
| 方法 | 优缺点 | 用例 |
|---|---|---|
| skip/limit | 简单;大量 skip 性能差 | 小数据量分页 |
| 游标分页 | 高效;依赖递增键;不支持随机页 | 大数据集连续翻页 |
| 排序分页 | 指定顺序,结合 skip/limit 使用 | 需按特定字段分页 |
MongoDB聚合查询概念
- Aggregation Pipeline:
aggregate([ 阶段1, 阶段2, … ]),上一阶段输出即下一阶段输入。 - 每阶段输入→处理→输出,可组合成统计、清洗、联表式多步变换。
- 典型阶段:
$match、$group、$sort、$skip、$limit、$project等。 - 习惯:尽量把
$match靠前,先减掉文档量,再分组排序,省 CPU 与内存。
MongoDB过滤文档聚合查询
$match:在管道中做过滤,查询算子与find同一套,可接$group、$project等。- 与
find的差别:在管道里只是一阶段,后面还能继续变形;尽量靠前以缩小后续工作集。
db.collection.aggregate([ { $match:{ age:{ $gt:25 } } } ])
| 操作符 | 用途 | 示例 |
|---|---|---|
$match |
文档过滤 | { $match:{ status:"A" } } |
MongoDB分组聚合查询
$group:类似 SQLGROUP BY_id定义分组键;写_id: null时表示全表聚成一组(全员汇总)。- 常用累加器:
$sum、$avg、$max、$min、$first、$last、$push、$addToSet $first/$last:取组内第一条或最后一条的字段值,依赖进入$group时的文档顺序,要「谁算第一」通常先$sort。
db.sales.aggregate([
{ $group:{ _id:"$item", total:{ $sum:"$quantity" } } }
])
| 累加器 | 功能 | 示例 |
|---|---|---|
$sum |
求和 | { totalQty:{ $sum:"$qty" } } |
$avg |
平均值 | { avgPrice:{ $avg:"$price" } } |
$max/$min |
组内最值 | { hi:{ $max:"$score" } } |
$push |
收集为数组 | { allQty:{ $push:"$qty" } } |
$addToSet |
收集去重集合 | { ids:{ $addToSet:"$userId" } } |
$first / $last |
组内首条/末条字段 | 常配合前置 $sort 才有业务含义 |
MongoDB排序聚合查询
$sort:管道内排序,多键顺序与find().sort()相同(先第一键,再第二键)。- 性能:大数据量、无索引支撑时排序昂贵;管道默认有单阶段内存上限(常见为 100MB 量级),超标可报错。
allowDiskUse: true:允许排序等阶段溢出到磁盘,避免一味加大内存(见正文aggregate(..., { allowDiskUse: true }))。
db.students.aggregate([ { $sort:{ score:-1, age:1 } } ])
| 排序符号 | 含义 |
|---|---|
1 |
升序 |
-1 |
降序 |
MongoDB分页聚合查询
$skip+$limit:管道内分页;大skip与find一样贵,深分页仍优先考虑排序键游标或改产品形态。- 顺序:要稳定顺序时 **
$sort→$skip→$limit**;可与$project组合先裁字段再取页(见正文)。
db.students.aggregate([
{ $sort:{ age:1 } },
{ $skip:5 },
{ $limit:5 }
])
| 阶段 | 描述 |
|---|---|
$skip |
跳过指定文档数 |
$limit |
限制返回文档数 |
MongoDB索引
- 读写权衡:索引加速过滤与排序,但每次写入要维护所有相关索引,索引越多写路径越重。
- 按访问模式建:先统计真实查询里的等值、范围、排序、唯一性,再决定单字段、复合或专用索引,避免盲建闲置索引。
- 部分与稀疏:同一条索引上
sparse与partialFilterExpression不能同时指定;部分索引还要求查询条件能蕴含partialFilterExpression,否则规划器可能不用(见第 29 篇)。
索引概念
- 常见单字段/复合索引底层为 B-tree,便于等值与范围;无可用索引时多退化为集合扫描(COLLSCAN)。
- 相对全表逐个文档比对,命中高选择性索引时访问路径通常从 O(n) 量级降到对数级为主,仍与数据分布、选择性有关。
- 每条索引占存储与缓存;插入/更新/删除会同步更新索引条目(正文对维护开销有展开)。
单字段索引
- 针对单个字段创建升序(
1)或降序(-1) - 语法:
db.coll.createIndex({ field: 1 });第二参数可设name自定义索引名 - 列举索引:
db.coll.getIndexes();删除:db.coll.dropIndex("索引名")或传入 key 文档(与正文一致) _id默认唯一索引;覆盖查询时若过滤与投影字段均落在同一索引内,可减少回表读文档
复合索引
- 多字段组合索引,字段顺序决定能否走索引:只查
b不含a通常用不上{a,b}。 - 语法:
db.coll.createIndex({ a:1, b:-1 });排序方向若与查询sort一致,更易避免额外排序。 - 前缀示例:索引
{a,b,c}可服务{a}、{a,b}、{a,b,c}等左起连续条件(与第 23 篇一致)。
文本索引
- 社区版典型路径:在查询条件里用
$text,内嵌$search指定关键词(如{ $text: { $search: "MongoDB" } },见第 24 篇);勿与 MongoDB Atlas Search 聚合管道里的$search阶段混为一谈。每个集合通常只允许一个 text 索引(多字段可合在同一 text 索引里)。 - 语法:
db.coll.createIndex({ title:"text", content:"text" }) - 短语、排除词、
textScore排序、权重、多语言等见第 24 篇
哈希索引
- 对字段值做哈希,仅利于等值查找
- 语法:
db.coll.createIndex({ key:"hashed" }) - 不支持范围查询;不能按哈希序做有序遍历,排序字段若只有哈希索引往往用不上(见第 25 篇)
地理空间索引
- 2d:平面坐标(XY)索引;2dsphere:球面(经纬度)索引
- 语法:
db.coll.createIndex({ loc:"2dsphere" }) - 支持
$near、$geoWithin、$centerSphere等地理查询
稀疏索引
- 仅索引包含该字段的文档,忽略字段缺失文档
- 语法:
db.coll.createIndex({ f:1 }, { sparse:true }) - 节省空间,适合字段偶尔存在场景
- 与唯一索引结合可对部分文档做唯一约束
唯一索引
- 保证单字段或复合键组合唯一;集合内已存在重复值时建唯一索引会失败,需先清洗。
- 语法:
db.coll.createIndex({ email:1 }, { unique:true })
部分索引
partialFilterExpression决定哪些文档进入索引;与sparse不可写在同一条索引。- 语法:
db.coll.createIndex({ f:1 }, { partialFilterExpression:{ status:"A" } }) - 查询若与过滤式无关,可能不走该部分索引,需为其他访问路径另建索引。
操作对比
| 索引类型 | 创建语法 | 适用场景 | 支持特点 | 限制 |
|---|---|---|---|---|
| 单字段索引 | { field:1 } |
单字段查询、排序 | 覆盖索引 | 写入开销、所有文档均有索引条目 |
| 复合索引 | { a:1, b:-1 } |
多字段筛选与排序 | 前缀匹配 | 字段顺序敏感 |
| 文本索引 | { f:"text", g:"text" } |
全文搜索、短语、相关性排序 | 多字段、权重、多语言、停用词 | 字段类型限于字符串 |
| 哈希索引 | { field:"hashed" } |
精确值查询 | 等值查找高效 | 不支持范围/排序 |
| 地理空间索引 | { loc:"2d" } / { loc:"2dsphere" } |
位置搜索、地图应用 | 支持多种地理操作符 | 数据格式需 GeoJSON 或平面坐标 |
| 稀疏索引 | { f:1 }, { sparse:true } |
字段偶尔存在、减少无用索引条目 | 只索引存在字段的文档 | 不索引缺失字段文档 |
| 唯一索引 | { f:1 }, { unique:true } |
防止重复值 | 单字段或复合字段唯一性 | 创建前需清理重复数据 |
| 部分索引 | { f:1 }, { partialFilterExpression: {...} } |
特定条件优化 | 灵活过滤条件 | 仅对符合条件文档索引 |
MongoDB更新
- 选型口诀:改几个字段用
updateOne/updateMany+ 操作符;整份文档换成新形状用replaceOne;条件写错时updateOne只动第一条,批量误伤用updateMany更危险。 arrayFilters:更新嵌套数组里满足子条件的元素时用占位符$[标识],并在选项里写arrayFilters(见第 31 篇)。
首条更新 (updateOne)
- 针对匹配的第一条文档执行更新
- 使用更新操作符:
$set(设置/新增字段)、$inc(增减数值)、$unset(删除字段)等 - 支持
upsert:true(无匹配时插入新文档;查询里带$gt等操作符的字段不会原样进新文档,见第 30 篇) - 常见选项:
writeConcern(写入确认)、collation(排序规则) - 宽条件 +
updateOne易改错行,先 find 再改或加唯一键过滤
多条更新 (updateMany)
- 针对所有匹配文档批量执行更新
- 第二参数须为操作符文档或管道更新,语义同 shell 要求,不要与
replaceOne的整文档替换混用 - 支持
upsert、arrayFilters、hint、collation、writeConcern - 适用于:批量状态修改、计数器更新、数据清理
替换更新 (replaceOne)
- 用完整新文档替换匹配的第一条文档;新文档里没出现的字段会从原文档消失
- 替换文档中的
_id一般应与被替换文档一致;不能带$set等更新操作符 - 选项支持
upsert、collation、writeConcern - 适用于:外部系统推来整包 DTO、需要文档形态与 schema 完全对齐时
操作对比
| 操作 | 更新范围 | 使用方式 | 文档保留 | 支持 Upsert | 覆盖方式 |
|---|---|---|---|---|---|
| updateOne | 第一条匹配文档 | 使用更新操作符(如 $set, $inc) |
只改动指定字段 | 是 | 局部字段更新 |
| updateMany | 所有匹配文档 | 同 updateOne |
只改动指定字段 | 是 | 局部字段更新 |
| replaceOne | 第一条匹配文档 | 提供完整替换文档(无更新操作符) | 删除未包含字段 | 是 | 整个文档被新文档覆盖 |
MongoDB删除
deleteOne
- 删除匹配的第一条文档
- 语法:
db.collection.deleteOne(filter, { collation }) - 不返回被删文档
- 只会删除第一个符合条件的,操作不可撤销
deleteMany
- 删除所有匹配文档
- 语法:
db.collection.deleteMany(filter, { collation }) - 不返回被删文档
{}匹配全集,等同清空集合,执行前务必二次确认
findOneAndDelete
- 查找并删除第一条匹配文档,同时返回它
- 语法:
db.collection.findOneAndDelete(filter, { sort, projection, collation }) - 返回被删文档,便于后续处理
- 可配合
sort决定优先删除哪条
drop
- 删除整个集合及其索引
- 语法:
db.collection.drop() - 一次性移除集合,无法恢复
remove (弃用)
- 旧版删除方法,不推荐
- 语法:
db.collection.remove(filter, { justOne }) - 功能已被
deleteOne/deleteMany取代
操作对比
| 操作 | 删除范围 | 返回被删文档 | 典型场景 |
|---|---|---|---|
| deleteOne | 第一条匹配文档 | 否 | 删除某个条件下的单条记录 |
| deleteMany | 所有匹配文档 | 否 | 批量清理符合条件的数据 |
| findOneAndDelete | 第一条匹配文档 | 是 | 删除并需要获取被删文档(审计/日志) |
| drop | 整个集合 | 否 | 清理整个集合及其索引 |
| remove (弃用) | 单条或多条(取决于 justOne) |
否 | 旧版代码兼容,建议迁移至新 API |
MongoDB联动C#
C#操作MongoDB
驱动安装
- 推荐通过 NuGet 安装
MongoDB.Driver,升级更便捷 - 也可从 GitHub 获取源代码,适合自定义需求
连接与模型
MongoClient("mongodb://…")全进程复用单例,线程安全;不要每次请求new一个GetDatabase、GetCollection<T>映射库与集合;强类型用User等 POCO + 特性,动态或异构结构用GetCollection<BsonDocument>(见第 35 篇)[BsonId]、[BsonRepresentation(BsonType.ObjectId)]、[BsonElement("name")]等与 BSON 字段对齐
CRUD 操作
| 操作 | API | 说明 |
|---|---|---|
| 插入 | InsertOne / InsertMany |
同步插入单个或多个文档 |
| 查询 | Find(filter).ToList() |
支持 Lambda 表达式和 Builders |
| 更新 | UpdateOne(filter, update) |
$set、$inc 等操作符,支持 upsert |
| 替换 | ReplaceOne(filter, replacement) |
整文档替换 |
| 删除 | DeleteOne / DeleteMany |
精确条件删除单条或多条 |
| 索引 | Indexes.CreateOne(new CreateIndexModel…) |
为常用查询字段创建 B-Tree 索引 |
使用建议
- 为高频查询字段建索引
- 整个应用复用单个
MongoClient - 捕获
MongoException并记录,保证健壮性
C#使用Bson操作MongoDB
BSON 文档构建与插入
var doc = new BsonDocument { … }支持嵌套BsonDocument、BsonArraycollection.InsertOne(doc)
BSON 查询与筛选
var all = collection.Find(new BsonDocument()).ToList();
var filtered = collection.Find(new BsonDocument("age", new BsonDocument("$gt", 25))).ToList();
BSON 更新与删除
| 操作 | 调用示例 |
|---|---|
| 更新 | collection.UpdateOne( new BsonDocument("name","John"), new BsonDocument("$set", new BsonDocument("age",35)) ) |
| 删除 | collection.DeleteOne( new BsonDocument("name","John") ) |
BSON 索引
collection.Indexes.CreateOne(new CreateIndexModel<BsonDocument>(new BsonDocument("name",1)));
要点
- 直接操作
BsonDocument,更灵活但少了类型安全 - 适合动态结构或快速原型
- 与强类型 API 可混合使用,按需选择
36.2 面试题精选
基础题
1. 关系库和文档库/键值库:「最小存储单元」分别长什么样?
题目
各用一句定义「一行/一条文档/一个 KV」在模式与查询上的含义,并各举一个代表产品;最后补一句:选型时除了模型还要看哪两类硬约束。
深入解析
- 考察点:是否能把「表行 + 固定列」与「嵌套文档 / 裸 KV」区分开,而不是背「NoSQL 快」这类口号。
- 关系型:最小单元是满足同一 Schema 的一行;关联靠外键与 join;典型 MySQL、PostgreSQL。
- 文档型:最小单元是可变形状的 BSON/JSON 文档,嵌套数组与子文档是一等公民;典型 MongoDB、Couchbase。
- 键值型:最小单元是
key → value,value 对引擎往往是透明 blob,复杂条件查询弱;典型 Redis、DynamoDB。 - 列式 / 图:列族按列组织、适合分析扫少列;图用点边——追问时常用来区分「是不是只会文档和 Redis」。
- 选型硬约束:一致性与事务边界(能不能接受最终一致、要不要跨文档事务);访问模式(等值、范围、排序、报表、全文)是否匹配引擎能力。
答题示例
核心结论:关系型按「固定列的行」存;文档型按「一棵 JSON 树」存;键值型按「一个 key 对应一块值」存。
展开:MySQL 这类库用表约束把事实拆开;MongoDB 把常一起读的对象嵌在一个文档里;Redis 适合按 key 命中、别指望复杂条件查询。
收口:还要看业务要不要强事务与复杂关联,以及真实查询是不是文档模型兜得住;产品名字不如这两条实在。
参考文章
- 1.SQL和NoSQL
- 2.NoSQL种类和特点优点
2. ACID 四个字母各指什么?转账场景里分别挡的是哪类事故?
题目
先按顺序说出英文全称对应的中文习惯译法;再各用半句话说明「如果没有这一条,转账会出什么烂摊子」。最后补一句:MongoDB 能不能说「完全没事务」?
深入解析
- A(Atomicity,原子性):事务内操作要么全部提交要么全部回滚;烂摊子示例:扣款成功、入账失败,余额对不上。
- C(Consistency,一致性):事务前后数据库满足约定约束与业务规则(含应用层规则);烂摊子:违反唯一约束、负库存仍可见等「非法中间态」被当成结果。
- I(Isolation,隔离性):并发事务互相隔离,隔离级别决定能读到多「脏」的中间态;烂摊子:未提交读导致重复扣款决策、统计读到半成品。
- D(Durability,持久性):已提交写入在故障后仍可恢复;烂摊子:确认支付后进程崩溃,钱款记录消失。
- MongoDB 边界:多文档事务在较新版本与限定条件下存在,且与副本集、读写关注级别相关;笼统说「NoSQL 没事务」容易在追问里翻车,应改成「默认叙事偏最终一致,事务能力与 SQL 不在同一档位」。
答题示例
核心结论:A 原子性、C 一致性、I 隔离性、D 持久性。
转账口述:原子性防「只扣不加」;一致性防破坏余额规则;隔离性防并发互相读到半截流水;持久性防提交后宕机丢账。
Mongo 一句:别一口咬死「文档库没事务」——有场景支持多文档事务,但和银行核心常用的关系型事务能力不能画等号,要说清版本与范围。
参考文章
- 1.SQL和NoSQL
3. ObjectId 的 12 字节怎么切?为什么说 _id 排序只有「大致」时间序?
题目
按顺序说出四段各占几字节、各干什么;再解释:同一秒内批量插入时,仅靠 _id 排序能否严格等价于业务上的「创建时间」;最后说工程上该用什么字段做报表时间。
深入解析
- 结构:
4+3+2+3字节——秒级 Unix 时间戳、机器标识、进程 ID、同秒内递增计数器。组合目标是在分布式、多进程下降低碰撞概率。 - 「大致」原因:排序键的前缀是秒级时间,整体按字节序比较时大体随插入时间增长;同一秒内排序由后 8 字节决定,不保证与真实插入的纳秒级先后一致,更不能当单调时钟。
- 业务误区:把
_id当「权威下单时间」或跨系统对齐的时间源;时钟回拨、批量导入、客户端预生成ObjectId都会让「时间序」叙事不成立。 - 工程做法:业务时间用独立字段(如
createdAt),必要时 TTL 索引也用业务语义明确的时间字段,而不是赌ObjectId。
答题示例
结构:4 字节时间戳、3 字节机器、2 字节进程、3 字节计数器,一共 12 字节。
大致:按
_id排大多能反映先后顺序,但精度到秒,同一秒内谁前谁后看后缀;不能把_id当精确创建时间或法律依据时间。落地:报表、对账、运营统计用明确的
createdAt之类字段;埋点、战斗日志若批量写入,更不要用_id代替业务时间戳。
参考文章
- 7.MongoDB的ObjectID
4. 复合索引 { a: 1, b: 1 }:什么叫最左前缀?哪种查询「以为能走其实常走不上」?
题目
列出至少两种能用到该索引的过滤形态;再举一个不能靠它前缀匹配的典型 filter;最后说要用什么命令验证,以及排序在什么情况下会白建索引。
深入解析
- 最左前缀:B-tree 复合索引按字段顺序组织;查询条件需从最左字段起形成有效前缀,才能用到索引的左半截。
- 能用的例子:
{ a: 1 }、{ a: 1, b: 2 }、{ a: { $gte: 1 }, b: … }(具体能否走索引仍看规划器与选择性)。 - 典型反例:仅
{ b: 1 }或仅对b排序且无a等值/范围锚点——通常无法用{a,b}的前缀(除非另有单字段索引或覆盖特殊场景,不能赌)。 - 排序陷阱:
sort({ b: 1 })且无a条件时,往往无法利用{a,b}避免内存排序;与find同理,顺序是设计出来的,不是「建了就会用」。 - 验证:
explain("executionStats")看winningPlan,盯IXSCAN/FETCH/SORT阶段,避免凭感觉答题。
答题示例
最左前缀:索引键从左到右用全了才叫吃到前缀;
{a,b}先服务带a的条件。能用:
a定值再查b,或只约束a。易错:只查
b、或只对bsort,却指望{a,b}万能——通常不行。验证:用
explain看是否IXSCAN、有没有额外SORT;线上问题十有八九是条件和索引顺序没对齐。
参考文章
- 23.MongoDB复合索引
进阶题
1. 垂直扩展与水平扩展各是什么?和 SQL / NoSQL 的常见画像怎么对应?
题目
各用一句话定义两种扩展;各说一个上限或成本痛点;最后举一类典型混合架构(哪类数据放 SQL、哪类放文档或缓存),说明理由。
深入解析
- 垂直扩展(scale up):更强单机 CPU、内存、磁盘、IO;痛点是硬件天花板、单点故障、大规格迁移窗口与许可成本。
- 水平扩展(scale out):加节点,通过副本、分片、缓存层分摊读写;痛点是运维与一致性模型复杂、跨分片事务与 join 更痛。
- 与 SQL / NoSQL 画像:关系型常与强一致、复杂查询绑定,历史路径上先榨干单机再做读写分离;文档、键值、宽列产品形态更常从一开始就为分片与海量写入设计(具体仍以产品为准)。
- 混合架构:支付、库存、账号核心表在 SQL;玩家存档、行为日志、配置快照在 MongoDB;Session、排行榜、限流在 Redis——用一致性要求与访问模式切边界,而不是「全盘 NoSQL」。
答题示例
定义:垂直扩展是把一台机器加粗;水平扩展是多台机器一起扛。
痛点:垂直扩展撞到单机天花板和迁移成本;水平扩展换运维复杂度和数据分布设计。
选型口语:强一致、复杂报表的核心表常见先垂直 + 读写分离;用户量与写入顶穿单机时,半结构化业务数据更常走分片文档库,热点再叠 Redis。
混合例子:充值、扣钻走 SQL;存档、战斗回放元数据走 Mongo;排行榜底表走 Redis——各层一致性要求不一样。
参考文章
- 1.SQL和NoSQL
2. 键值、列式、文档三类:各举一个「最划算」的负载,再各举一个「明显别选」的反例。
题目
九句话内答完(每类三句:适合 / 不适合 / 代表产品任选其一);追问准备:列式为什么不适合高并发小行更新?
深入解析
- 键值:划算在 Session、缓存、计数器、排行榜、特征向量按 key 覆盖;不划算在多维条件检索、报表、复杂关联;代表 Redis、Memcached、DynamoDB。
- 列式:划算在 OLAP、数仓、扫海量行只取少数列;不划算在 OLTP 式高频随机更新与小事务;代表 HBase、Cassandra、Bigtable。
- 文档:划算在用户资料、内容、订单快照、游戏存档 JSON;不划算在多表星型模型报表、强依赖跨文档复杂 join 且不愿冗余;代表 MongoDB、CouchDB、Couchbase。
- 追问根因:列式存储按列压缩与批量读优化,行级反复改写会破坏压缩与存储局部性,写入路径与关系型行存完全不同。
答题示例
键值:按 key 读写极快,适合缓存、计数器;复杂查询别硬上;Redis 最常被点到。
列式:扫几列分析、报表香;当高并发订单库会写爆;HBase、Cassandra 那一路。
文档:业务对象是树状 JSON 时顺手;要星型报表、到处 join 不如关系型;MongoDB 是常见代表。
收口:先问读写模式与一致性,再选存储模型;没有「一种 NoSQL 吃天下」。
参考文章
- 2.NoSQL种类和特点优点
3. 聚合管道 aggregate([...]) 在模型上是什么?$match 为什么要尽量靠前?
题目
用「数组 + 阶段」两句话讲清管道语义;举一条典型阶段链(至少四个阶段名);再从「文档数、CPU、内存上限」三个角度解释提前 $match 的收益。
深入解析
- 模型:
aggregate接收阶段数组,每个阶段消费上游文档流、输出新流;顺序有语义,不是无序集合。 - 与
find关系:$match使用的查询算子与find同族,但在管道里只是一段,后面还可$lookup、$facet等继续变形。 - 提前过滤收益:后续
$group、$sort、$lookup处理的文档数下降,降低 CPU、内存与磁盘溢出概率;大集合上这是性能分水岭。 - 内存边界:排序、分组等阶段受内存限制,超标需
allowDiskUse或改索引与管道顺序;把$match顶在前面是「先瘦身再折腾」的基本功。 - 骨架记忆:
$match→$group/$sort→$skip/$limit→$project,按题裁剪。
答题示例
一句话模型:
aggregate里是流水线,上一阶段的输出文档进下一阶段。典型链:
$match过滤 →$group统计 →$sort排序 →$limit取 TopN,中间可插$project裁剪字段。为什么要早 match:先扔掉无关文档,后面分组排序的数据量小,省 CPU、省内存;管道阶段有内存门槛,先瘦身再聚合是常规答法。
参考文章
- 16.MongoDB聚合查询概念
- 17.MongoDB过滤文档聚合查询
4. 深分页里 skip 为什么贵?用 _id 做游标分页时 filter 怎么写?
题目
用「引擎仍要扫描/丢弃多少文档」说清 skip 的复杂度直觉;写出一类基于 _id Strictly Increasing 假设的下一页查询形态(可用伪代码);最后点出并发插入删除下产品侧要接受什么现象。
深入解析
skip成本:即使只返回一页,skip(n)仍要遍历并丢弃前n条匹配文档;n增大时 CPU 与 IO 近似线性变差,索引也只能减轻而不能魔法消除「跳过」本身。- 游标分页思路:记住上一页最后一条的
_id(或复合排序键),下一页用大于该键的范围条件接力limit,避免重复skip。 - 伪代码形态:
find({ _id: { $gt: lastId } }).sort({ _id: 1 }).limit(pageSize);若按业务字段排序,需用「排序键 +_id」组成稳定排序键防重复。 - 并发副作用:插入可能导致「跳读」或「重复」;删除可能导致当前页条数不足;需产品容忍或引入版本号、快照类设计。
答题示例
skip:数据库仍然要把前面 N 条走过一遍再扔掉,N 越大越像自虐。
游标:记住这页最后一条的
_id,下一页用{ _id: { $gt: lastId } }接龙,再sort+limit。稳定排序:如果按时间排,常用「时间 +
_id」双键,避免同毫秒冲突。并发:边翻边写会跳行或重复,产品要接受或做补偿,别假装静态集合。
参考文章
- 15.MongoDB分页查询
深度题
1. 别背 CAP:用「读到的后果」区分强一致与最终一致,各举一例。
题目
各举一个必须强一致的业务(说明错一致模型会怎样);再举一个可最终一致的业务(说明能容忍多长窗口、错模型主要损什么);最后一句落到「怎么跟产品对齐指标」。
深入解析
- 强一致叙事:用户读到的就是已提交的最新事实;错用最终一致在余额、库存、道具扣减、防重复支付上可能造成资损、超卖、重复发货,属于 P0 级事故。
- 最终一致叙事:短时间读到旧值,但在可接受时间内收敛;点赞数、在线人数、推荐列表、非关键排行榜,错模型多是体验问题,很少直接等价于资金错误。
- 落地方法:与产品/风控对齐「可接受的不一致窗口、是否允许读到旧值、失败是否可补偿」;再决定缓存、读写分离、事务、幂等与对账——用后果表驱动架构,而不是先选 buzzword。
- 追问防守:能说出「强一致不等于只用 SQL」「最终一致也要幂等与监控」,分数会明显高于空喊 CAP。
答题示例
强一致例子:点券、付费货币、限量道具库存;读到旧库存会超卖,读到旧余额会重复消费,这是钱的问题。
最终一致例子:公会点赞数、世界频道热度、推荐列表;几秒旧一点能接受,主要损体验。
对齐方式:先问「错读一秒会不会赔钱」,再定缓存与副本读;能赔偿与对账的业务才敢放宽。
参考文章
- 1.SQL和NoSQL
2. 关系型范式化 vs 文档嵌套与冗余:从读、写、改模式各说一个权衡。
题目
各答三点——读路径(几次往返、是否 join)、写一致性(一份事实更新几处)、模式演进(加字段/拆表的代价);最后用游戏业务举一个「拆表合理」与一个「嵌套合理」的例子。
深入解析
- 关系型:读路径通过 join 拼视图,索引设计成熟时报表强;写一致性单点更新清晰;模式演进靠迁移脚本,强约束变更成本高但边界清楚。
- 文档型:读路径可把热点聚合嵌在一个文档,减少往返与跨分片 join;写一致性要处理冗余字段同步、部分更新与文档体积膨胀;模式演进灵活但对团队纪律要求高(版本字段、兼容读写)。
- 常见追问:「MongoDB 为什么不全放一个集合」——单文档 16MB 上限、索引与写放大、热点行竞争、跨实体生命周期不同,都是拆文档边界理由。
- 游戏侧例:账号安全、支付流水适合关系型或强约束存储;单局内装备列表、天赋树快照适合嵌套文档;全服邮件模板与玩家邮箱列表常拆集合并受控冗余。
答题示例
关系型:读靠 join,写改一行是一处事实;改表要迁移,但约束清楚。
文档型:读把一次界面要用的嵌在一起,写要管冗余是否同步;加字段灵活,但要约定兼容策略。
游戏例:充值订单、风控流水适合 SQL;单局背包、技能树嵌在玩家文档里更顺;别把全服配置和每个玩家存档塞进同一个大文档。
参考文章
- 1.SQL和NoSQL
- 2.NoSQL种类和特点优点
3. 开发机一直用 bindIp: 127.0.0.1,上线要对外开放:风险链怎么讲?最小加固清单列哪几项?
题目
按「监听地址 → 网络可达性 → 认证与账号 → 秘密管理」四层口述;每层各说一个具体失误(例如绑到 0.0.0.0 且未开 authorization);清单不少于五项,其中必须包含防火墙/安全组与独立业务账号。
深入解析
- 监听与暴露:
127.0.0.1仅本机回环;改为0.0.0.0或公网 IP 即扩大监听面,需与安全组、VPC、iptables 同步收紧,否则等于在公网吆喝端口。 - 匿名访问:未启用
authorization时,能连上的人往往能读写;内网误配路由或 SSRF 时同样危险。 - 账号与最小权限:管理员与普通业务库用户分离;应用连接串使用仅必要权限的账号,避免 root 写进仓库或配置中心明文扩散。
- 秘密与传输:密码轮换、TLS(若跨公网或合规要求)、审计日志与备份可用性同属「上线检查表」。
- 配置面:
net、security、systemLog、storage要一起看,只改端口不改认证是常见事故模板。
答题示例
风险链:先看清监听的是本机还是全网;再看过防火墙有没有洞;再看有没有开认证;最后看连接串里是不是管理员账号裸奔。
典型失误:
0.0.0.0+ 无认证 + 公网安全组全开,等于送库。最小清单:只监听内网网卡;安全组白名单收口;
authorization: enabled;业务独立账号最小权限;强密码与轮换;日志与备份可用;连接串不进 Git。
参考文章
- 4.MongoDB配置文件
- 6.MongoDB连接和启用认证
4. 稀疏索引与部分索引各解决什么数据形态?为何不能在同一条索引上同时写 sparse 与 partialFilterExpression?
题目
各用「哪些文档会进索引」一句话定义;各举一个查询侧注意点($exists、与 partial 谓词对齐);最后用「语义重叠 / 规划器复杂度」解释禁混开,并点一句部分索引要配合什么样的 filter 才容易被选中。
深入解析
- 稀疏
sparse:索引字段缺失的文档不进入索引;适合字段可有可无、查询常与$exists搭配的场景;与唯一索引组合可实现「仅对有字段文档唯一」一类需求(仍以官方语义为准)。 - 部分
partialFilterExpression:仅满足谓词的文档进入索引,可表达比「字段存在」更细的业务子集(如status: "active");常与唯一约束组合做「子集唯一」。 - 查询匹配:部分索引要求过滤条件能蕴含或一致于
partialFilterExpression,否则规划器可能放弃该索引;这是面试里区分「建了索引」与「会用索引」的细节。 - 禁混开原因:两者都在缩小索引中文档子集,语义重叠,组合规则复杂;服务端直接拒绝同开,强迫二选一,通常 partial 表达力更强。
- 治理:避免为同一访问路径重复堆叠多条「半索引」,定期用
explain与慢查询核对。
答题示例
稀疏:没有该字段的文档不进索引,适合可选字段。
部分:只把满足条件的文档放进索引,比如只要活跃用户的索引。
查询侧:查条件要和 partial 的谓词对得上,否则可能根本不用这条索引。
禁混开:两个开关都在「缩小谁进索引」,叠在一起语义重复,官方直接不让配;一般优先 partial。
落地:建完用
explain验证,别假设「建了就会走」。
参考文章
- 27.MongoDB稀疏索引
- 29.MongoDB部分索引
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com