一篇面向实战的 NoSQL 博客:从 安装与服务 、Shell CRUD 、Mongoose ODM ,到 记账本 Express 项目 的完整链路。示例可独立运行,不依赖外部讲义路径。
官方文档:mongodb.com/docs | Mongoose:mongoosejs.com/docs | 中文社区:mongoosejs.net
目录
- 零、导读与学习价值
- [0.1 案例覆盖清单](#0.1 案例覆盖清单)
- [0.2 核心名词速查](#0.2 核心名词速查)
- [0.3 为什么要学本篇](#0.3 为什么要学本篇)
- 导读:知识架构与权威参考
- 本文解决什么问题
- 知识脉络(Mermaid)
- 权威文档
- [与 Express / LowDB 的衔接](#与 Express / LowDB 的衔接)
- [0. 安装、服务与工具](#0. 安装、服务与工具)
- [0.1 安装与启动服务](#0.1 安装与启动服务)
- [0.2 命令行 Shell 与 GUI 工具](#0.2 命令行 Shell 与 GUI 工具)
- [0.3 关系型数据库 vs MongoDB(名词归纳)](#0.3 关系型数据库 vs MongoDB(名词归纳))
- [1. MongoDB 核心概念解析](#1. MongoDB 核心概念解析)
- [1.1 什么是 MongoDB?](#1.1 什么是 MongoDB?)
- [1.2 MongoDB 数据模型](#1.2 MongoDB 数据模型)
- [1.3 BSON 数据类型详解](#1.3 BSON 数据类型详解)
- [1.4 BSON 与 ObjectId 底层原理](#1.4 BSON 与 ObjectId 底层原理)
- [2. MongoDB 架构设计原理](#2. MongoDB 架构设计原理)
- [2.1 存储引擎架构](#2.1 存储引擎架构)
- [2.2 索引机制](#2.2 索引机制)
- [2.3 深入存储引擎与索引原理](#2.3 深入存储引擎与索引原理)
- [3. 数据库操作实战](#3. 数据库操作实战)
- [3.1 数据库基本操作](#3.1 数据库基本操作)
- [3.2 集合操作详解](#3.2 集合操作详解)
- [3.3 文档操作(CRUD)深度解析](#3.3 文档操作(CRUD)深度解析)
- [3.4 查询运算符与逻辑条件(课堂对照)](#3.4 查询运算符与逻辑条件(课堂对照))
- [4. Mongoose ODM 深度应用](#4. Mongoose ODM 深度应用)
- [4.1 Mongoose 核心概念](#4.1 Mongoose 核心概念)
- [4.2 Mongoose 连接配置](#4.2 Mongoose 连接配置)
- [4.3 Schema 定义最佳实践](#4.3 Schema 定义最佳实践)
- [4.4 Mongoose 中间件机制](#4.4 Mongoose 中间件机制)
- [4.5 实例方法和静态方法](#4.5 实例方法和静态方法)
- [4.6 Mongoose 五步实战:01~05 脚本对照](#4.6 Mongoose 五步实战:01~05 脚本对照)
- [5. 高级查询与性能优化](#5. 高级查询与性能优化)
- [5.1 高级查询技巧](#5.1 高级查询技巧)
- [5.2 性能优化策略](#5.2 性能优化策略)
- [5.3 聚合管道执行原理](#5.3 聚合管道执行原理)
- [6. 生产环境最佳实践](#6. 生产环境最佳实践)
- [6.1 连接池管理](#6.1 连接池管理)
- [6.2 事务处理](#6.2 事务处理)
- [6.3 数据备份与恢复](#6.3 数据备份与恢复)
- [6.4 认证与安全连接](#6.4 认证与安全连接)
- [7. 实战项目:记账系统](#7. 实战项目:记账系统)
- [7.1 数据模型设计](#7.1 数据模型设计)
- [7.2 业务逻辑实现](#7.2 业务逻辑实现)
- [7.3 课堂记账本:Express + Mongoose 三步落地](#7.3 课堂记账本:Express + Mongoose 三步落地)
- [8. 性能监控与故障排查](#8. 性能监控与故障排查)
- [8.1 性能监控工具](#8.1 性能监控工具)
- [8.2 常见问题排查](#8.2 常见问题排查)
- [9. 核心案例速查与知识点归纳](#9. 核心案例速查与知识点归纳)
- [9.4 BSON 类型 HTML 演示](#9.4 BSON 类型 HTML 演示)
- [9.5 文档查询 HTML 演示](#9.5 文档查询 HTML 演示)
- [9.8 Mongoose 校验 HTML 演示](#9.8 Mongoose 校验 HTML 演示)
- 总结
零、导读与学习价值
0.1 案例覆盖清单
本篇知识点与下列全部练习一一对应,学完应能独立复现每一类操作(本地需已安装 MongoDB 与 Node):
| 模块 | 练习要点 | 本文对应章节 |
|---|---|---|
| 命令行 CRUD | show dbs、use、集合与文档增删改查、比较运算符 |
§0~§3 |
| Mongoose 脚本 01 | strictQuery、connect、open 内 Schema/Model、users 单条 create |
§4.6 ① |
| Mongoose 脚本 02 | songs 集合、data.json 的 song_list 批量 create |
§4.6 ② |
| Mongoose 脚本 03 | deleteOne / deleteMany |
§4.6 ④ |
| Mongoose 脚本 04 | updateOne / updateMany |
§4.6 ④ |
| Mongoose 脚本 05 | findOne、findById、select、sort、skip、limit、exec |
§4.6 ③ |
| 记账本 Express | bin/www 先连库再 listen、models/accounts、routes/account、EJS 四页、Bootstrap 日期选择器 |
§7.3 |
| 可运行 HTML | 文档过滤演示、BSON 类型对照 | §9.4~§9.5 |
0.2 核心名词速查
| 术语 | 一句话解释 |
|---|---|
| mongod | 数据库服务端进程,监听 27017 |
| mongosh | 官方 Shell 客户端,执行 db.xxx 命令 |
| database / collection / document | 库 → 集合 → 文档(BSON 对象) |
| ObjectId | 文档默认主键,12 字节唯一 |
| ODM | 在驱动之上用 Schema/Model 操作库,Mongoose 即 ODM |
| Schema / Model | 结构定义 → 构造函数,对应一个集合 |
| 聚合管道 | $match → $group → $sort 等阶段处理数据 |
| 连接池 | 进程内复用多条 TCP 连接,提高并发 |
0.3 为什么要学本篇
- 岗位:Node 全栈、后台接口、中台配置中心普遍要求 MongoDB + Mongoose。
- 工程:文档模型适合需求频繁变更的业务(商品、日志、用户画像);与 Express 组合是尚硅谷路线上的标准持久化方案。
- 衔接:Day11 记账本(LowDB)→ 本章(MongoDB)→ Day13(Session 多用户),形成完整 Web 应用链路。
导读:知识架构与权威参考
本文解决什么问题
| 阶段 | 你会掌握 | 典型产出 |
|---|---|---|
| 安装服务 | mongod、mongosh、数据目录 |
本地 27017 可连 |
| Shell CRUD | use、insertOne、find、updateOne |
命令行验证数据 |
| 运算符 | $gt、$in、$or、正则 |
条件查询 |
| Mongoose | Schema → Model → create/find |
Node 操作库 |
| 记账本 | models + routes + 先连库再起 HTTP |
持久化账单站 |
知识脉络(Mermaid)
#mermaid-svg-QNAp4XFSBp6HdfbN{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-QNAp4XFSBp6HdfbN .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-QNAp4XFSBp6HdfbN .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-QNAp4XFSBp6HdfbN .error-icon{fill:#552222;}#mermaid-svg-QNAp4XFSBp6HdfbN .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-QNAp4XFSBp6HdfbN .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-QNAp4XFSBp6HdfbN .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-QNAp4XFSBp6HdfbN .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-QNAp4XFSBp6HdfbN .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-QNAp4XFSBp6HdfbN .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-QNAp4XFSBp6HdfbN .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-QNAp4XFSBp6HdfbN .marker{fill:#333333;stroke:#333333;}#mermaid-svg-QNAp4XFSBp6HdfbN .marker.cross{stroke:#333333;}#mermaid-svg-QNAp4XFSBp6HdfbN svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-QNAp4XFSBp6HdfbN p{margin:0;}#mermaid-svg-QNAp4XFSBp6HdfbN .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-QNAp4XFSBp6HdfbN .cluster-label text{fill:#333;}#mermaid-svg-QNAp4XFSBp6HdfbN .cluster-label span{color:#333;}#mermaid-svg-QNAp4XFSBp6HdfbN .cluster-label span p{background-color:transparent;}#mermaid-svg-QNAp4XFSBp6HdfbN .label text,#mermaid-svg-QNAp4XFSBp6HdfbN span{fill:#333;color:#333;}#mermaid-svg-QNAp4XFSBp6HdfbN .node rect,#mermaid-svg-QNAp4XFSBp6HdfbN .node circle,#mermaid-svg-QNAp4XFSBp6HdfbN .node ellipse,#mermaid-svg-QNAp4XFSBp6HdfbN .node polygon,#mermaid-svg-QNAp4XFSBp6HdfbN .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-QNAp4XFSBp6HdfbN .rough-node .label text,#mermaid-svg-QNAp4XFSBp6HdfbN .node .label text,#mermaid-svg-QNAp4XFSBp6HdfbN .image-shape .label,#mermaid-svg-QNAp4XFSBp6HdfbN .icon-shape .label{text-anchor:middle;}#mermaid-svg-QNAp4XFSBp6HdfbN .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-QNAp4XFSBp6HdfbN .rough-node .label,#mermaid-svg-QNAp4XFSBp6HdfbN .node .label,#mermaid-svg-QNAp4XFSBp6HdfbN .image-shape .label,#mermaid-svg-QNAp4XFSBp6HdfbN .icon-shape .label{text-align:center;}#mermaid-svg-QNAp4XFSBp6HdfbN .node.clickable{cursor:pointer;}#mermaid-svg-QNAp4XFSBp6HdfbN .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-QNAp4XFSBp6HdfbN .arrowheadPath{fill:#333333;}#mermaid-svg-QNAp4XFSBp6HdfbN .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-QNAp4XFSBp6HdfbN .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-QNAp4XFSBp6HdfbN .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-QNAp4XFSBp6HdfbN .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-QNAp4XFSBp6HdfbN .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-QNAp4XFSBp6HdfbN .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-QNAp4XFSBp6HdfbN .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-QNAp4XFSBp6HdfbN .cluster text{fill:#333;}#mermaid-svg-QNAp4XFSBp6HdfbN .cluster span{color:#333;}#mermaid-svg-QNAp4XFSBp6HdfbN div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-QNAp4XFSBp6HdfbN .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-QNAp4XFSBp6HdfbN rect.text{fill:none;stroke-width:0;}#mermaid-svg-QNAp4XFSBp6HdfbN .icon-shape,#mermaid-svg-QNAp4XFSBp6HdfbN .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-QNAp4XFSBp6HdfbN .icon-shape p,#mermaid-svg-QNAp4XFSBp6HdfbN .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-QNAp4XFSBp6HdfbN .icon-shape .label rect,#mermaid-svg-QNAp4XFSBp6HdfbN .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-QNAp4XFSBp6HdfbN .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-QNAp4XFSBp6HdfbN .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-QNAp4XFSBp6HdfbN :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} mongod 服务
数据库
集合 collection
文档 document BSON
Node.js
Mongoose ODM
Express 路由
EJS 页面
【代码注释】(知识脉络图)
- 自下而上:先有 mongod 进程 监听 27017,才有库、集合、文档可读写。
- database → collection → document 三层与 MySQL 库表行类比,但文档无固定列。
- Node 通过驱动/Mongoose 访问文档;Express 路由只处理 HTTP,不直接操作 TCP。
- 课堂终点是 EJS 渲染 + MongoDB 持久化 的记账本,而非仅 Shell 操作。
权威文档
| 主题 | 链接 |
|---|---|
| MongoDB Manual | mongodb.com/docs/manual |
| mongosh | mongodb.com/docs/mongodb-shell |
| Node Driver | mongodb.com/docs/drivers/node |
| Mongoose | mongoosejs.com/docs |
| MongoDB University | university.mongodb.com |
与 Express / LowDB 的衔接
- Day11 记账本用 LowDB + JSON 文件 ;本章改为 MongoDB 持久化,路由与 EJS 模板结构可复用。
- 原则:数据库连接成功后再启动 HTTP 服务 (在
bin/www或入口里先mongoose.connect)。
0. 安装、服务与工具
0.1 安装与启动服务
名词解析:
- mongod:数据库守护进程,负责监听端口、读写数据文件。
- mongosh :现代 Shell 客户端(替代旧版
mongo命令),用于执行show dbs、db.users.find()等。 - dbpath:数据文件存放目录,不存在时服务启动失败。
Windows / macOS 安装要点:
- 社区版下载:MongoDB Community Download
- 默认端口 27017 ;可将
bin目录加入 PATH,全局使用mongod、mongosh。
启动服务:
bash
# 默认数据目录(当前盘符 /data/db,需提前创建)
mongod
# 指定数据目录与端口
mongod --dbpath /path/to/data/db --port 27017
【代码注释】
mongod是数据库服务端进程 ,负责监听端口、读写磁盘数据;与mongosh(客户端)不是同一个程序。- 默认数据目录因系统而异;macOS/Linux 常用
/data/db,Windows 安装程序可指定--dbpath。 --port 27017为默认端口;多实例开发时注意勿与 Docker 中 Mongo 端口冲突。- 前台运行时关闭终端或
Ctrl+C会终止服务;生产用 systemd、Windows 服务或 Docker 常驻。 - 启动失败常见原因:数据目录不存在、权限不足、端口被占用(
EADDRINUSE)。
0.2 命令行 Shell 与 GUI 工具
连接:
bash
# 本机无认证
mongosh
# 指定主机、库、用户(有认证时)
mongosh "mongodb://127.0.0.1:27017/mydb" -u user -p password
【代码注释】
- 无参数
mongosh连接本机127.0.0.1:27017,进入后提示符显示当前库名。 - URI 形式
mongodb://主机:端口/库名与 Node 中mongoose.connect使用同一规范。 -u/-p在启用--auth后必填;认证库常用?authSource=admin(见 §6.4)。- 连接失败:先确认
mongod已启动,再查端口占用与防火墙。
bash
exit
【代码注释】(退出命令)
exit或Ctrl+D关闭当前 mongosh 会话,返回系统终端。- 仅断开客户端,mongod 服务仍在后台运行(除非你是前台启动的 mongod 且未另开终端)。
【代码注释】
mongosh是 MongoDB 6+ 官方 Shell,替代已弃用的mongo命令;语法与旧版大体兼容。- 无参数连接本机默认
mongodb://127.0.0.1:27017;URI 形式便于脚本与文档复制。 -u/-p用于启用--auth后的认证;认证库由authSource指定(见 §6.4)。exit仅退出 Shell 客户端,不会 停止mongod服务进程。- 连接失败先检查:
mongod是否运行、netstat看 27017 是否监听、防火墙规则。
GUI 工具(市面常用):
| 工具 | 特点 |
|---|---|
| MongoDB Compass | 官方免费,可视化 CRUD、索引、性能 |
| Studio 3T | 企业常用,导入导出、聚合构建器 |
行业落点: 开发用 Compass 看文档结构;运维用 mongodump / mongorestore 做备份。
0.3 关系型数据库 vs MongoDB(名词归纳)
| 对比项 | 关系型(MySQL 等) | MongoDB(文档型 NoSQL) |
|---|---|---|
| 数据单元 | 行 row | 文档 document(类 JSON 对象) |
| 逻辑分组 | 表 table | 集合 collection |
| 结构 | 固定列、需 ALTER | 灵活 Schema,字段可增减 |
| 关联 | JOIN | 嵌入文档或 $lookup 聚合 |
| 事务 | 成熟 | 4.0+ 多文档事务(Replica Set) |
| 典型场景 | 强一致账务核心 | 内容、日志、用户画像、快速迭代 |
经典应用场景:
- MongoDB 擅长:商品目录、评论流、IoT 时序、CMS、游戏背包。
- 仍用 SQL 的场景:复杂报表 JOIN、强事务银行核心(或 MongoDB + 消息队列折中)。
【实战要点】
- 经典应用场景:电商商品 SKU(嵌套规格)、社交动态流(评论数组内嵌)、IoT 设备上报(时序 + TTL 索引)。
- 常见坑:把 MongoDB 当「无模式」而从不设计字段约定,半年后同一集合内字段名混乱、无法建索引。
- 性能与最佳实践:读多写少的热点字段建索引;大文档(>16MB 单文档上限)应拆集合或 GridFS。
【面试考点】
Q1:MongoDB 和 MySQL 的核心区别?
A:MySQL 行存表、固定列、JOIN 成熟;MongoDB 文档存集合、Schema 灵活、横向扩展(分片)原生。选型看结构是否频繁变、是否要复杂 JOIN 报表。追问「能否 JOIN」:可用 $lookup 聚合,但不如 SQL 方便。
【本章小结】
| 角色 | 程序 / 命令 | 职责 | 生活类比 |
|---|---|---|---|
| 服务端 | mongod |
监听 27017、读写数据文件、管理存储引擎 | 餐厅后厨 |
| Shell 客户端 | mongosh |
交互式执行 db.xxx 命令 |
点单的服务员 |
| GUI 客户端 | Compass / Studio 3T | 可视化浏览文档、建索引、看性能 | 自助点餐屏 |
| 备份工具 | mongodump / mongorestore |
导出 / 导入 BSON 快照 | 仓库盘点 |
记忆口诀 :「一服务、多客户端 」------mongod 是唯一需要长期驻留的服务进程,mongosh、Compass 都只是连上去的客户端;关掉客户端不影响数据,关掉 mongod 才算真正停库。安装排错记「端口、目录、权限 」三连:27017 是否被占、dbpath 是否存在、进程能否读写该目录。
1. MongoDB 核心概念解析
1.1 什么是 MongoDB?
MongoDB 是一个基于文档的分布式数据库,专为现代应用开发而设计。它使用类似 JSON 的文档格式,使得数据存储更加灵活和直观。
核心特性:
- 文档导向:使用 BSON(Binary JSON)格式存储数据
- 模式灵活:支持动态 Schema,适应快速迭代
- 水平扩展:原生支持分片,易于扩展
- 高性能:内存映射存储引擎,支持高并发
- 丰富查询:支持丰富的查询语言和聚合框架
名词解析:
- BSON:Binary JSON 的简称,是 JSON 的二进制编码格式,支持更多数据类型
- 文档:MongoDB 中的基本数据单位,类似于关系型数据库中的行
- 集合:文档的集合,类似于关系型数据库中的表
- 数据库:集合的容器,一个 MongoDB 实例可以承载多个数据库
1.2 MongoDB 数据模型
#mermaid-svg-vFpd22yxoCcQxj1m{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-vFpd22yxoCcQxj1m .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-vFpd22yxoCcQxj1m .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-vFpd22yxoCcQxj1m .error-icon{fill:#552222;}#mermaid-svg-vFpd22yxoCcQxj1m .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-vFpd22yxoCcQxj1m .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-vFpd22yxoCcQxj1m .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-vFpd22yxoCcQxj1m .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-vFpd22yxoCcQxj1m .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-vFpd22yxoCcQxj1m .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-vFpd22yxoCcQxj1m .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-vFpd22yxoCcQxj1m .marker{fill:#333333;stroke:#333333;}#mermaid-svg-vFpd22yxoCcQxj1m .marker.cross{stroke:#333333;}#mermaid-svg-vFpd22yxoCcQxj1m svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-vFpd22yxoCcQxj1m p{margin:0;}#mermaid-svg-vFpd22yxoCcQxj1m .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-vFpd22yxoCcQxj1m .cluster-label text{fill:#333;}#mermaid-svg-vFpd22yxoCcQxj1m .cluster-label span{color:#333;}#mermaid-svg-vFpd22yxoCcQxj1m .cluster-label span p{background-color:transparent;}#mermaid-svg-vFpd22yxoCcQxj1m .label text,#mermaid-svg-vFpd22yxoCcQxj1m span{fill:#333;color:#333;}#mermaid-svg-vFpd22yxoCcQxj1m .node rect,#mermaid-svg-vFpd22yxoCcQxj1m .node circle,#mermaid-svg-vFpd22yxoCcQxj1m .node ellipse,#mermaid-svg-vFpd22yxoCcQxj1m .node polygon,#mermaid-svg-vFpd22yxoCcQxj1m .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-vFpd22yxoCcQxj1m .rough-node .label text,#mermaid-svg-vFpd22yxoCcQxj1m .node .label text,#mermaid-svg-vFpd22yxoCcQxj1m .image-shape .label,#mermaid-svg-vFpd22yxoCcQxj1m .icon-shape .label{text-anchor:middle;}#mermaid-svg-vFpd22yxoCcQxj1m .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-vFpd22yxoCcQxj1m .rough-node .label,#mermaid-svg-vFpd22yxoCcQxj1m .node .label,#mermaid-svg-vFpd22yxoCcQxj1m .image-shape .label,#mermaid-svg-vFpd22yxoCcQxj1m .icon-shape .label{text-align:center;}#mermaid-svg-vFpd22yxoCcQxj1m .node.clickable{cursor:pointer;}#mermaid-svg-vFpd22yxoCcQxj1m .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-vFpd22yxoCcQxj1m .arrowheadPath{fill:#333333;}#mermaid-svg-vFpd22yxoCcQxj1m .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-vFpd22yxoCcQxj1m .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-vFpd22yxoCcQxj1m .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vFpd22yxoCcQxj1m .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-vFpd22yxoCcQxj1m .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vFpd22yxoCcQxj1m .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-vFpd22yxoCcQxj1m .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-vFpd22yxoCcQxj1m .cluster text{fill:#333;}#mermaid-svg-vFpd22yxoCcQxj1m .cluster span{color:#333;}#mermaid-svg-vFpd22yxoCcQxj1m div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-vFpd22yxoCcQxj1m .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-vFpd22yxoCcQxj1m rect.text{fill:none;stroke-width:0;}#mermaid-svg-vFpd22yxoCcQxj1m .icon-shape,#mermaid-svg-vFpd22yxoCcQxj1m .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vFpd22yxoCcQxj1m .icon-shape p,#mermaid-svg-vFpd22yxoCcQxj1m .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-vFpd22yxoCcQxj1m .icon-shape .label rect,#mermaid-svg-vFpd22yxoCcQxj1m .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vFpd22yxoCcQxj1m .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-vFpd22yxoCcQxj1m .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-vFpd22yxoCcQxj1m :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} MongoDB 实例
数据库1
数据库2
数据库3
集合1
集合2
文档1
文档2
文档3
字段1: 值1
字段2: 值2
字段3: 值3
【代码注释】(数据模型图)
- 一个 mongod 实例 可挂多个 database,彼此命名空间隔离(如
account_book与test)。 - 一个 database 含多个 collection ;collection 无列定义,每条记录是一个 document。
- 文档由任意多个 field: value 组成,同类业务建议字段名尽量统一,便于查询与索引。
_id字段每个文档必有(可自定义),默认ObjectId全局唯一。
1.3 BSON 数据类型详解
| 类型 | 描述 | 示例 |
|---|---|---|
| String | 字符串 | "hello world" |
| Integer | 整数 | 42, -7 |
| Double | 浮点数 | 3.14, -0.5 |
| Boolean | 布尔值 | true, false |
| Array | 数组 | [1, 2, 3], ["a", "b"] |
| Object | 嵌套文档 | { "name": "John", "age": 30 } |
| Null | 空值 | null |
| Date | 日期时间 | new Date() |
| ObjectId | 文档唯一标识 | ObjectId("507f1f77bcf86cd799439011") |
经典应用场景:
- 电商平台:商品信息、用户数据、订单管理
- 内容管理:文章、评论、标签系统
- 实时应用:聊天记录、用户行为追踪
- 日志系统:应用日志、监控数据
1.4 BSON 与 ObjectId 底层原理
前面把 BSON 解释成「JSON 的二进制版」,这是表层。要真正理解 MongoDB 为什么快、为什么单文档限制 16MB、为什么 _id 默认能「自带时间」,必须看清 BSON 在磁盘和网络上的二进制布局。
名词解析(底层):
- BSON :Binary JSON,一种带长度前缀、带类型标签 的二进制序列化格式,由 MongoDB 设计并开源(规范见 bsonspec.org)。
- 长度前缀:每个文档 / 子文档 / 数组的最前面有一个 4 字节 int32,记录自身总字节数。
- 类型标签:每个字段值前有 1 字节,标明这是 String、Int32、Date 还是 ObjectId 等。
- ObjectId :12 字节的默认主键,由客户端(驱动)生成,不需要访问数据库。
一、BSON 的二进制布局------为什么解析比 JSON 快
一个 BSON 文档的结构是:
text
[int32 文档总长度][元素1][元素2]...[元素N][0x00 结束符]
每个元素 = [1 字节类型码][字段名 cstring \0][值]
【代码注释】关键在 长度前缀 。解析 JSON 文本时,要找到 {...} 的配对结尾,必须逐字符扫描 、还要处理转义和引号;而 BSON 一上来就知道「这个文档/子文档共 240 字节」,驱动想跳过一个不需要的嵌套对象时,直接把读指针 +240,是 O(1) 跳跃 而非 O(n) 扫描。这就是「投影只取部分字段」能省 CPU 的底层原因。代价是:BSON 有时比等价 JSON 更大 (重复存字段名、加了长度前缀和类型码),所谓「轻量」指的是遍历轻量 ,不是体积轻量。市面应用 :MongoDB 驱动、bson npm 包,以及 Mongoose 在网络层收发的就是这种字节流。
二、为什么单文档上限 16MB
BSON 长度前缀是 int32,理论可达 2GB,但 MongoDB 人为把单文档限制在 16MB 。原因是:文档是读写与网络传输的最小原子单位 ,一次更新会把整个文档读进内存、改完整体写回;若允许超大文档,单次操作就会占用大量 RAM 并撑爆缓存。需要存大文件(媒体文件、镜像包)时改用 GridFS,它把大文件切成 255KB 的 chunk 分散存储。
三、ObjectId 的 12 字节构成------_id 为什么自带时间
#mermaid-svg-tOSR9mmHNluygJH0{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-tOSR9mmHNluygJH0 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-tOSR9mmHNluygJH0 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-tOSR9mmHNluygJH0 .error-icon{fill:#552222;}#mermaid-svg-tOSR9mmHNluygJH0 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-tOSR9mmHNluygJH0 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-tOSR9mmHNluygJH0 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-tOSR9mmHNluygJH0 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-tOSR9mmHNluygJH0 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-tOSR9mmHNluygJH0 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-tOSR9mmHNluygJH0 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-tOSR9mmHNluygJH0 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-tOSR9mmHNluygJH0 .marker.cross{stroke:#333333;}#mermaid-svg-tOSR9mmHNluygJH0 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-tOSR9mmHNluygJH0 p{margin:0;}#mermaid-svg-tOSR9mmHNluygJH0 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-tOSR9mmHNluygJH0 .cluster-label text{fill:#333;}#mermaid-svg-tOSR9mmHNluygJH0 .cluster-label span{color:#333;}#mermaid-svg-tOSR9mmHNluygJH0 .cluster-label span p{background-color:transparent;}#mermaid-svg-tOSR9mmHNluygJH0 .label text,#mermaid-svg-tOSR9mmHNluygJH0 span{fill:#333;color:#333;}#mermaid-svg-tOSR9mmHNluygJH0 .node rect,#mermaid-svg-tOSR9mmHNluygJH0 .node circle,#mermaid-svg-tOSR9mmHNluygJH0 .node ellipse,#mermaid-svg-tOSR9mmHNluygJH0 .node polygon,#mermaid-svg-tOSR9mmHNluygJH0 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-tOSR9mmHNluygJH0 .rough-node .label text,#mermaid-svg-tOSR9mmHNluygJH0 .node .label text,#mermaid-svg-tOSR9mmHNluygJH0 .image-shape .label,#mermaid-svg-tOSR9mmHNluygJH0 .icon-shape .label{text-anchor:middle;}#mermaid-svg-tOSR9mmHNluygJH0 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-tOSR9mmHNluygJH0 .rough-node .label,#mermaid-svg-tOSR9mmHNluygJH0 .node .label,#mermaid-svg-tOSR9mmHNluygJH0 .image-shape .label,#mermaid-svg-tOSR9mmHNluygJH0 .icon-shape .label{text-align:center;}#mermaid-svg-tOSR9mmHNluygJH0 .node.clickable{cursor:pointer;}#mermaid-svg-tOSR9mmHNluygJH0 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-tOSR9mmHNluygJH0 .arrowheadPath{fill:#333333;}#mermaid-svg-tOSR9mmHNluygJH0 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-tOSR9mmHNluygJH0 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-tOSR9mmHNluygJH0 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-tOSR9mmHNluygJH0 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-tOSR9mmHNluygJH0 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-tOSR9mmHNluygJH0 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-tOSR9mmHNluygJH0 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-tOSR9mmHNluygJH0 .cluster text{fill:#333;}#mermaid-svg-tOSR9mmHNluygJH0 .cluster span{color:#333;}#mermaid-svg-tOSR9mmHNluygJH0 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-tOSR9mmHNluygJH0 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-tOSR9mmHNluygJH0 rect.text{fill:none;stroke-width:0;}#mermaid-svg-tOSR9mmHNluygJH0 .icon-shape,#mermaid-svg-tOSR9mmHNluygJH0 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-tOSR9mmHNluygJH0 .icon-shape p,#mermaid-svg-tOSR9mmHNluygJH0 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-tOSR9mmHNluygJH0 .icon-shape .label rect,#mermaid-svg-tOSR9mmHNluygJH0 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-tOSR9mmHNluygJH0 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-tOSR9mmHNluygJH0 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-tOSR9mmHNluygJH0 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ObjectId(12 字节 / 24 位十六进制)
4 字节
Unix 时间戳(秒)
5 字节
进程随机值(启动时生成一次)
3 字节
自增计数器(初值随机)
【代码注释】(ObjectId 结构图)现代 MongoDB(3.4+ 驱动规范)的 ObjectId = 4 字节时间戳 + 5 字节随机数 + 3 字节自增计数器 。三段设计同时解决三个问题:① 时间戳在最高位 → 不同时刻生成的 _id 天然按时间有序 ,sort({ _id: 1 }) 约等于按创建时间排序;② 随机数保证不同机器/进程不撞;③ 自增计数器保证同一进程同一秒内连续生成也不重复。最关键的一点:ObjectId 由客户端驱动生成 ,new Account({...}) 在 Node 进程里就拿到了 _id,不需要先访问数据库------这让分布式写入、提前知道主键都成为可能,和 MySQL 自增主键「必须 insert 后才知道 id」形成鲜明对比。
下面是一个入门示例 :在浏览器里手写一个 ObjectId 解析器,把 24 位十六进制字符串还原成它的创建时间。保存为 objectid-parser.html 放到课程目录,双击即可运行:
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>ObjectId 结构解析器</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 640px; margin: 2rem auto; padding: 0 1rem; }
input { width: 100%; padding: 8px; font-size: 15px; box-sizing: border-box; }
button { margin-top: 10px; padding: 8px 16px; cursor: pointer; }
table { border-collapse: collapse; width: 100%; margin-top: 14px; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
th { background: #f4f4f4; }
.seg1 { color: #c0392b; } .seg2 { color: #2980b9; } .seg3 { color: #27ae60; }
</style>
</head>
<body>
<h1>ObjectId 12 字节解析</h1>
<p>输入 24 位十六进制的 <code>_id</code>,下方拆解出时间戳、随机段与计数器。</p>
<input id="oid" value="647ae9d013ab34ca2595e162" />
<button type="button" id="parse">解析</button>
<table>
<tbody id="out"></tbody>
</table>
<script>
function parse() {
const hex = document.getElementById('oid').value.trim().toLowerCase();
const out = document.getElementById('out');
if (!/^[0-9a-f]{24}$/.test(hex)) {
out.innerHTML = '<tr><td>错误</td><td>必须是 24 位十六进制字符</td></tr>';
return;
}
const tsHex = hex.slice(0, 8); // 前 4 字节:时间戳
const randHex = hex.slice(8, 18); // 中 5 字节:随机值
const cntHex = hex.slice(18, 24); // 后 3 字节:计数器
const seconds = parseInt(tsHex, 16); // 秒级时间戳
const date = new Date(seconds * 1000); // 转为日期对象
out.innerHTML =
'<tr><th>段</th><th>十六进制</th><th>含义</th></tr>' +
`<tr><td class="seg1">时间戳(4B)</td><td class="seg1">${tsHex}</td><td>${date.toLocaleString()}</td></tr>` +
`<tr><td class="seg2">随机值(5B)</td><td class="seg2">${randHex}</td><td>进程启动时生成一次</td></tr>` +
`<tr><td class="seg3">计数器(3B)</td><td class="seg3">${cntHex}</td><td>同进程内自增,当前值 ${parseInt(cntHex, 16)}</td></tr>`;
}
document.getElementById('parse').onclick = parse;
parse();
</script>
</body>
</html>
【代码注释】这段代码把一个 ObjectId 字符串按 8 / 10 / 6 个十六进制位(即 4 / 5 / 3 字节)切成三段:slice(0,8) 是时间戳,parseInt(tsHex, 16) 把它当成 16 进制整数得到 Unix 秒数,乘 1000 交给 new Date() 即还原出文档的创建时刻。这正是驱动里 ObjectId.getTimestamp() 的等价实现。为什么这样写 :它直观证明了「_id 里藏着创建时间」------很多项目因此省掉 createdAt 字段 ,直接用 _id.getTimestamp()。市面应用 :后台列表「按创建时间倒序」常直接 sort({ _id: -1 });数据导出时用 _id 反推记录产生的大致时段做审计。
【实战要点】
- 经典应用场景 :用
_id自带的时间戳给文档「免费」加创建时间------日志、订单等只读历史数据可省去createdAt,sort({ _id: -1 })即「最新优先」。 - 常见坑:把超大内容(整段富文本、Base64 图片)直接塞进文档,逼近 16MB 上限后更新极慢、内存飙升;图片 / 文件应走对象存储或 GridFS,文档里只存 URL。
- 性能与最佳实践 :字段名也会逐文档存进 BSON,海量小文档时
userName比un多占的字节会被放大 N 倍------但可读性优先,除非是亿级集合才考虑短字段名。
【本章小结】
| 概念 | 一句话本质 | 关键数字 |
|---|---|---|
| 文档 document | 一条 BSON 记录,最小读写单位 | 单文档 ≤ 16MB |
| 集合 collection | 文档的容器,无固定列 | 一库可多集合 |
| BSON | 带长度前缀 + 类型码的二进制 JSON | 文档以 int32 长度开头 |
| ObjectId | 客户端生成的 12 字节主键 | 4 + 5 + 3 字节,时间戳在前 |
记忆口诀 :「库套集合、集合套文档,文档是 BSON 」;ObjectId 记「时间在前、随机在中、计数在后」------所以它有序、唯一、还不用问数据库就能生成。
【面试考点】
Q1:BSON 和 JSON 是什么关系?为什么 MongoDB 用 BSON?
A:BSON 是 JSON 的二进制序列化格式,在 JSON 基础上加了长度前缀 和类型标签 。长度前缀让驱动能 O(1) 跳过不需要的子文档,解析比逐字符扫描 JSON 文本快;类型标签让它能表达 JSON 没有的 Date、ObjectId、Decimal128、Int32/Int64。代价是体积有时比 JSON 大,「轻量」指遍历轻量。
Q2:ObjectId 由几部分组成?它和 MySQL 自增主键有什么不同?
A:12 字节 = 4 字节时间戳 + 5 字节进程随机值 + 3 字节自增计数器。两点关键区别:① ObjectId 由客户端生成 ,insert 之前就能拿到主键,MySQL 自增 id 必须 insert 后才知道;② ObjectId 高位是时间戳,全局近似有序且适合分布式,自增主键在分库分表时会冲突。追问「16MB 限制为什么存在」:文档是读写原子单位,整体进出内存,限制大小是为保护内存与缓存。
2. MongoDB 架构设计原理
名词解析:
- 存储引擎 :数据库中真正负责「数据如何在内存与磁盘之间组织、读写」的底层模块,MongoDB 默认用 WiredTiger。
- WAL(Write-Ahead Log,预写日志) :先把改动写进日志、再异步刷数据文件的机制,MongoDB 中称 journal。
- 检查点(Checkpoint):把内存中的脏数据成批刷到磁盘、形成一致快照的动作,WiredTiger 默认约 60 秒一次。
- MVCC(多版本并发控制):读写各看自己的数据「快照」,让读不阻塞写、写不阻塞读。
- 索引(Index):为某些字段额外维护的有序数据结构(B-tree),用空间和写入代价换查询速度。
- 覆盖查询(Covered Query):查询所需字段全在索引里,无需回表读文档。
2.1 存储引擎架构
MongoDB 使用内存映射存储引擎 (MMAPv1)或WiredTiger存储引擎,后者从 MongoDB 3.2 开始成为默认选择。
WiredTiger 存储引擎特性:
- 文档级别锁定
- 压缩算法支持(Snappy、Zlib)
- 检查点机制
- 写前日志(WAL)
磁盘存储 内存缓存 MongoDB 服务 客户端应用 磁盘存储 内存缓存 MongoDB 服务 客户端应用 #mermaid-svg-QBdKgjYLHZjob0Rd{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-QBdKgjYLHZjob0Rd .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-QBdKgjYLHZjob0Rd .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-QBdKgjYLHZjob0Rd .error-icon{fill:#552222;}#mermaid-svg-QBdKgjYLHZjob0Rd .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-QBdKgjYLHZjob0Rd .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-QBdKgjYLHZjob0Rd .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-QBdKgjYLHZjob0Rd .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-QBdKgjYLHZjob0Rd .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-QBdKgjYLHZjob0Rd .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-QBdKgjYLHZjob0Rd .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-QBdKgjYLHZjob0Rd .marker{fill:#333333;stroke:#333333;}#mermaid-svg-QBdKgjYLHZjob0Rd .marker.cross{stroke:#333333;}#mermaid-svg-QBdKgjYLHZjob0Rd svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-QBdKgjYLHZjob0Rd p{margin:0;}#mermaid-svg-QBdKgjYLHZjob0Rd .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-QBdKgjYLHZjob0Rd text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-QBdKgjYLHZjob0Rd .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-QBdKgjYLHZjob0Rd .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-QBdKgjYLHZjob0Rd .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-QBdKgjYLHZjob0Rd .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-QBdKgjYLHZjob0Rd #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-QBdKgjYLHZjob0Rd .sequenceNumber{fill:white;}#mermaid-svg-QBdKgjYLHZjob0Rd #sequencenumber{fill:#333;}#mermaid-svg-QBdKgjYLHZjob0Rd #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-QBdKgjYLHZjob0Rd .messageText{fill:#333;stroke:none;}#mermaid-svg-QBdKgjYLHZjob0Rd .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-QBdKgjYLHZjob0Rd .labelText,#mermaid-svg-QBdKgjYLHZjob0Rd .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-QBdKgjYLHZjob0Rd .loopText,#mermaid-svg-QBdKgjYLHZjob0Rd .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-QBdKgjYLHZjob0Rd .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-QBdKgjYLHZjob0Rd .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-QBdKgjYLHZjob0Rd .noteText,#mermaid-svg-QBdKgjYLHZjob0Rd .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-QBdKgjYLHZjob0Rd .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-QBdKgjYLHZjob0Rd .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-QBdKgjYLHZjob0Rd .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-QBdKgjYLHZjob0Rd .actorPopupMenu{position:absolute;}#mermaid-svg-QBdKgjYLHZjob0Rd .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-QBdKgjYLHZjob0Rd .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-QBdKgjYLHZjob0Rd .actor-man circle,#mermaid-svg-QBdKgjYLHZjob0Rd line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-QBdKgjYLHZjob0Rd :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} alt 数据在缓存中 数据不在缓存中 发起查询请求 检查内存缓存 返回缓存数据 从磁盘加载数据 写入内存缓存 返回数据 返回查询结果
【代码注释】(读写路径图)
- WiredTiger 将热数据缓存在内存;命中缓存则读延迟低,未命中则从磁盘加载并回填缓存。
- 写操作先写 WAL(写前日志) 再落盘,崩溃恢复时重放日志,保证持久性。
- 文档级锁(相对旧版 MMAPv1 库级锁)提高并发;高并发写仍需合理索引与文档设计。
- 课堂单机开发感受不明显;生产需关注内存、磁盘 IOPS 与慢查询。
2.2 索引机制
索引类型:
- 单字段索引:基于单个字段创建
- 复合索引:基于多个字段创建
- 文本索引:支持全文搜索
- 地理空间索引:支持地理位置查询
- 哈希索引:基于哈希值的索引
索引创建示例:
javascript
// 单字段索引
db.users.createIndex({ username: 1 })
// 复合索引
db.users.createIndex({ username: 1, email: 1 })
// 文本索引
db.articles.createIndex({ content: "text" })
// 地理空间索引
db.locations.createIndex({ coordinates: "2dsphere" })
// 哈希索引
db.users.createIndex({ username: "hashed" })
【代码注释】
createIndex({ username: 1 })单字段 B-Tree 索引;1升序、-1降序,单字段时方向对查询影响小。- 复合索引
{ username: 1, email: 1 }遵循最左前缀 :查username可走索引,只查email往往不行。 "text"全文索引,支持$text搜索;一个集合通常一个 text 索引(多字段可合并)。"2dsphere"用于 GeoJSON,如{ type: "Point", coordinates: [经度, 纬度] },配合$near等算子。"hashed"哈希索引多用于分片键;随机分布,不适合范围查询。- 用
db.users.getIndexes()查看已有索引;explain("executionStats")中COLLSCAN表示全表扫描,需优化。
经典使用场景:
- 用户系统:基于用户名或邮箱的唯一索引
- 电商搜索:商品名称和类别的复合索引
- 地理位置:附近的商店、餐厅查询
- 内容搜索:文章全文检索
2.3 深入存储引擎与索引原理
§2.1~§2.2 给出了「有什么」,这一节回答「为什么这样设计」------这是面试区分「用过」和「懂」的分水岭。
一、WiredTiger 的三板斧:MVCC + WAL + Checkpoint
#mermaid-svg-TpEZa7yuibaEl3Yt{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-TpEZa7yuibaEl3Yt .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-TpEZa7yuibaEl3Yt .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-TpEZa7yuibaEl3Yt .error-icon{fill:#552222;}#mermaid-svg-TpEZa7yuibaEl3Yt .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-TpEZa7yuibaEl3Yt .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-TpEZa7yuibaEl3Yt .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-TpEZa7yuibaEl3Yt .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-TpEZa7yuibaEl3Yt .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-TpEZa7yuibaEl3Yt .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-TpEZa7yuibaEl3Yt .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-TpEZa7yuibaEl3Yt .marker{fill:#333333;stroke:#333333;}#mermaid-svg-TpEZa7yuibaEl3Yt .marker.cross{stroke:#333333;}#mermaid-svg-TpEZa7yuibaEl3Yt svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-TpEZa7yuibaEl3Yt p{margin:0;}#mermaid-svg-TpEZa7yuibaEl3Yt .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-TpEZa7yuibaEl3Yt .cluster-label text{fill:#333;}#mermaid-svg-TpEZa7yuibaEl3Yt .cluster-label span{color:#333;}#mermaid-svg-TpEZa7yuibaEl3Yt .cluster-label span p{background-color:transparent;}#mermaid-svg-TpEZa7yuibaEl3Yt .label text,#mermaid-svg-TpEZa7yuibaEl3Yt span{fill:#333;color:#333;}#mermaid-svg-TpEZa7yuibaEl3Yt .node rect,#mermaid-svg-TpEZa7yuibaEl3Yt .node circle,#mermaid-svg-TpEZa7yuibaEl3Yt .node ellipse,#mermaid-svg-TpEZa7yuibaEl3Yt .node polygon,#mermaid-svg-TpEZa7yuibaEl3Yt .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-TpEZa7yuibaEl3Yt .rough-node .label text,#mermaid-svg-TpEZa7yuibaEl3Yt .node .label text,#mermaid-svg-TpEZa7yuibaEl3Yt .image-shape .label,#mermaid-svg-TpEZa7yuibaEl3Yt .icon-shape .label{text-anchor:middle;}#mermaid-svg-TpEZa7yuibaEl3Yt .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-TpEZa7yuibaEl3Yt .rough-node .label,#mermaid-svg-TpEZa7yuibaEl3Yt .node .label,#mermaid-svg-TpEZa7yuibaEl3Yt .image-shape .label,#mermaid-svg-TpEZa7yuibaEl3Yt .icon-shape .label{text-align:center;}#mermaid-svg-TpEZa7yuibaEl3Yt .node.clickable{cursor:pointer;}#mermaid-svg-TpEZa7yuibaEl3Yt .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-TpEZa7yuibaEl3Yt .arrowheadPath{fill:#333333;}#mermaid-svg-TpEZa7yuibaEl3Yt .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-TpEZa7yuibaEl3Yt .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-TpEZa7yuibaEl3Yt .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-TpEZa7yuibaEl3Yt .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-TpEZa7yuibaEl3Yt .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-TpEZa7yuibaEl3Yt .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-TpEZa7yuibaEl3Yt .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-TpEZa7yuibaEl3Yt .cluster text{fill:#333;}#mermaid-svg-TpEZa7yuibaEl3Yt .cluster span{color:#333;}#mermaid-svg-TpEZa7yuibaEl3Yt div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-TpEZa7yuibaEl3Yt .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-TpEZa7yuibaEl3Yt rect.text{fill:none;stroke-width:0;}#mermaid-svg-TpEZa7yuibaEl3Yt .icon-shape,#mermaid-svg-TpEZa7yuibaEl3Yt .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-TpEZa7yuibaEl3Yt .icon-shape p,#mermaid-svg-TpEZa7yuibaEl3Yt .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-TpEZa7yuibaEl3Yt .icon-shape .label rect,#mermaid-svg-TpEZa7yuibaEl3Yt .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-TpEZa7yuibaEl3Yt .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-TpEZa7yuibaEl3Yt .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-TpEZa7yuibaEl3Yt :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 每约 60 秒
重启后
写请求
1.写 journal(WAL,磁盘顺序写)
2.改内存中的页(标记为脏页)
3.返回客户端「写成功」
4.Checkpoint:脏页成批刷入数据文件
进程崩溃
重放 journal 中未落盘的改动
【代码注释】(写入路径图)一次写操作并不会 立刻把数据文件改掉。WiredTiger 的顺序是:先把改动追加到 journal (磁盘顺序写,极快)→ 修改内存里的数据页(变成「脏页」)→ 就返回成功。真正把脏页写进数据文件是 Checkpoint (约 60 秒一次)批量做的。为什么这样设计 :随机写磁盘慢、顺序写快,WAL 把「慢的随机写」推迟并合并,用「快的顺序日志」先保证持久性 ------万一进程在两次 Checkpoint 之间崩溃,重启时重放 journal 就能补回丢失的改动。这也解释了 w: 'majority' + j: true 的含义:j: true 要求写操作等 journal 落盘 才算成功,安全但稍慢。市面应用:MySQL 的 redo log、PostgreSQL 的 WAL 是同一思想,「预写日志」是数据库持久化的通用范式。
WiredTiger 还用 MVCC 做并发:每个操作看到的是某一时刻的数据快照 ,因此「读不会被写阻塞,写不会被读阻塞」。配合文档级并发控制(旧 MMAPv1 是库级锁),多个请求改同一集合的不同文档可以并行------这是 MongoDB 3.x 之后写并发大幅提升的根因。
二、索引为什么快:B-tree 与最左前缀
MongoDB 索引底层是 B-tree(B 树家族) 。它是一棵「矮而宽」的多叉平衡树:几百万条数据通常只有 3~4 层,查一个值最多 3~4 次磁盘页访问,复杂度 O(log n);而没有索引就是 COLLSCAN(全表扫描),复杂度 O(n)。
#mermaid-svg-9Ai7Tz95yIwcuSNL{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-9Ai7Tz95yIwcuSNL .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-9Ai7Tz95yIwcuSNL .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-9Ai7Tz95yIwcuSNL .error-icon{fill:#552222;}#mermaid-svg-9Ai7Tz95yIwcuSNL .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-9Ai7Tz95yIwcuSNL .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-9Ai7Tz95yIwcuSNL .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-9Ai7Tz95yIwcuSNL .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-9Ai7Tz95yIwcuSNL .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-9Ai7Tz95yIwcuSNL .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-9Ai7Tz95yIwcuSNL .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-9Ai7Tz95yIwcuSNL .marker{fill:#333333;stroke:#333333;}#mermaid-svg-9Ai7Tz95yIwcuSNL .marker.cross{stroke:#333333;}#mermaid-svg-9Ai7Tz95yIwcuSNL svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-9Ai7Tz95yIwcuSNL p{margin:0;}#mermaid-svg-9Ai7Tz95yIwcuSNL .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-9Ai7Tz95yIwcuSNL .cluster-label text{fill:#333;}#mermaid-svg-9Ai7Tz95yIwcuSNL .cluster-label span{color:#333;}#mermaid-svg-9Ai7Tz95yIwcuSNL .cluster-label span p{background-color:transparent;}#mermaid-svg-9Ai7Tz95yIwcuSNL .label text,#mermaid-svg-9Ai7Tz95yIwcuSNL span{fill:#333;color:#333;}#mermaid-svg-9Ai7Tz95yIwcuSNL .node rect,#mermaid-svg-9Ai7Tz95yIwcuSNL .node circle,#mermaid-svg-9Ai7Tz95yIwcuSNL .node ellipse,#mermaid-svg-9Ai7Tz95yIwcuSNL .node polygon,#mermaid-svg-9Ai7Tz95yIwcuSNL .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-9Ai7Tz95yIwcuSNL .rough-node .label text,#mermaid-svg-9Ai7Tz95yIwcuSNL .node .label text,#mermaid-svg-9Ai7Tz95yIwcuSNL .image-shape .label,#mermaid-svg-9Ai7Tz95yIwcuSNL .icon-shape .label{text-anchor:middle;}#mermaid-svg-9Ai7Tz95yIwcuSNL .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-9Ai7Tz95yIwcuSNL .rough-node .label,#mermaid-svg-9Ai7Tz95yIwcuSNL .node .label,#mermaid-svg-9Ai7Tz95yIwcuSNL .image-shape .label,#mermaid-svg-9Ai7Tz95yIwcuSNL .icon-shape .label{text-align:center;}#mermaid-svg-9Ai7Tz95yIwcuSNL .node.clickable{cursor:pointer;}#mermaid-svg-9Ai7Tz95yIwcuSNL .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-9Ai7Tz95yIwcuSNL .arrowheadPath{fill:#333333;}#mermaid-svg-9Ai7Tz95yIwcuSNL .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-9Ai7Tz95yIwcuSNL .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-9Ai7Tz95yIwcuSNL .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-9Ai7Tz95yIwcuSNL .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-9Ai7Tz95yIwcuSNL .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-9Ai7Tz95yIwcuSNL .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-9Ai7Tz95yIwcuSNL .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-9Ai7Tz95yIwcuSNL .cluster text{fill:#333;}#mermaid-svg-9Ai7Tz95yIwcuSNL .cluster span{color:#333;}#mermaid-svg-9Ai7Tz95yIwcuSNL div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-9Ai7Tz95yIwcuSNL .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-9Ai7Tz95yIwcuSNL rect.text{fill:none;stroke-width:0;}#mermaid-svg-9Ai7Tz95yIwcuSNL .icon-shape,#mermaid-svg-9Ai7Tz95yIwcuSNL .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-9Ai7Tz95yIwcuSNL .icon-shape p,#mermaid-svg-9Ai7Tz95yIwcuSNL .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-9Ai7Tz95yIwcuSNL .icon-shape .label rect,#mermaid-svg-9Ai7Tz95yIwcuSNL .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-9Ai7Tz95yIwcuSNL .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-9Ai7Tz95yIwcuSNL .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-9Ai7Tz95yIwcuSNL :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 根节点(一段值区间)
中间节点 A-M
中间节点 N-Z
叶子:值 + 文档指针
叶子:值 + 文档指针
叶子:值 + 文档指针
【代码注释】(B-tree 图)查询 find({ username: 'tom' }) 时:从根节点 判断 tom 落在哪个区间 → 进入对应中间节点 → 再定位到叶子节点 ,叶子里存着该值对应文档的磁盘位置,按指针取出文档即可。整个过程只碰 3~4 个节点。复合索引 为什么遵守最左前缀 :复合索引 { a: 1, b: 1 } 是「先按 a 排,a 相同再按 b 排」的一棵树------就像电话簿先按姓排、再按名排。只给「名」、不给「姓」,电话簿帮不上忙;所以 find({ b: ... }) 单独查 b 用不上这个索引,必须带上最左的 a。ESR 法则 :设计复合索引时字段顺序按「Equality(等值)→ Sort(排序)→ Range(范围)」排列,能同时服务过滤、排序、范围三类需求。
三、用 explain 看清查询有没有走索引(实战示例)
javascript
// 不带索引:通常是 COLLSCAN
db.users.find({ age: 25 }).explain('executionStats')
// 建索引后再看
db.users.createIndex({ age: 1 })
db.users.find({ age: 25 }).explain('executionStats')
【代码注释】explain('executionStats') 返回里要重点看三个字段:stage 是 COLLSCAN (全表扫描,坏)还是 IXSCAN (走索引,好);totalDocsExamined(实际检查了多少文档);nReturned(最终返回多少条)。健康的查询里 totalDocsExamined 应接近 nReturned------若检查了 10 万条只返回 10 条,说明索引缺失或不合适。为什么重要 :开发期数据量小,全表扫描也「很快」,问题被掩盖;上线数据涨上来后同一句查询就成了慢查询。市面应用 :所有数据库性能优化的第一步都是「看执行计划」------MySQL 的 EXPLAIN、MongoDB 的 explain(),思路一致。
下面用一个入门示例 直观感受「索引扫描」与「全表扫描」的差距。保存为 index-scan-demo.html 放到课程目录后双击运行:
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>索引扫描 vs 全表扫描</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 680px; margin: 2rem auto; padding: 0 1rem; }
button { margin: 8px 8px 8px 0; padding: 8px 16px; cursor: pointer; }
.bar { height: 22px; background: #3498db; color: #fff; font-size: 12px; line-height: 22px; padding-left: 6px; border-radius: 3px; margin: 6px 0; white-space: nowrap; }
.scan { background: #e74c3c; }
pre { background: #f4f4f4; padding: 12px; border-radius: 6px; }
</style>
</head>
<body>
<h1>查 1 个目标值:扫了多少条数据?</h1>
<p>集合里有 <strong>100000</strong> 条文档,要找 <code>age = 25</code> 的那一条。</p>
<button type="button" id="collscan">全表扫描 COLLSCAN</button>
<button type="button" id="ixscan">索引扫描 IXSCAN</button>
<pre id="out"></pre>
<div id="chart"></div>
<script>
const TOTAL = 100000;
function show(stage, examined) {
document.getElementById('out').textContent =
`stage: ${stage}\ntotalDocsExamined: ${examined}\nnReturned: 1`;
const pct = Math.max(1, examined / TOTAL * 100);
document.getElementById('chart').innerHTML =
`<div class="bar ${stage === 'COLLSCAN' ? 'scan' : ''}" style="width:${pct}%">` +
`检查了 ${examined} 条</div>`;
}
// 全表扫描:100000 条全部检查一遍
document.getElementById('collscan').onclick = () => show('COLLSCAN', TOTAL);
// 索引扫描:B-tree 约 log 级别定位,几次即命中
document.getElementById('ixscan').onclick = () => show('IXSCAN', Math.ceil(Math.log2(TOTAL)));
show('COLLSCAN', TOTAL);
</script>
</body>
</html>
【代码注释】这个演示用一个数字直观回答「索引到底省了多少」:全表扫描要 totalDocsExamined = 100000(10 万条全看一遍);B-tree 索引扫描只需 Math.log2(100000) ≈ 17 次节点访问。代码里 Math.log2(TOTAL) 模拟 B-tree 的对数级查找,红色长条 vs 蓝色短条把 O(n) 与 O(log n) 的差距画了出来。为什么这样写 :把抽象的「复杂度」变成可点击对比的条形图,比单纯讲「索引快」更有说服力。市面应用 :这正是 explain() 里 COLLSCAN 报警、DBA 要求「线上查询必须走索引」的现实依据------数据量越大,这根红条越长。
【实战要点】
- 经典应用场景 :用户表
email建唯一索引防重复注册;列表页「按时间倒序 + 按状态过滤」建{ status: 1, createdAt: -1 }复合索引(ESR 法则);session / 验证码集合用 TTL 索引自动过期。 - 常见坑 :① 给低区分度字段(如
gender只有两个值)单独建索引,几乎没用还拖慢写入;② 复合索引字段顺序写反,导致排序或范围查询用不上;③ 索引建太多------每次写文档要同步更新所有相关索引,写入被放大。 - 性能与最佳实践 :上线前用
explain('executionStats')抽查核心查询,确保IXSCAN且totalDocsExamined ≈ nReturned;能用覆盖查询(投影字段全在索引内)就避免回表。
【本章小结】
| 机制 | 解决什么问题 | 一句话原理 |
|---|---|---|
| WAL / journal | 持久性 + 写性能 | 先顺序写日志,崩溃靠重放恢复 |
| Checkpoint | 数据最终落盘 | 约 60 秒批量刷脏页 |
| MVCC + 文档级并发 | 高并发读写 | 各看各的快照,读写不互锁 |
| B-tree 索引 | 查询从 O(n) 降到 O(log n) | 矮宽多叉树,几层定位 |
| 复合索引最左前缀 | 一索引服务多查询 | 先按左字段排,缺左字段则失效 |
记忆口诀 :写靠「日志先行、检查点收尾 」,查靠「B 树定位、最左前缀 」,索引设计记 ESR(等值 → 排序 → 范围)。
【面试考点】
Q1:MongoDB 的写操作是怎么保证「不丢数据」又「不慢」的?
A:靠 WAL(journal) 。写操作先把改动顺序追加 到 journal(顺序写磁盘极快),再改内存脏页就返回成功,真正的数据文件由约 60 秒一次的 Checkpoint 批量刷盘。崩溃重启时重放 journal 即可补回未落盘的改动。这样既避免了「每次写都随机刷盘」的慢,又用日志兜住了持久性。j: true 就是要求等 journal 落盘才算写成功。
Q2:复合索引 { a: 1, b: 1 },查询 find({ b: 5 }) 能走索引吗?为什么?
A:通常不能。复合索引是「先按 a 排序、a 相同再按 b 排序」的一棵 B-tree,类似电话簿先按姓再按名。只给名字、不给姓,无法在簿子里快速定位------这就是最左前缀原则 。find({ a: 1 })、find({ a: 1, b: 5 }) 能用,find({ b: 5 }) 用不上。追问索引顺序怎么定:按 ESR 法则------等值字段在前、排序字段居中、范围字段在后。
3. 数据库操作实战
3.1 数据库基本操作
连接 MongoDB 服务:
bash
# 连接本地 MongoDB(默认端口 27017)
mongo
# 连接指定主机和端口
mongo mongodb://localhost:27017
# 连接需要认证的数据库(现代客户端用 mongosh)
mongosh "mongodb://username:password@localhost:27017/database"
【代码注释】
mongo为旧客户端,MongoDB 6+ 请用 mongosh ;URI 格式mongodb://host:port/db与 Node 驱动一致。- 本地默认
127.0.0.1:27017;远程或 Docker 需改主机名与端口。 - 带用户名密码的 URI:
mongodb://user:pass@host:27017/dbname?authSource=admin(authSource为校验库)。 - 连接失败:确认
mongod已启动、防火墙放行、认证信息正确。
数据库管理命令:
bash
# 显示所有数据库
show dbs
show databases
# 切换或创建数据库
use myDatabase
# 显示当前数据库
db
# 删除当前数据库
db.dropDatabase()
【代码注释】
show dbs/show databases列出磁盘上至少有一个集合 的库;仅use未写入的空库可能不出现。use myDatabase切换上下文;库不存在时延迟创建 ,首次insert才真正落盘。db显示当前库名;所有db.xxx命令都作用于当前库。db.dropDatabase()删除当前 库及其中所有集合,不可恢复,生产务必二次确认库名。
3.2 集合操作详解
集合管理命令:
javascript
// 创建集合
db.createCollection('users')
// 查看所有集合
show collections
// 重命名集合
db.users.renameCollection('members')
// 删除集合
db.users.drop()
// 查看集合统计信息
db.users.stats()
【代码注释】
createCollection('users')显式创建集合;通常第一次 insert 也会自动建集合,课堂两种写法均可。show collections列出当前库下所有集合名(表名)。renameCollection('members')将users改名为members;跨库重命名格式为db.old.renameCollection('new', 'targetDb')。drop()删除整个集合及其索引,比deleteMany({})更彻底。stats()返回文档数、存储大小等,用于容量评估与监控。
3.3 文档操作(CRUD)深度解析
3.3.1 创建文档
插入单个文档:
javascript
db.users.insertOne({
username: "john_doe",
email: "john@example.com",
age: 28,
createdAt: new Date(),
interests: ["programming", "reading", "traveling"],
address: {
street: "123 Main St",
city: "San Francisco",
country: "USA"
}
})
【代码注释】
insertOne插入一条 文档;成功返回含insertedId的结果对象。- 未指定
_id时 MongoDB 自动生成ObjectId(12 字节唯一标识)。 - 支持嵌套对象 (
address)与数组 (interests);字段可随时增减,无需改表结构。 new Date()存为 BSON Date 类型;注意时区与序列化(Node 与 Shell 一致即可)。- 在
mongosh中需先use 数据库名,否则写入test库。
批量插入文档:
javascript
db.users.insertMany([
{
username: "alice",
email: "alice@example.com",
age: 25,
role: "developer"
},
{
username: "bob",
email: "bob@example.com",
age: 32,
role: "designer"
},
{
username: "charlie",
email: "charlie@example.com",
age: 28,
role: "manager"
}
])
【代码注释】
insertMany接收数组 ,一次网络往返插入多条,比循环insertOne高效。- 数组中任一文档格式错误会导致整批失败(默认有序插入);可传
{ ordered: false }让失败项跳过(了解即可)。 - 适合初始化种子数据、批量导入;超大批量建议
mongoimport或驱动bulkWrite。 - 每条文档可有不同字段(无 schema 约束),但业务上建议结构尽量统一。
3.3.2 查询文档
基本查询:
javascript
// 查询所有文档
db.users.find()
// 条件查询
db.users.find({ age: 28 })
// 多条件查询
db.users.find({ age: 28, role: "developer" })
// 查询单个文档
db.users.findOne({ username: "john_doe" })
// 使用运算符查询
db.users.find({ age: { $gt: 25 } }) // age > 25
db.users.find({ age: { $gte: 25 } }) // age >= 25
db.users.find({ age: { $lt: 30 } }) // age < 30
db.users.find({ age: { $lte: 30 } }) // age <= 30
db.users.find({ age: { $ne: 28 } }) // age != 28
// 逻辑运算
db.users.find({ $or: [{ age: 25 }, { age: 32 }] })
db.users.find({ $and: [{ age: { $gt: 25 } }, { age: { $lt: 30 } }] })
// 数组查询
db.users.find({ interests: "programming" })
db.users.find({ interests: { $in: ["programming", "reading"] } })
// 正则表达式查询
db.users.find({ username: /^john/ })
【代码注释】
find({})无过滤条件时返回集合内所有 文档(游标),大数据集勿直接.toArray()爆内存。- 多键
{ age: 28, role: "developer" }为 AND 关系,等价 SQLWHERE age=28 AND role='developer'。 findOne找到第一条即返回对象 ,无匹配返回null;按_id查用findOne({ _id: ObjectId("...") })。$gt/$gte/$lt/$lte/$ne替代 SQL 比较符;不能 在 Shell 里写age > 25。$or/$and的值必须是数组,元素为条件对象。interests: "programming"表示数组包含 该元素;$in表示命中列表中任一项。/^john/为正则;前有通配符/.*john/难以走索引,生产慎用。
查询结果处理:
javascript
// 投影(只返回指定字段)
db.users.find({}, { username: 1, email: 1, _id: 0 })
// 排序
db.users.find().sort({ age: 1 }) // 升序
db.users.find().sort({ age: -1 }) // 降序
// 限制结果数量
db.users.find().limit(10)
// 跳过指定数量
db.users.find().skip(5)
// 分页查询
db.users.find().skip(0).limit(10) // 第1页
db.users.find().skip(10).limit(10) // 第2页
【代码注释】
- 投影
{ username: 1, email: 1, _id: 0 }:1包含、0排除;默认总含_id,不需时请显式_id: 0。 sort({ age: 1 })升序,-1降序;排序字段宜有索引,否则内存排序慢。limit(10)限制条数;skip(5)跳过前 5 条,常用于分页:skip((page-1)*pageSize).limit(pageSize)。skip很大时性能差(需扫描跳过部分);深度分页可用「上次_id」游标方案。- 链式调用顺序建议:
find→sort→skip→limit。
3.3.3 更新文档
更新操作符:
javascript
// $set - 设置字段值
db.users.updateOne(
{ username: "john_doe" },
{ $set: { age: 29 } }
)
// $unset - 删除字段
db.users.updateOne(
{ username: "john_doe" },
{ $unset: { address: "" } }
)
// $inc - 增加数值
db.users.updateOne(
{ username: "john_doe" },
{ $inc: { age: 1 } }
)
// $mul - 相乘
db.users.updateOne(
{ username: "john_doe" },
{ $mul: { score: 2 } }
)
// $rename - 重命名字段
db.users.updateOne(
{ username: "john_doe" },
{ $rename: { "email": "emailAddress" } }
)
// 数组操作
db.users.updateOne(
{ username: "john_doe" },
{ $push: { interests: "cooking" } } // 添加元素
)
db.users.updateOne(
{ username: "john_doe" },
{ $pull: { interests: "reading" } } // 删除元素
)
db.users.updateOne(
{ username: "john_doe" },
{ $addToSet: { interests: "photography" } } // 避免重复添加
)
【代码注释】
updateOne(过滤, 更新)只改第一条 匹配文档;updateMany改所有匹配项。- 必须用更新算子:
$set改字段、$unset删字段、$inc数值自增、$push/$pull/$addToSet操作数组。 - 错误写法:直接
{ age: 29 }作为第二参数会替换 整文档(丢失其他字段),除非用replaceOne语义。 $rename改字段名;$mul数值乘法;$addToSet向数组加唯一值,$push可重复。- 更新默认不返回新文档;需要新文档可传
{ returnDocument: 'after' }(驱动层选项,了解即可)。
3.3.4 删除文档
javascript
// 删除单个文档
db.users.deleteOne({ username: "john_doe" })
// 删除多个文档
db.users.deleteMany({ age: { $lt: 25 } })
// 删除集合中所有文档
db.users.deleteMany({})
【代码注释】
deleteOne删除一条 匹配文档;无匹配时deletedCount为 0,不报错。deleteMany({ age: { $lt: 25 } })按条件批量删除;生产删除前宜先find确认范围。deleteMany({})清空集合内全部文档,集合与索引仍在 ;彻底移除集合用db.users.drop()。- 删除操作不可撤销 (无事务时);重要数据先备份或软删除(
isDeleted: true+$set)。
3.4 查询运算符与逻辑条件(课堂对照)
Shell 中不能 直接用 >、<、===,需用下列符号:
| 含义 | 运算符 |
|---|---|
| 大于 | $gt |
| 小于 | $lt |
| 大于等于 | $gte |
| 小于等于 | $lte |
| 不等于 | $ne |
| 在列表中 | $in |
| 逻辑或 | $or |
| 逻辑与 | $and |
javascript
// 年龄大于 18
db.users.find({ age: { $gt: 18 } })
// 年龄在 18、24、26 之一
db.users.find({ age: { $in: [18, 24, 26] } })
// 年龄为 18 或 24
db.users.find({ $or: [{ age: 18 }, { age: 24 }] })
// 年龄在 15 与 20 之间
db.users.find({ $and: [{ age: { $lt: 20 } }, { age: { $gt: 15 } }] })
// 正则:name 包含 imissyou(JS 正则语法)
db.users.find({ name: /imissyou/ })
【代码注释】
{ age: { $gt: 18 } }年龄大于 18;Shell 中不能写age > 18,必须用$gt等算子。{ age: { $in: [18, 24, 26] } }等价 SQLIN (18,24,26),字段值命中数组任一项即可。{ $or: [{ age: 18 }, { age: 24 }] }逻辑或;$and同理,值为条件对象数组。- 区间查询用
$and组合$gt与$lt,如 15 < age < 20。 { name: /imissyou/ }为 JS 正则;无索引时 COLLSCAN,高频搜索考虑 text 索引或精确匹配。
课堂旧 API 对照(了解即可):
| 操作 | 旧写法(已弃用) | 推荐写法 |
|---|---|---|
| 插入 | insert(doc) |
insertOne / insertMany |
| 删除 | remove(filter) |
deleteOne / deleteMany |
| 更新 | update(filter, doc) |
updateOne + $set |
【实战要点】
- 经典应用场景 :运营后台按注册时间筛用户(
$gte+$lt)、按标签数组查文章(tags: 'nodejs')。 - 常见坑 :表单提交的
type是字符串"1",库里是数字1,find({ type: 1 })无结果------统一Number()或 Schema 类型。 - 性能与最佳实践 :正则查询避免前导
.*;分页深度过大时用_id游标代替大skip。
【面试考点】
Q1:Shell 里为什么不能用 age > 18?
A:查询文档是 BSON 对象匹配,比较必须用 $gt、$lt 等算子。{ age: { $gt: 18 } } 才合法。追问 $in 与 $or:$in 单字段多值;$or 多条件择一。
CRUD 全景图:
#mermaid-svg-WgmWecHfCTDvq31j{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-WgmWecHfCTDvq31j .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-WgmWecHfCTDvq31j .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-WgmWecHfCTDvq31j .error-icon{fill:#552222;}#mermaid-svg-WgmWecHfCTDvq31j .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-WgmWecHfCTDvq31j .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-WgmWecHfCTDvq31j .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-WgmWecHfCTDvq31j .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-WgmWecHfCTDvq31j .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-WgmWecHfCTDvq31j .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-WgmWecHfCTDvq31j .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-WgmWecHfCTDvq31j .marker{fill:#333333;stroke:#333333;}#mermaid-svg-WgmWecHfCTDvq31j .marker.cross{stroke:#333333;}#mermaid-svg-WgmWecHfCTDvq31j svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-WgmWecHfCTDvq31j p{margin:0;}#mermaid-svg-WgmWecHfCTDvq31j .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-WgmWecHfCTDvq31j .cluster-label text{fill:#333;}#mermaid-svg-WgmWecHfCTDvq31j .cluster-label span{color:#333;}#mermaid-svg-WgmWecHfCTDvq31j .cluster-label span p{background-color:transparent;}#mermaid-svg-WgmWecHfCTDvq31j .label text,#mermaid-svg-WgmWecHfCTDvq31j span{fill:#333;color:#333;}#mermaid-svg-WgmWecHfCTDvq31j .node rect,#mermaid-svg-WgmWecHfCTDvq31j .node circle,#mermaid-svg-WgmWecHfCTDvq31j .node ellipse,#mermaid-svg-WgmWecHfCTDvq31j .node polygon,#mermaid-svg-WgmWecHfCTDvq31j .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-WgmWecHfCTDvq31j .rough-node .label text,#mermaid-svg-WgmWecHfCTDvq31j .node .label text,#mermaid-svg-WgmWecHfCTDvq31j .image-shape .label,#mermaid-svg-WgmWecHfCTDvq31j .icon-shape .label{text-anchor:middle;}#mermaid-svg-WgmWecHfCTDvq31j .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-WgmWecHfCTDvq31j .rough-node .label,#mermaid-svg-WgmWecHfCTDvq31j .node .label,#mermaid-svg-WgmWecHfCTDvq31j .image-shape .label,#mermaid-svg-WgmWecHfCTDvq31j .icon-shape .label{text-align:center;}#mermaid-svg-WgmWecHfCTDvq31j .node.clickable{cursor:pointer;}#mermaid-svg-WgmWecHfCTDvq31j .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-WgmWecHfCTDvq31j .arrowheadPath{fill:#333333;}#mermaid-svg-WgmWecHfCTDvq31j .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-WgmWecHfCTDvq31j .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-WgmWecHfCTDvq31j .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-WgmWecHfCTDvq31j .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-WgmWecHfCTDvq31j .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-WgmWecHfCTDvq31j .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-WgmWecHfCTDvq31j .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-WgmWecHfCTDvq31j .cluster text{fill:#333;}#mermaid-svg-WgmWecHfCTDvq31j .cluster span{color:#333;}#mermaid-svg-WgmWecHfCTDvq31j div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-WgmWecHfCTDvq31j .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-WgmWecHfCTDvq31j rect.text{fill:none;stroke-width:0;}#mermaid-svg-WgmWecHfCTDvq31j .icon-shape,#mermaid-svg-WgmWecHfCTDvq31j .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-WgmWecHfCTDvq31j .icon-shape p,#mermaid-svg-WgmWecHfCTDvq31j .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-WgmWecHfCTDvq31j .icon-shape .label rect,#mermaid-svg-WgmWecHfCTDvq31j .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-WgmWecHfCTDvq31j .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-WgmWecHfCTDvq31j .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-WgmWecHfCTDvq31j :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 投影 / 排序 / 分页
Create
insertOne / insertMany
Read
find / findOne + 运算符
Update
updateOne + set/inc/$push
Delete
deleteOne / deleteMany
【代码注释】(CRUD 图)四类操作里,Read 最复杂 ------它额外挂着投影(取哪些字段)、排序、skip/limit 分页,所以图中 R 有一个自指箭头。Update 的精髓 是「必须用更新算子」:$set 改字段、$inc 自增、$push/$pull 操作数组;直接传 { age: 29 } 会整文档替换 。Delete 的红线是不可恢复。记住这张图,命令行操作就有了骨架。
【本章小结】
| 操作 | 单条 API | 批量 API | 必备搭档 |
|---|---|---|---|
| 增 | insertOne |
insertMany |
------ |
| 查 | findOne |
find |
$gt/$in/$or、sort/skip/limit、投影 |
| 改 | updateOne |
updateMany |
$set/$inc/$push/$pull 更新算子 |
| 删 | deleteOne |
deleteMany |
删前先 find 预览 |
记忆口诀 :「查用算子、改用 $set、删前先查 」。Shell 里没有 >、<、==,比较一律 $ 开头算子;更新第二参数不写 $set 就是整文档替换,是新手最常见的「数据莫名丢字段」事故。
4. Mongoose ODM 深度应用
4.1 Mongoose 核心概念
Node 操作 MongoDB 的两种方式:
| 方式 | 包 | 特点 |
|---|---|---|
| Native Driver | mongodb |
官方驱动,API 贴近 Shell,灵活偏底层 |
| Mongoose(ODM) | mongoose |
在驱动上封装 Schema/校验/中间件,课堂与项目首选 |
bash
npm install mongoose@6
# 若用原生驱动:npm install mongodb
【代码注释】
npm install mongoose@6安装 ODM;@6锁定大版本,避免课堂代码与 API 因大版本升级不兼容。- Mongoose 在官方
mongodb驱动之上封装 Schema、校验、中间件、populate,适合业务 CRUD。 - Native Driver 更贴近 Shell,适合聚合管道、运维脚本、对性能极致控制的场景。
- 连接 URI 格式两者相同:
mongodb://host:port/dbname;Atlas 云库为mongodb+srv://...。 - 配套 Mongoose 练习共五个脚本(01 插入~05 查询),与 §4.6 逐步对照。
名词解析:
- Schema:数据结构定义,类似于类的蓝图
- Model:基于 Schema 创建的构造函数,用于操作数据库
- Document:Model 的实例,代表具体的数据库文档
- Instance Methods:文档实例方法
- Static Methods:模型静态方法
- Virtuals:虚拟字段,不实际存储在数据库中
4.2 Mongoose 连接配置
基础连接配置:
javascript
const mongoose = require('mongoose');
// 连接 MongoDB 数据库
mongoose.connect('mongodb://localhost:27017/myDatabase', {
useNewUrlParser: true,
useUnifiedTopology: true
});
// 监听连接事件
mongoose.connection.on('connected', () => {
console.log('MongoDB 连接成功');
});
mongoose.connection.on('error', (err) => {
console.error('MongoDB 连接错误:', err);
});
mongoose.connection.on('disconnected', () => {
console.log('MongoDB 连接断开');
});
【代码注释】
mongoose.connect(uri, options)建立连接池;URI 末尾/myDatabase为默认库名。useNewUrlParser/useUnifiedTopology在 Mongoose 6+ 多为默认,保留是为兼容旧教程写法。connected事件在连接就绪后触发;课堂脚本常用open(历史别名),二者择一监听即可。error应记录完整err便于排查(拒绝连接、认证失败、超时)。disconnected可用于打日志或触发重连;生产环境配合 PM2 / K8s 健康检查。
高级连接配置:
javascript
const mongoose = require('mongoose');
// 连接配置选项
const options = {
useNewUrlParser: true,
useUnifiedTopology: true,
maxPoolSize: 10, // 连接池最大连接数
minPoolSize: 5, // 连接池最小连接数
socketTimeoutMS: 45000, // Socket 超时时间
serverSelectionTimeoutMS: 5000, // 服务器选择超时时间
heartbeatFrequencyMS: 10000, // 心跳频率
retryWrites: true, // 重试写入操作
w: 'majority' // 写确认级别
};
mongoose.connect('mongodb://localhost:27017/myDatabase', options);
// 连接生命周期管理
mongoose.connection.on('connected', () => {
console.log('MongoDB 连接成功');
});
mongoose.connection.on('error', (err) => {
console.error('MongoDB 连接错误:', err);
});
mongoose.connection.on('disconnected', () => {
console.log('MongoDB 连接断开,尝试重连...');
});
// 优雅关闭
process.on('SIGINT', async () => {
await mongoose.connection.close();
console.log('MongoDB 连接已关闭');
process.exit(0);
});
【代码注释】
maxPoolSize/minPoolSize控制连接池大小;并发高时适当增大,避免过多空闲连接占资源。serverSelectionTimeoutMS选主超时;网络抖动时过小易误报连不上。retryWrites: true副本集下写操作失败自动重试(幂等写更安全)。w: 'majority'写关注:多数节点确认后才返回,数据更安全、延迟略高。SIGINT(Ctrl+C)时connection.close()优雅关闭,避免进程强杀导致连接泄漏。- 高级选项在单机课堂可省略;上生产再按副本集、分片调整。
4.3 Schema 定义最佳实践
完整 Schema 定义示例:
javascript
const mongoose = require('mongoose');
const { Schema } = mongoose;
// 用户 Schema 定义
const userSchema = new Schema({
// 基础字段
username: {
type: String,
required: [true, '用户名不能为空'],
unique: true,
trim: true,
minlength: [3, '用户名至少3个字符'],
maxlength: [20, '用户名最多20个字符'],
match: [/^[a-zA-Z0-9_]+$/, '用户名只能包含字母、数字和下划线']
},
email: {
type: String,
required: [true, '邮箱不能为空'],
unique: true,
lowercase: true,
trim: true,
match: [/^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/, '邮箱格式不正确']
},
password: {
type: String,
required: [true, '密码不能为空'],
minlength: [6, '密码至少6个字符']
},
// 个人信息
profile: {
firstName: String,
lastName: String,
avatar: String,
bio: String,
website: String,
location: {
type: {
type: String,
enum: ['Point'],
default: 'Point'
},
coordinates: {
type: [Number],
default: [0, 0]
}
}
},
// 状态字段
status: {
type: String,
enum: ['active', 'inactive', 'suspended'],
default: 'active'
},
role: {
type: String,
enum: ['user', 'admin', 'moderator'],
default: 'user'
},
// 统计字段
stats: {
loginCount: {
type: Number,
default: 0
},
postCount: {
type: Number,
default: 0
}
},
// 时间戳
lastLoginAt: Date,
createdAt: {
type: Date,
default: Date.now
},
updatedAt: {
type: Date,
default: Date.now
}
}, {
// Schema 选项
timestamps: true, // 自动管理 createdAt 和 updatedAt
collection: 'users', // 集合名称
strict: true, // 严格模式,不允许未定义的字段
toJSON: { // 转换为 JSON 时的选项
virtuals: true,
transform: function(doc, ret) {
delete ret.password; // 排除敏感字段
delete ret.__v;
return ret;
}
},
toObject: {
virtuals: true
}
});
// 添加索引
userSchema.index({ username: 1 });
userSchema.index({ email: 1 });
userSchema.index({ 'profile.location': '2dsphere' }); // 地理空间索引
userSchema.index({ createdAt: -1 }); // 时间索引
// 创建 Model
const User = mongoose.model('User', userSchema);
【代码注释】
required: [true, '消息']校验失败时抛出 ValidationError,第二条为自定义错误文案。unique: true在 Schema 层声明唯一;首次启动会尝试建唯一索引,已有重复数据会失败。trim/lowercase在保存前自动处理字符串,减少脏数据。enum限制枚举值;default未传字段时写入默认值。- 嵌套
profile、stats对应 BSON 子文档;location为 GeoJSON 结构,配2dsphere索引。 - Schema 选项
timestamps: true自动维护createdAt/updatedAt;toJSON.transform可剔除password再返回 API。 mongoose.model('User', userSchema)模型名单数,集合名默认复数小写 users (可collection覆盖)。
4.4 Mongoose 中间件机制
中间件类型:
- Document Middleware:文档中间件(init、validate、save、remove)
- Query Middleware:查询中间件(count、find、findOne、update...)
- Aggregate Middleware:聚合中间件
- Model Middleware:模型中间件
中间件示例:
javascript
// Pre-save 中间件
userSchema.pre('save', function(next) {
// 在保存前执行
console.log('准备保存用户...');
// 如果密码被修改,进行加密
if (this.isModified('password')) {
this.password = bcrypt.hashSync(this.password, 10);
}
// 更新时间戳
this.updatedAt = Date.now();
next();
});
// Post-save 中间件
userSchema.post('save', function(doc) {
console.log('用户保存完成:', doc.username);
});
// Pre-find 中间件
userSchema.pre('find', function() {
console.log('执行查询操作');
this.where({ status: 'active' }); // 只查询活跃用户
});
// Pre-validate 中间件
userSchema.pre('validate', function(next) {
if (this.username === 'admin') {
this.invalidate('username', '不能使用 admin 作为用户名');
}
next();
});
// Pre-deleteOne 中间件
userSchema.pre('deleteOne', function(next) {
console.log('准备删除用户');
next();
});
【代码注释】
pre('save')在文档 save 前执行;this指向当前文档实例。this.isModified('password')避免每次 save 都重复加密;仅密码变更时 hash。next()必须调用(或返回 Promise),否则中间件挂起;忘记next是常见坑。post('save')在保存成功后执行,可做日志、发消息队列(勿放耗时阻塞逻辑)。pre('find')中this是 Query,可用this.where()注入全局过滤(如只查status: active)。pre('validate')里this.invalidate(path, msg)手动标记字段非法。pre('deleteOne')在 Mongoose 6+ 对应 Query 中间件,注意与 Document 的remove钩子区分。
4.5 实例方法和静态方法
实例方法:
javascript
// 添加实例方法
userSchema.methods.comparePassword = function(candidatePassword) {
const isMatch = bcrypt.compareSync(candidatePassword, this.password);
return isMatch;
};
userSchema.methods.updateProfile = function(profileData) {
this.profile = { ...this.profile, ...profileData };
return this.save();
};
userSchema.methods.incrementLoginCount = function() {
this.stats.loginCount += 1;
this.lastLoginAt = new Date();
return this.save();
};
// 使用实例方法
const user = await User.findOne({ username: 'john_doe' });
const isMatch = user.comparePassword('password123');
if (isMatch) {
await user.incrementLoginCount();
}
【代码注释】
schema.methods.xxx挂在文档实例 上,内部用this访问当前用户字段。comparePassword用 bcrypt 比对明文与库中 hash,登录流程标准写法。updateProfile合并对象后save()触发校验与pre('save')中间件。incrementLoginCount改统计字段并save();高频计数可考虑updateOne+$inc减少读-改-写。- 调用前需
await User.findOne(...)得到文档;静态方法不能写在实例方法里混用this语义。
静态方法:
javascript
// 添加静态方法
userSchema.statics.findByUsername = function(username) {
return this.findOne({ username: username });
};
userSchema.statics.findActiveUsers = function() {
return this.find({ status: 'active' });
};
userSchema.statics.findByEmail = function(email) {
return this.findOne({ email: email });
};
userSchema.statics.getUsersWithRole = function(role) {
return this.find({ role: role });
};
// 使用静态方法
const user = await User.findByUsername('john_doe');
const activeUsers = await User.findActiveUsers();
【代码注释】
schema.statics.xxx挂在 Model 上,函数内this即User模型,可return this.findOne(...)。- 静态方法适合封装常用查询(按用户名、邮箱、角色),路由层代码更简洁。
findByUsername返回 Query,可继续链式.select();需文档实例时再await。- 与实例方法分工:静态负责「查谁」,实例负责「当前这条数据做什么」。
4.6 Mongoose 五步实战:01~05 脚本对照
五个 Node 脚本共用同一骨架,仅 Model 名、集合、CRUD 方法 不同。下表便于对照运行顺序:
| 脚本 | 集合 | 核心 API | 数据准备 |
|---|---|---|---|
| 01-操作步骤 | users |
create 单条 |
手写对象(高小乐) |
| 02-批量插入数据 | songs |
create(数组) |
同目录 data.json → song_list |
| 03-删除数据 | songs |
deleteOne / deleteMany |
先跑 02 再有数据 |
| 04-更新数据 | songs |
updateOne / updateMany |
按 author 批量改名 |
| 05-查询数据 | songs |
find 链式 + exec |
skip(90).limit(10) 分页实验 |
运行前:npm install mongoose,mongod 已启动,库名 project04 与 connect URI 一致。
标准五步(所有脚本共用):
MongoDB Mongoose Node 脚本 MongoDB Mongoose Node 脚本 #mermaid-svg-foxA05dmCX9H8JAH{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-foxA05dmCX9H8JAH .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-foxA05dmCX9H8JAH .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-foxA05dmCX9H8JAH .error-icon{fill:#552222;}#mermaid-svg-foxA05dmCX9H8JAH .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-foxA05dmCX9H8JAH .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-foxA05dmCX9H8JAH .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-foxA05dmCX9H8JAH .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-foxA05dmCX9H8JAH .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-foxA05dmCX9H8JAH .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-foxA05dmCX9H8JAH .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-foxA05dmCX9H8JAH .marker{fill:#333333;stroke:#333333;}#mermaid-svg-foxA05dmCX9H8JAH .marker.cross{stroke:#333333;}#mermaid-svg-foxA05dmCX9H8JAH svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-foxA05dmCX9H8JAH p{margin:0;}#mermaid-svg-foxA05dmCX9H8JAH .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-foxA05dmCX9H8JAH text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-foxA05dmCX9H8JAH .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-foxA05dmCX9H8JAH .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-foxA05dmCX9H8JAH .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-foxA05dmCX9H8JAH .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-foxA05dmCX9H8JAH #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-foxA05dmCX9H8JAH .sequenceNumber{fill:white;}#mermaid-svg-foxA05dmCX9H8JAH #sequencenumber{fill:#333;}#mermaid-svg-foxA05dmCX9H8JAH #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-foxA05dmCX9H8JAH .messageText{fill:#333;stroke:none;}#mermaid-svg-foxA05dmCX9H8JAH .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-foxA05dmCX9H8JAH .labelText,#mermaid-svg-foxA05dmCX9H8JAH .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-foxA05dmCX9H8JAH .loopText,#mermaid-svg-foxA05dmCX9H8JAH .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-foxA05dmCX9H8JAH .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-foxA05dmCX9H8JAH .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-foxA05dmCX9H8JAH .noteText,#mermaid-svg-foxA05dmCX9H8JAH .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-foxA05dmCX9H8JAH .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-foxA05dmCX9H8JAH .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-foxA05dmCX9H8JAH .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-foxA05dmCX9H8JAH .actorPopupMenu{position:absolute;}#mermaid-svg-foxA05dmCX9H8JAH .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-foxA05dmCX9H8JAH .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-foxA05dmCX9H8JAH .actor-man circle,#mermaid-svg-foxA05dmCX9H8JAH line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-foxA05dmCX9H8JAH :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} require + connect TCP 27017 event open Schema → model create / find / update / delete
【代码注释】(流程图)
- 顺序固定:先 connect → 等 open → 再定义 Schema/Model → 再 CRUD;未连上就操作会报错。
- Mongoose 维护连接池,Node 进程与 MongoDB 之间通常一条 TCP 长连接(池内多条)。
- 课堂脚本在
open回调里写业务,避免竞态;Express 项目在bin/www里连库成功后再listen。
① 连接并插入单条(users 集合):
javascript
const mongoose = require('mongoose');
mongoose.set('strictQuery', false);
mongoose.connect('mongodb://127.0.0.1:27017/project04');
mongoose.connection.on('open', () => {
const usersSchema = new mongoose.Schema({
name: String,
age: Number,
address: String,
ctime: Date
});
const usersModel = mongoose.model('users', usersSchema);
usersModel.create({
name: '高小乐',
age: 101,
address: '上海',
ctime: new Date()
}, (err, res) => {
if (err) console.log('添加失败');
else console.log('添加成功', res);
});
});
mongoose.connection.on('error', err => {
console.log('数据库连接失败');
throw err;
});
【代码注释】
mongoose.set('strictQuery', false)关闭「空 filter 被过滤」的严格模式,消除 Mongoose 7+ 警告。connect('mongodb://127.0.0.1:27017/project04')库名project04与 01-操作步骤 脚本一致,可改。- 必须在
connection.on('open')内再Schema/model/create,否则可能尚未连上就写库。 model('users', usersSchema)第三参数集合名默认 users ;模型名users与集合对应。create({...}, callback)成功时res为单条文档;err常见为校验失败、重复键、连接断开。- 回调风格为课堂写法;等价 Promise:
await usersModel.create({ name: '高小乐', ... })。
② 批量插入(songs 集合 + 本地 JSON 数据):
javascript
mongoose.connection.on('open', () => {
const songsSchema = new mongoose.Schema({
author: String,
language: String,
duration: Number,
hot: Number,
title: String
});
const songsModel = mongoose.model('songs', songsSchema);
songsModel.create(require('./data.json').song_list, (err, res) => {
if (err) console.log('批量添加失败');
else console.log('批量添加成功', res.length);
});
});
【代码注释】
- 与 ① 相同:在
open里定义songsSchema与songsModel,集合名为 songs。 create(require('./data.json').song_list)传入对象数组 即insertMany语义。data.json结构需含song_list字段,每项字段与 Schema(author、title、hot 等)对应。- 成功时
res为文档数组,res.length为插入条数;路径错误会MODULE_NOT_FOUND。 - 重复运行会重复插入;课堂可先
deleteMany({})再批量插入做实验。
③ 查询、排序、分页:
javascript
// 单条
songsModel.findOne({ author: '高小乐' }, callback);
songsModel.findById('647ae9d013ab34ca2595e162', callback);
// 多条 + 字段筛选 + 排序 + 分页
songsModel.find({ hot: { $gt: 500 } })
.select({ _id: 0, author: 1, title: 1 })
.sort({ hot: 1 }) // 1 升序,-1 降序
.skip(90)
.limit(10)
.exec(callback);
【代码注释】
findOne({ author: '高小乐' })返回单条或null;findById(id)按_id查,id 为 24 位十六进制字符串。find({ hot: { $gt: 500 } })与 Shell 相同,Mongoose 支持$gt等算子。.select({ _id: 0, author: 1, title: 1 })投影:1 包含、0 排除,减轻网络传输。.sort({ hot: 1 })按热度升序,-1降序;排序字段建议有索引。.skip(90).limit(10)第 10 页(每页 10 条);skip 过大时性能差,生产可用基于_id的游标分页。.exec(callback)显式执行;亦可await songsModel.find(...).exec()或省略 exec 直接 await Query。
④ 更新与删除:
javascript
songsModel.updateOne({ author: 'JJ Lin' }, { author: '高小乐' }, callback);
songsModel.updateMany({ author: 'Leehom Wang' }, { author: '高小乐' }, callback);
songsModel.deleteOne({ _id: '5dd65f32be6401035cb5b1ed' }, callback);
songsModel.deleteMany({ author: 'Jay' }, callback);
【代码注释】
updateOne(条件, 更新)只改第一条匹配;课堂简写{ author: '高小乐' }在部分版本会当$set用,规范写法为{ $set: { author: '高小乐' } }。updateMany改所有匹配文档,批量改名场景;注意误匹配范围。deleteOne({ _id: '...' })按主键删一条;_id须与库中 ObjectId 字符串一致。deleteMany({ author: 'Jay' })按条件删多条,不可恢复 ;删前可用find预览。- 回调
(err, res)中res.deletedCount/modifiedCount可判断是否真的删改成功。
Schema 类型速查(课堂):
| 类型 | 说明 |
|---|---|
| String | 字符串 |
| Number | 数字 |
| Boolean | 布尔 |
| Array | 数组,也可 [] |
| Date | 日期 |
| ObjectId | mongoose.Schema.Types.ObjectId |
| Mixed | 任意类型 |
【实战要点】
- 经典应用场景 :用户中心用 Schema 校验手机号格式;订单状态用
enum限制;统计字段用$inc而非读-改-写。 - 常见坑 :在
open回调外 定义 Model 仍可能连上,但在open前create会竞态失败;业务写在open内或await mongoose.connect()(Mongoose 6+)。 - 性能与最佳实践 :列表只读用
.lean();批量导入用insertMany/create(数组)而非循环create。
【面试考点】
Q1:Mongoose 中 Schema 和 Model 的区别?
A:Schema 是结构蓝图(类型、校验、默认值);mongoose.model('User', schema) 得到 Model,对应集合(默认复数小写 users),用于 find/create 等。追问:实例方法 methods 挂在文档上,静态方法 statics 挂在 Model 上。
【本章小结】
| 概念 | 挂在哪 | 作用 | 类比 |
|---|---|---|---|
| Schema | ------ | 定义结构、类型、校验、默认值 | 设计图纸 |
| Model | 由 Schema 生成 | 操作集合(find/create...) |
按图纸开的工厂 |
| Document | Model 实例 | 一条具体数据 | 工厂产出的产品 |
methods |
Document | 「这条数据自己」能做什么 | 产品的功能 |
statics |
Model | 「针对整个集合」的封装查询 | 工厂的生产线 |
中间件 pre/post |
Schema | 在 save / find 等前后插入逻辑 | 流水线质检工位 |
记忆口诀 :「Schema 画图、Model 建厂、Document 是货 」;五步流程「连接 → 监听 open → 定 Schema → 建 Model → CRUD 」一步都不能乱,业务代码务必写在 open 之后。
5. 高级查询与性能优化
5.1 高级查询技巧
聚合框架示例:
javascript
// 管道操作示例
const result = await User.aggregate([
// $match - 过滤阶段
{
$match: {
status: 'active',
age: { $gte: 18 }
}
},
// $group - 分组阶段
{
$group: {
_id: '$role',
count: { $sum: 1 },
avgAge: { $avg: '$age' }
}
},
// $sort - 排序阶段
{
$sort: { count: -1 }
},
// $limit - 限制结果
{
$limit: 10
}
]);
// 复杂聚合示例:用户行为分析
const behaviorAnalysis = await User.aggregate([
{
$match: {
'stats.lastLoginAt': {
$gte: new Date('2024-01-01')
}
}
},
{
$project: {
username: 1,
role: 1,
loginCount: '$stats.loginCount',
lastLogin: '$stats.lastLoginAt',
accountAge: {
$divide: [
{ $subtract: [new Date(), '$createdAt'] },
1000 * 60 * 60 * 24 // 转换为天数
]
}
}
},
{
$group: {
_id: '$role',
totalUsers: { $sum: 1 },
totalLogins: { $sum: '$loginCount' },
avgAccountAge: { $avg: '$accountAge' }
}
}
]);
【代码注释】
aggregate([...])为聚合管道 ,数据按阶段依次流过$match→$group→$sort→$limit。$match宜放管道前部,先过滤减少后续数据量(类似 SQL WHERE)。$group的_id为分组键,$sum: 1计数,$avg求平均;$role前加$表示字段引用。- 第二段示例
$project计算accountAge(注册天数),$subtract/$divide为表达式算子。 - 聚合在数据库端完成,比查出全部再在 Node 里统计更省内存与带宽。
- 复杂统计、报表、大屏数据常用聚合;简单条件仍用
find即可。
5.2 性能优化策略
索引优化:
javascript
// 创建复合索引
userSchema.index({ username: 1, status: 1 });
// 创建唯一索引
userSchema.index({ email: 1 }, { unique: true });
// 创建稀疏索引(只包含有该字段的文档)
userSchema.index({ 'profile.website': 1 }, { sparse: true });
// 创建 TTL 索引(自动过期)
const sessionSchema = new Schema({
userId: Schema.Types.ObjectId,
createdAt: { type: Date, default: Date.now }
});
sessionSchema.index({ createdAt: 1 }, { expireAfterSeconds: 3600 });
// 查看索引使用情况
User.collection.getIndexes()
【代码注释】
- 复合索引
{ username: 1, status: 1 }适合「按用户名且状态」联合查询。 { unique: true }在索引层保证唯一;与 Schemaunique二选一或配合使用,避免重复建索引。sparse: true稀疏索引:仅索引存在该字段的文档,适合可选字段(如 website)。- TTL 索引
expireAfterSeconds: 3600使文档在createdAt约 1 小时后自动删除,适合 session、验证码。 getIndexes()列出当前集合索引,上线前检查是否多余索引(写放大)。
查询优化技巧:
javascript
// 1. 使用投影减少返回字段
const users = await User.find({}, { username: 1, email: 1 });
// 2. 使用 lean() 返回普通 JavaScript 对象
const users = await User.find().lean();
// 3. 使用 cursor 处理大量数据
const cursor = User.find().cursor();
cursor.eachAsync(doc => {
console.log(doc.username);
return Promise.resolve();
});
// 4. 使用 select() 明确选择字段
const users = await User.find()
.select('username email profile')
.lean();
// 5. 避免使用正则表达式前通配符
// 不推荐
db.users.find({ username: /.*john.*/ })
// 推荐
db.users.find({ username: /john.*/ })
【代码注释】
- 投影
find({}, { username: 1, email: 1 })只取必要字段,减少序列化与传输开销。 .lean()返回普通 JS 对象而非 Mongoose Document,只读列表页性能更好,无save方法。cursor+eachAsync流式处理百万级数据,避免find().then一次载入内存。.select('username email profile')空格分隔字段名,等价投影简写。- 正则避免前导
.*;^john或前缀匹配更易利用索引(若建了合适索引)。
5.3 聚合管道执行原理
聚合(aggregation)是 MongoDB 最被低估的能力。很多人把全部数据 find 出来再在 Node 里 reduce 统计------数据量一大就内存爆炸。聚合的本质是:让数据库在数据所在地完成计算,只把结果传回来。
一、管道 = 流水线
#mermaid-svg-eA2vew1h9rUwgRo4{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-eA2vew1h9rUwgRo4 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-eA2vew1h9rUwgRo4 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-eA2vew1h9rUwgRo4 .error-icon{fill:#552222;}#mermaid-svg-eA2vew1h9rUwgRo4 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-eA2vew1h9rUwgRo4 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-eA2vew1h9rUwgRo4 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-eA2vew1h9rUwgRo4 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-eA2vew1h9rUwgRo4 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-eA2vew1h9rUwgRo4 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-eA2vew1h9rUwgRo4 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-eA2vew1h9rUwgRo4 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-eA2vew1h9rUwgRo4 .marker.cross{stroke:#333333;}#mermaid-svg-eA2vew1h9rUwgRo4 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-eA2vew1h9rUwgRo4 p{margin:0;}#mermaid-svg-eA2vew1h9rUwgRo4 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-eA2vew1h9rUwgRo4 .cluster-label text{fill:#333;}#mermaid-svg-eA2vew1h9rUwgRo4 .cluster-label span{color:#333;}#mermaid-svg-eA2vew1h9rUwgRo4 .cluster-label span p{background-color:transparent;}#mermaid-svg-eA2vew1h9rUwgRo4 .label text,#mermaid-svg-eA2vew1h9rUwgRo4 span{fill:#333;color:#333;}#mermaid-svg-eA2vew1h9rUwgRo4 .node rect,#mermaid-svg-eA2vew1h9rUwgRo4 .node circle,#mermaid-svg-eA2vew1h9rUwgRo4 .node ellipse,#mermaid-svg-eA2vew1h9rUwgRo4 .node polygon,#mermaid-svg-eA2vew1h9rUwgRo4 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-eA2vew1h9rUwgRo4 .rough-node .label text,#mermaid-svg-eA2vew1h9rUwgRo4 .node .label text,#mermaid-svg-eA2vew1h9rUwgRo4 .image-shape .label,#mermaid-svg-eA2vew1h9rUwgRo4 .icon-shape .label{text-anchor:middle;}#mermaid-svg-eA2vew1h9rUwgRo4 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-eA2vew1h9rUwgRo4 .rough-node .label,#mermaid-svg-eA2vew1h9rUwgRo4 .node .label,#mermaid-svg-eA2vew1h9rUwgRo4 .image-shape .label,#mermaid-svg-eA2vew1h9rUwgRo4 .icon-shape .label{text-align:center;}#mermaid-svg-eA2vew1h9rUwgRo4 .node.clickable{cursor:pointer;}#mermaid-svg-eA2vew1h9rUwgRo4 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-eA2vew1h9rUwgRo4 .arrowheadPath{fill:#333333;}#mermaid-svg-eA2vew1h9rUwgRo4 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-eA2vew1h9rUwgRo4 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-eA2vew1h9rUwgRo4 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-eA2vew1h9rUwgRo4 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-eA2vew1h9rUwgRo4 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-eA2vew1h9rUwgRo4 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-eA2vew1h9rUwgRo4 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-eA2vew1h9rUwgRo4 .cluster text{fill:#333;}#mermaid-svg-eA2vew1h9rUwgRo4 .cluster span{color:#333;}#mermaid-svg-eA2vew1h9rUwgRo4 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-eA2vew1h9rUwgRo4 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-eA2vew1h9rUwgRo4 rect.text{fill:none;stroke-width:0;}#mermaid-svg-eA2vew1h9rUwgRo4 .icon-shape,#mermaid-svg-eA2vew1h9rUwgRo4 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-eA2vew1h9rUwgRo4 .icon-shape p,#mermaid-svg-eA2vew1h9rUwgRo4 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-eA2vew1h9rUwgRo4 .icon-shape .label rect,#mermaid-svg-eA2vew1h9rUwgRo4 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-eA2vew1h9rUwgRo4 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-eA2vew1h9rUwgRo4 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-eA2vew1h9rUwgRo4 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 集合全部文档
$match
过滤
$group
分组聚合
$sort
排序
$limit
截断
结果文档
【代码注释】(聚合管道图)数据像水流一样依次穿过每个阶段 ,上一阶段的输出就是下一阶段的输入。最重要的优化原则藏在顺序里:$match 必须尽量靠前 。如果先 $group 再 $match,等于先把全集合 100 万条都分组算一遍、再丢掉大半;先 $match 过滤到 1 万条再 $group,计算量差 100 倍。MongoDB 的查询优化器在某些情况下会自动把 $match 前移,但不要依赖它 ,手写时就放对位置。$match 阶段在最前且字段有索引时,还能直接用上 IXSCAN 。市面应用:电商「按品类统计销售额」、运营「按日活跃用户数」、记账本「按收支类型汇总金额」全靠聚合管道。
二、聚合 vs find:什么时候用哪个
| 需求 | 用什么 |
|---|---|
| 按条件取若干文档原样返回 | find |
| 分组、求和、求平均、计数 | aggregate + $group |
| 跨集合「关联」 | aggregate + $lookup |
| 计算派生字段(如注册天数) | aggregate + $project |
实战示例 :把 §7 记账本的「按收支类型统计」做成可视化。保存为 aggregation-demo.html 放课程目录,双击运行:
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>聚合管道分组统计模拟</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 720px; margin: 2rem auto; padding: 0 1rem; }
button { margin: 8px 8px 8px 0; padding: 8px 16px; cursor: pointer; }
pre { background: #f4f4f4; padding: 12px; border-radius: 6px; overflow: auto; }
</style>
</head>
<body>
<h1>$group 分组统计演示</h1>
<p>模拟 <code>accounts</code> 集合,复现聚合管道 <code>$match → $group</code> 的计算过程。</p>
<button type="button" id="run">运行聚合管道</button>
<pre id="pipe"></pre>
<pre id="out"></pre>
<script>
// 模拟集合中的账单文档
const accounts = [
{ title: '工资', type: 1, account: 8000 },
{ title: '兼职', type: 1, account: 500 },
{ title: '午餐', type: -1, account: 35 },
{ title: '打车', type: -1, account: 22 },
{ title: '购物', type: -1, account: 260 }
];
document.getElementById('pipe').textContent =
'聚合管道:\n[\n { $match: { account: { $gt: 0 } } },\n' +
' { $group: { _id: "$type", total: { $sum: "$account" }, count: { $sum: 1 } } }\n]';
document.getElementById('run').onclick = () => {
// $match:过滤金额大于 0
const matched = accounts.filter(a => a.account > 0);
// $group:按 type 分组,累加金额与笔数
const grouped = {};
matched.forEach(a => {
if (!grouped[a.type]) grouped[a.type] = { _id: a.type, total: 0, count: 0 };
grouped[a.type].total += a.account; // 对应 $sum: "$account"
grouped[a.type].count += 1; // 对应 $sum: 1
});
document.getElementById('out').textContent =
'聚合结果:\n' + JSON.stringify(Object.values(grouped), null, 2);
};
</script>
</body>
</html>
【代码注释】这段 JS 一比一复刻了聚合管道的两个阶段:accounts.filter(a => a.account > 0) 就是 $match;接着用一个 grouped 对象按 type 归并,total += a.account 对应 $sum: "$account"、count += 1 对应 $sum: 1,最后 Object.values 取出分组结果------这正是 $group 的内部逻辑。为什么这样写 :在浏览器里把「黑盒」的聚合拆成看得见的循环,理解后再写真实的 Account.aggregate([...]) 就不再陌生。市面应用 :记账本的「本月收入 / 支出总额」、运营后台的「各渠道转化数」都是这套 $match → $group 模板;区别只是真实场景里这段计算跑在 MongoDB 服务端,不占 Node 内存。
【实战要点】
- 经典应用场景 :数据大屏、月度报表、漏斗分析全用聚合;
$lookup做跨集合关联(如账单带出用户名);$project计算派生字段(注册天数、订单时长)。 - 常见坑 :①
$match放在$group之后,白算一遍再丢弃,性能差几十倍;② 聚合结果集过大时受限制,超大结果要加$limit或开allowDiskUse;③ 误以为.lean()能加速聚合------aggregate本就返回纯对象,无需lean。 - 性能与最佳实践 :能用
find就别用aggregate;用聚合时把$match、$sort尽量前移并让其字段有索引;只读列表用.lean(),大结果集用游标cursor()流式处理。
【本章小结】
| 优化手段 | 作用 | 适用场景 |
|---|---|---|
| 索引 | 查询 O(n) → O(log n) | 所有高频查询字段 |
投影 / select |
减少返回字段与传输量 | 列表页只需部分字段 |
.lean() |
跳过 Mongoose 文档包装 | 只读、不需 save 的查询 |
cursor 游标 |
流式处理不撑内存 | 百万级数据遍历 |
| 聚合管道 | 计算下推到数据库 | 分组、统计、报表 |
记忆口诀 :查询优化「索引打底、投影瘦身、lean 提速、游标兜大数据 」;聚合记住一句话------「$match 越早越好」。
【面试考点】
Q1:什么时候该用聚合管道而不是 find?
A:find 只能「按条件取原始文档」;一旦需要分组、求和、求平均、计数、跨集合关联或计算派生字段 ,就该用 aggregate。核心收益是计算下推 ------在数据所在的数据库端算完只传结果,而不是把全量数据查到 Node 再 reduce,后者数据一大就内存溢出。
Q2:聚合管道里 $match 为什么要尽量放前面?
A:管道是流水线,每个阶段处理上一阶段的全部输出。$match 在前,先把数据过滤少,后续 $group、$sort 的计算量随之骤降;放在 $group 之后则是「先全量计算再丢弃」,可能差几十上百倍。同理 $match 在最前且字段有索引时还能直接走 IXSCAN。
6. 生产环境最佳实践
6.1 连接池管理
javascript
// 连接池配置最佳实践
const mongoose = require('mongoose');
const options = {
// 连接池设置
maxPoolSize: 50, // 最大连接数
minPoolSize: 10, // 最小连接数
socketTimeoutMS: 45000, // Socket 超时
serverSelectionTimeoutMS: 5000, // 服务器选择超时
heartbeatFrequencyMS: 10000, // 心跳频率
connectTimeoutMS: 10000, // 连接超时
// 重试设置
retryWrites: true, // 重试写入
retryReads: true, // 重试读取
// 写确认级别
w: 'majority', // 多数节点确认
j: true, // 写入日志
// 读设置
readPreference: 'primaryPreferred', // 优先主节点
readConcern: 'majority' // 读关注级别
};
mongoose.connect('mongodb://localhost:27017/myDatabase', options);
【代码注释】
- 连接池
maxPoolSize按 QPS 与实例数调优;过小排队,过大占满 MongoDB 连接数上限。 retryReads/retryWrites在副本集故障切换时提高可用性。w: 'majority'+j: true保证多数节点落盘且写 journal,一致性更强。readPreference: 'primaryPreferred'优先读主节点,主不可用时读从节点(需副本集)。readConcern: 'majority'避免读到未提交的脏数据(高级场景,课堂了解即可)。
6.2 事务处理
javascript
// MongoDB 事务示例
const session = await mongoose.startSession();
try {
await session.withTransaction(async () => {
// 转账操作
const fromAccount = await Account.findOneAndUpdate(
{ userId: fromUserId },
{ $inc: { balance: -amount } },
{ session }
);
const toAccount = await Account.findOneAndUpdate(
{ userId: toUserId },
{ $inc: { balance: amount } },
{ session }
);
// 记录交易
await Transaction.create([{
fromUserId,
toUserId,
amount,
timestamp: new Date()
}], { session });
});
console.log('事务执行成功');
} catch (error) {
console.error('事务执行失败:', error);
} finally {
session.endSession();
}
【代码注释】
- 事务需 副本集或分片集群;单机 MongoDB 4.0+ 也支持,课堂单机可了解语法。
startSession()创建会话;withTransaction内所有写操作传{ session }同一上下文。findOneAndUpdate+$inc实现转账扣款/加款;任一步失败则整事务回滚。Transaction.create([...], { session })数组形式插入日志表,与账户更新同事务提交。finally中endSession()释放会话,避免泄漏。
6.3 数据备份与恢复
bash
# 数据备份
mongodump --uri="mongodb://localhost:27017/myDatabase" --out=/backup/mongodb/
# 数据恢复
mongorestore --uri="mongodb://localhost:27017" /backup/mongodb/
# 导出集合为 JSON
mongoexport --db=myDatabase --collection=users --out=users.json
# 导入 JSON 数据
mongoimport --db=myDatabase --collection=users --file=users.json
【代码注释】
mongodump --uri=... --out=目录导出 BSON 快照,含索引与元数据,适合全库/全实例备份。mongorestore将 dump 目录写回;恢复前确认目标库名,避免覆盖生产。mongoexport单集合导出 JSON/CSV,便于人工查看或与 MySQL 等交换。mongoimport从文件导入;字段类型以 JSON 为准,日期字符串不会自动变 Date。- 生产建议定时任务 + 异地存储 + 恢复演练;大库用
--gzip压缩(了解即可)。
6.4 认证与安全连接
启用认证启动:
bash
mongod --dbpath /path/to/data --auth
【代码注释】
--auth启用访问控制;未创建用户前可先无认证建库,再createUser后重启带--auth。- 数据目录
--dbpath须存在且进程有读写权限。
创建管理员:
javascript
use admin
db.createUser({
user: "admin",
pwd: "password",
roles: ["root"]
})
【代码注释】
- 在 admin 库执行
createUser;roles: ["root"]为超级管理员,可管理所有库。 pwd明文仅课堂演示;生产用强密码且勿写入仓库。- 创建后需
mongod --auth重启,后续连接须带用户名密码。
javascript
// Shell 内认证
use admin
db.auth("admin", "password")
【代码注释】
use admin切换到认证库;db.auth(用户, 密码)返回 1 表示成功、0 表示失败。- 认证通过后才能对业务库执行 CRUD;未认证时多数写操作被拒绝。
Mongoose 带认证连接:
javascript
mongoose.connect('mongodb://admin:password@127.0.0.1:27017/prepare?authSource=admin');
【代码注释】
- URI 格式
mongodb://用户名:密码@主机:端口/业务库?authSource=admin。 - 业务库 为 URI 路径
/prepare;认证库 由authSource=admin指定,用户记录在 admin。 - 密码含特殊字符需 URL 编码(如
@→%40)。 - Node 项目用
process.env.MONGODB_URI,勿把密码提交 Git;.env加入.gitignore。
连接池工作方式:
#mermaid-svg-NZklcOdnI5yHYjyC{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-NZklcOdnI5yHYjyC .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-NZklcOdnI5yHYjyC .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-NZklcOdnI5yHYjyC .error-icon{fill:#552222;}#mermaid-svg-NZklcOdnI5yHYjyC .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-NZklcOdnI5yHYjyC .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-NZklcOdnI5yHYjyC .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-NZklcOdnI5yHYjyC .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-NZklcOdnI5yHYjyC .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-NZklcOdnI5yHYjyC .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-NZklcOdnI5yHYjyC .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-NZklcOdnI5yHYjyC .marker{fill:#333333;stroke:#333333;}#mermaid-svg-NZklcOdnI5yHYjyC .marker.cross{stroke:#333333;}#mermaid-svg-NZklcOdnI5yHYjyC svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-NZklcOdnI5yHYjyC p{margin:0;}#mermaid-svg-NZklcOdnI5yHYjyC .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-NZklcOdnI5yHYjyC .cluster-label text{fill:#333;}#mermaid-svg-NZklcOdnI5yHYjyC .cluster-label span{color:#333;}#mermaid-svg-NZklcOdnI5yHYjyC .cluster-label span p{background-color:transparent;}#mermaid-svg-NZklcOdnI5yHYjyC .label text,#mermaid-svg-NZklcOdnI5yHYjyC span{fill:#333;color:#333;}#mermaid-svg-NZklcOdnI5yHYjyC .node rect,#mermaid-svg-NZklcOdnI5yHYjyC .node circle,#mermaid-svg-NZklcOdnI5yHYjyC .node ellipse,#mermaid-svg-NZklcOdnI5yHYjyC .node polygon,#mermaid-svg-NZklcOdnI5yHYjyC .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-NZklcOdnI5yHYjyC .rough-node .label text,#mermaid-svg-NZklcOdnI5yHYjyC .node .label text,#mermaid-svg-NZklcOdnI5yHYjyC .image-shape .label,#mermaid-svg-NZklcOdnI5yHYjyC .icon-shape .label{text-anchor:middle;}#mermaid-svg-NZklcOdnI5yHYjyC .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-NZklcOdnI5yHYjyC .rough-node .label,#mermaid-svg-NZklcOdnI5yHYjyC .node .label,#mermaid-svg-NZklcOdnI5yHYjyC .image-shape .label,#mermaid-svg-NZklcOdnI5yHYjyC .icon-shape .label{text-align:center;}#mermaid-svg-NZklcOdnI5yHYjyC .node.clickable{cursor:pointer;}#mermaid-svg-NZklcOdnI5yHYjyC .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-NZklcOdnI5yHYjyC .arrowheadPath{fill:#333333;}#mermaid-svg-NZklcOdnI5yHYjyC .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-NZklcOdnI5yHYjyC .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-NZklcOdnI5yHYjyC .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NZklcOdnI5yHYjyC .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-NZklcOdnI5yHYjyC .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NZklcOdnI5yHYjyC .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-NZklcOdnI5yHYjyC .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-NZklcOdnI5yHYjyC .cluster text{fill:#333;}#mermaid-svg-NZklcOdnI5yHYjyC .cluster span{color:#333;}#mermaid-svg-NZklcOdnI5yHYjyC div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-NZklcOdnI5yHYjyC .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-NZklcOdnI5yHYjyC rect.text{fill:none;stroke-width:0;}#mermaid-svg-NZklcOdnI5yHYjyC .icon-shape,#mermaid-svg-NZklcOdnI5yHYjyC .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NZklcOdnI5yHYjyC .icon-shape p,#mermaid-svg-NZklcOdnI5yHYjyC .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-NZklcOdnI5yHYjyC .icon-shape .label rect,#mermaid-svg-NZklcOdnI5yHYjyC .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NZklcOdnI5yHYjyC .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-NZklcOdnI5yHYjyC .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-NZklcOdnI5yHYjyC :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Node 进程
请求1
连接池
maxPoolSize 条 TCP 长连接
请求2
请求3
MongoDB 27017
【代码注释】(连接池图)很多人误以为 mongoose.connect 只建一条 连接------实际上它建的是一个连接池 。多个并发请求来时,各自从池里借 一条空闲连接用,用完还 回去,而不是每个请求都新建 TCP(三次握手 + 认证开销巨大)。maxPoolSize 就是池子上限:太小则高并发时请求排队等连接,太大则占满 MongoDB 的连接配额。为什么 Node 单线程也需要连接池 :Node 虽单线程执行 JS,但 I/O 是异步并发的------同一时刻可以有几十个查询「在路上」,每个都需要一条连接来收发数据。市面应用 :所有数据库客户端(MySQL 的 mysql2 连接池、Redis 客户端)都用连接池,这是服务端访问数据库的标准姿势;切忌「每次查询前 connect、查完 close」。
【实战要点】
- 经典应用场景 :转账、下单减库存等「多步要么全成、要么全败」的操作用事务 ;session、验证码、临时令牌用 TTL 索引 自动过期;线上配置用
process.env+.env注入连接串。 - 常见坑 :① 在请求处理函数里反复
mongoose.connect,连接数暴涨直至 MongoDB 拒绝连接------connect全进程只调一次;② 单机 MongoDB 直接用事务会报错,事务需要副本集;③ 把含密码的 URI 硬编码并提交到 Git,造成凭据泄露。 - 性能与最佳实践 :
maxPoolSize按并发量压测调优;w: 'majority'保安全、w: 1保延迟,按业务取舍;备份要「定时 + 异地 + 定期做恢复演练」,没演练过的备份等于没有备份。
【本章小结】
| 主题 | 关键点 | 红线 |
|---|---|---|
| 连接池 | 全进程连一次,池内复用 TCP | 不要每请求 connect / close |
| 事务 | withTransaction 内传 { session } |
需副本集,单机不支持 |
| 备份 | mongodump / mongorestore |
必须做恢复演练 |
| 认证 | --auth + authSource 指定认证库 |
密码不进 Git |
记忆口诀 :生产四件事「池子复用、事务兜底、备份能恢、密码进环境变量」。开发期可全跳过,但上线前必须逐项过一遍。
【面试考点】
Q1:为什么数据库访问要用连接池?Node 是单线程,开一条连接不够吗?
A:不够。Node 执行 JS 是单线程,但 I/O 是异步并发的------同一时刻可能有几十个查询同时在等数据库响应,每个都需要一条独立连接收发数据。连接池预先维护若干条 TCP 长连接供请求借还,避免了「每次查询重新三次握手 + 认证」的高昂开销。maxPoolSize 控制上限,太小请求排队、太大占满数据库连接配额。
Q2:MongoDB 的事务有什么前提条件?
A:多文档事务需要 副本集(Replica Set)或分片集群 ,单机 standalone 实例不支持(会直接报错)。MongoDB 4.0 起支持副本集事务、4.2 起支持分片事务。用法是 startSession() 开会话,withTransaction 内所有写操作传同一个 { session },任一步失败整体回滚。课堂单机环境只需了解语法,真正用要先把单机配成单节点副本集。
7. 实战项目:记账系统
7.1 数据模型设计
用户模型:
javascript
const userSchema = new mongoose.Schema({
username: {
type: String,
required: true,
unique: true,
trim: true
},
email: {
type: String,
required: true,
unique: true,
lowercase: true
},
password: {
type: String,
required: true
},
createdAt: {
type: Date,
default: Date.now
}
});
const User = mongoose.model('User', userSchema);
【代码注释】
- 记账系统用户表:
username/email唯一索引建议在 Schema 或userSchema.index声明。 password存 hash 而非明文;注册时用 bcrypt,与 §4.4pre('save')一致。mongoose.model('User', userSchema)集合名默认 users ;与账单表通过userId关联(可 populate)。
账单模型:
javascript
const accountSchema = new mongoose.Schema({
userId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
title: {
type: String,
required: true
},
time: {
type: Date,
required: true
},
type: {
type: Number,
required: true,
min: -1,
max: 1
},
account: {
type: Number,
required: true
},
remarks: String,
createdAt: {
type: Date,
default: Date.now
}
});
// 添加索引
accountSchema.index({ userId: 1 });
accountSchema.index({ time: -1 });
const Account = mongoose.model('Account', accountSchema);
【代码注释】
userId+ref: 'User'为外键语义,查询时可用.populate('userId')带出用户信息(Day13+ 扩展)。type用数字区分收入/支出(如 1 / -1),与课堂表单option的 value 保持一致。account字段名为金额数值,勿与 Model 名Account混淆。index({ userId: 1 })加速「某用户全部账单」;index({ time: -1 })加速按时间倒序列表。- 课堂记账本 Schema 更简(
models/accounts.js),本节为带用户隔离的扩展设计。
7.2 业务逻辑实现
创建账单:
javascript
async function createAccount(userId, accountData) {
const account = new Account({
userId: userId,
title: accountData.title,
time: accountData.time || new Date(),
type: accountData.type,
account: accountData.account,
remarks: accountData.remarks
});
return await account.save();
}
// 使用示例
const newAccount = await createAccount(userId, {
title: '餐饮支出',
time: new Date('2024-01-15'),
type: -1,
account: 150,
remarks: '午餐'
});
【代码注释】
new Account({...})构造文档实例,未写入库;save()才持久化并触发校验与中间件。userId须为有效 ObjectId 字符串或 ObjectId 实例,否则 CastError。type: -1表示支出,1表示收入;与 EJS 表单提交的字符串比较时注意Number()转换。time默认new Date()可省略;业务指定日期时传Date对象而非仅字符串更稳妥。
查询账单:
javascript
async function getUserAccounts(userId, options = {}) {
const query = { userId };
// 日期范围筛选
if (options.startDate || options.endDate) {
query.time = {};
if (options.startDate) {
query.time.$gte = new Date(options.startDate);
}
if (options.endDate) {
query.time.$lte = new Date(options.endDate);
}
}
// 类型筛选
if (options.type !== undefined) {
query.type = options.type;
}
const accounts = await Account.find(query)
.sort({ time: -1 })
.limit(options.limit || 50)
.lean();
return accounts;
}
// 使用示例
const accounts = await getUserAccounts(userId, {
startDate: '2024-01-01',
endDate: '2024-01-31',
type: -1
});
【代码注释】
query始终带userId,实现多用户数据隔离,避免查到他人账单。time.$gte/$lte构成日期区间,与 Shell$and区间查询一致。.sort({ time: -1 })最新在前;.limit(50)防止一次拉取过多。.lean()返回纯对象,适合 JSON 接口或 EJS 只读展示。options.type为undefined时不加类型条件,即查全部收支。
统计账单:
javascript
async function getAccountStatistics(userId, startDate, endDate) {
const stats = await Account.aggregate([
{
$match: {
userId: new mongoose.Types.ObjectId(userId),
time: {
$gte: new Date(startDate),
$lte: new Date(endDate)
}
}
},
{
$group: {
_id: '$type',
totalAmount: { $sum: '$account' },
count: { $sum: 1 }
}
}
]);
return stats;
}
【代码注释】
$match限定用户与时间范围,缩小聚合输入集。new mongoose.Types.ObjectId(userId)将字符串 id 转为 ObjectId,与库中userId类型一致;Mongoose 6+ 的 bson 要求new调用,省略会报错或告警。$group按type分组,$sum: '$account'合计金额,$sum: 1统计笔数。- 返回形如
[{ _id: 1, totalAmount: 8000, count: 5 }, { _id: -1, ... }],前端可画饼图/柱状图。 - 统计在库内完成,比
find全表再在 Node 里reduce更高效。
7.3 课堂记账本:Express + Mongoose 三步落地
将 Day11 LowDB 记账本 升级为 MongoDB 持久化,路由与页面结构保持一致。
#mermaid-svg-h5URTOKsurZr6S9i{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-h5URTOKsurZr6S9i .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-h5URTOKsurZr6S9i .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-h5URTOKsurZr6S9i .error-icon{fill:#552222;}#mermaid-svg-h5URTOKsurZr6S9i .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-h5URTOKsurZr6S9i .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-h5URTOKsurZr6S9i .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-h5URTOKsurZr6S9i .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-h5URTOKsurZr6S9i .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-h5URTOKsurZr6S9i .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-h5URTOKsurZr6S9i .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-h5URTOKsurZr6S9i .marker{fill:#333333;stroke:#333333;}#mermaid-svg-h5URTOKsurZr6S9i .marker.cross{stroke:#333333;}#mermaid-svg-h5URTOKsurZr6S9i svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-h5URTOKsurZr6S9i p{margin:0;}#mermaid-svg-h5URTOKsurZr6S9i .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-h5URTOKsurZr6S9i .cluster-label text{fill:#333;}#mermaid-svg-h5URTOKsurZr6S9i .cluster-label span{color:#333;}#mermaid-svg-h5URTOKsurZr6S9i .cluster-label span p{background-color:transparent;}#mermaid-svg-h5URTOKsurZr6S9i .label text,#mermaid-svg-h5URTOKsurZr6S9i span{fill:#333;color:#333;}#mermaid-svg-h5URTOKsurZr6S9i .node rect,#mermaid-svg-h5URTOKsurZr6S9i .node circle,#mermaid-svg-h5URTOKsurZr6S9i .node ellipse,#mermaid-svg-h5URTOKsurZr6S9i .node polygon,#mermaid-svg-h5URTOKsurZr6S9i .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-h5URTOKsurZr6S9i .rough-node .label text,#mermaid-svg-h5URTOKsurZr6S9i .node .label text,#mermaid-svg-h5URTOKsurZr6S9i .image-shape .label,#mermaid-svg-h5URTOKsurZr6S9i .icon-shape .label{text-anchor:middle;}#mermaid-svg-h5URTOKsurZr6S9i .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-h5URTOKsurZr6S9i .rough-node .label,#mermaid-svg-h5URTOKsurZr6S9i .node .label,#mermaid-svg-h5URTOKsurZr6S9i .image-shape .label,#mermaid-svg-h5URTOKsurZr6S9i .icon-shape .label{text-align:center;}#mermaid-svg-h5URTOKsurZr6S9i .node.clickable{cursor:pointer;}#mermaid-svg-h5URTOKsurZr6S9i .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-h5URTOKsurZr6S9i .arrowheadPath{fill:#333333;}#mermaid-svg-h5URTOKsurZr6S9i .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-h5URTOKsurZr6S9i .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-h5URTOKsurZr6S9i .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-h5URTOKsurZr6S9i .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-h5URTOKsurZr6S9i .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-h5URTOKsurZr6S9i .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-h5URTOKsurZr6S9i .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-h5URTOKsurZr6S9i .cluster text{fill:#333;}#mermaid-svg-h5URTOKsurZr6S9i .cluster span{color:#333;}#mermaid-svg-h5URTOKsurZr6S9i div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-h5URTOKsurZr6S9i .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-h5URTOKsurZr6S9i rect.text{fill:none;stroke-width:0;}#mermaid-svg-h5URTOKsurZr6S9i .icon-shape,#mermaid-svg-h5URTOKsurZr6S9i .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-h5URTOKsurZr6S9i .icon-shape p,#mermaid-svg-h5URTOKsurZr6S9i .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-h5URTOKsurZr6S9i .icon-shape .label rect,#mermaid-svg-h5URTOKsurZr6S9i .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-h5URTOKsurZr6S9i .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-h5URTOKsurZr6S9i .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-h5URTOKsurZr6S9i :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} bin/www 连接 MongoDB
models/accounts.js
routes/account.js
EJS 列表/表单/成功页
【代码注释】(架构图)
bin/www负责进程入口:连 MongoDB 成功后再http.createServer/listen,避免「服务已启、库未连」。models/accounts.js只定义 Schema 与 Model,不写路由,符合分层。routes/account.js处理 HTTP,调用 Model;视图在views/account/*.ejs。- 与 Day11 LowDB 版对比:仅持久化层由 JSON 文件换成 MongoDB,路由与页面可复用思路。
第一步:先连库,再启 HTTP(bin/www)
javascript
const mongoose = require('mongoose');
mongoose.set('strictQuery', false);
mongoose.connect('mongodb://127.0.0.1:27017/account_book');
mongoose.connection.on('open', () => {
console.log('数据库连接成功');
// 此处再启动 http server(listen)
});
mongoose.connection.on('error', () => {
console.log('数据库连接失败');
});
【代码注释】
bin/www中先require('mongoose')再connect,不要 在app.js里重复 connect 两次。mongoose.set('strictQuery', false)与课堂 Mongoose 脚本一致,消除严格查询警告。connection.on('open')回调里再启动 HTTP(server.listen),保证首条路由访问时库已就绪。error回调只打日志不够,生产应process.exit(1)或交 PM2 重启。- 课堂完整入口使用库名
account-project(mongodb://127.0.0.1:27017/account-project),与简化示例account_book二选一即可。
课堂完整入口(bin/www 节选):
javascript
const mongoose = require('mongoose');
const app = require('../app');
const http = require('http');
mongoose.connect('mongodb://127.0.0.1:27017/account-project');
mongoose.connection.on('open', () => {
const port = process.env.PORT || 3000;
app.set('port', port);
const server = http.createServer(app);
server.listen(port);
});
mongoose.connection.on('error', err => {
console.log('数据库连接失败!应用无法启动,请排查错误!');
throw err;
});
【代码注释】
- 与简化版相比:在
open内完成normalizePort、createServer、listen全套逻辑,端口默认 3000。 require('../app')只注册中间件与路由,不 在app.js里connect,避免重复连接。- 连接失败
throw err阻止无库启动 HTTP,防止首屏 500。
app.js 中间件栈(记账本):
javascript
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use('/', indexRouter);
app.use('/account', accountRouter);
app.use('/users', usersRouter);
【代码注释】
urlencoded解析表单POST /account/create的title、time、type、account、remarks。static托管public/下 Bootstrap、jQuery、bootstrap-datepicker 与main.js。- 路由前缀
/account对应routes/account.js中router.get('/')即访问/account。
添加页表单与日期组件(create.ejs + main.js):
html
<form method="post" action="/account/create">
<input type="text" name="title" />
<input type="text" name="time" id="time" />
<select name="type">
<option value="-1">支出</option>
<option value="1">收入</option>
</select>
<input type="text" name="account" />
<textarea name="remarks"></textarea>
</form>
<script src="/js/jquery.min.js"></script>
<script src="/js/bootstrap-datepicker.min.js"></script>
<script src="/js/bootstrap-datepicker.zh-CN.min.js"></script>
<script src="/js/main.js"></script>
【代码注释】
action="/account/create"与routes/account.js的POST /create对应(路由挂载在/account前缀下)。- 五个
name属性决定req.body的键名,须与 Mongoose Schema 字段完全一致。 - 静态脚本路径以
/js/、/css/开头,由express.static('public')映射到public/js、public/css。 - 页面还引入 bootstrap.css 、bootstrap-datepicker.css(完整模板在记账本添加页中)。
javascript
$('#time').datepicker({
format: 'yyyy-mm-dd',
language: 'zh-CN',
autoclose: true,
todayBtn: true,
todayHighlight: true
});
【代码注释】
- 表单
name与accountsSchema字段一一对应;POST后accountsModel.create(req.body)直接入库。 type的value为 -1 / 1 数字字符串,入库后 Mongoose 转为 Number(与 Schema 一致)。- 日期选择器把
time格式化为yyyy-mm-dd字符串写入库;列表页index.ejs遍历data展示。 - 成功/失败页
success.ejs、fail.ejs通过title、url变量跳转回列表。
第二步:Model 层(models/accounts.js)
javascript
const mongoose = require('mongoose');
const accountsSchema = new mongoose.Schema({
title: String,
remarks: String,
type: Number, // 1 收入,0 或 -1 支出(与表单 option 一致)
account: Number,
time: String
});
module.exports = mongoose.model('accounts', accountsSchema);
【代码注释】
- 单独文件导出 Model,路由
const accountsModel = require('../models/accounts')解耦。 - 字段全用
String/Number简化课堂;time存字符串与表单input一致,生产可改Date。 type: Number与表单<option value="1">/value="0">对应,避免字符串"1"与数字 1 查询不一致。- 集合名 accounts (复数);
_id24 位 hex,删除链接/delete/:id即req.params.id。 - 无
userId字段表示单用户记账本;多用户版见 §7.1 的userId设计。
第三步:路由 CRUD(核心逻辑)
javascript
const accountsModel = require('../models/accounts');
// 列表
router.get('/', (req, res) => {
accountsModel.find((err, data) => {
if (err) return res.status(500).send('数据库读取失败');
res.render('account/index', { data });
});
});
// 添加表单
router.get('/create', (req, res) => {
res.render('account/create');
});
// 提交添加
router.post('/create', (req, res) => {
accountsModel.create(req.body, err => {
if (err) res.render('account/fail', { title: '账单添加失败', url: '/account' });
else res.render('account/success', { title: '账单添加成功', url: '/account' });
});
});
// 删除
router.get('/delete/:id', (req, res) => {
accountsModel.deleteOne({ _id: req.params.id }, err => {
if (err) res.render('account/fail', { title: '账单删除失败', url: '/account' });
else res.render('account/success', { title: '账单删除成功', url: '/account' });
});
});
【代码注释】
GET /:find查全部账单传给index.ejs的data,模板里forEach渲染表格。GET /create只渲染空表单,不写库。POST /create:req.body来自express.urlencoded(),字段名须与表单name、Schema 一致。create(req.body)一次插入;成功/失败分别渲染success/fail页,带返回链接url。GET /delete/:id:deleteOne({ _id: req.params.id });_id非法格式会 err。- 删除用 GET 仅为课堂演示;生产应
POST+ 确认或DELETE方法,并加 CSRF / 登录鉴权。
本节配套练习路线:
| 顺序 | 练习 | 内容 |
|---|---|---|
| ① | 命令行 CRUD 笔记 | 库 / 集合 / 文档 / 运算符 |
| ② | Mongoose 01~05 | 插入 → 批量 → 删 → 改 → 查 |
| ③ | 记账本 Express | bin/www + Model + 路由 + EJS + 日期组件 |
#mermaid-svg-92FLo6IZS6wlFRdB{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-92FLo6IZS6wlFRdB .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-92FLo6IZS6wlFRdB .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-92FLo6IZS6wlFRdB .error-icon{fill:#552222;}#mermaid-svg-92FLo6IZS6wlFRdB .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-92FLo6IZS6wlFRdB .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-92FLo6IZS6wlFRdB .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-92FLo6IZS6wlFRdB .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-92FLo6IZS6wlFRdB .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-92FLo6IZS6wlFRdB .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-92FLo6IZS6wlFRdB .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-92FLo6IZS6wlFRdB .marker{fill:#333333;stroke:#333333;}#mermaid-svg-92FLo6IZS6wlFRdB .marker.cross{stroke:#333333;}#mermaid-svg-92FLo6IZS6wlFRdB svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-92FLo6IZS6wlFRdB p{margin:0;}#mermaid-svg-92FLo6IZS6wlFRdB .edge{stroke-width:3;}#mermaid-svg-92FLo6IZS6wlFRdB .section--1 rect,#mermaid-svg-92FLo6IZS6wlFRdB .section--1 path,#mermaid-svg-92FLo6IZS6wlFRdB .section--1 circle,#mermaid-svg-92FLo6IZS6wlFRdB .section--1 polygon,#mermaid-svg-92FLo6IZS6wlFRdB .section--1 path{fill:hsl(240, 100%, 76.2745098039%);}#mermaid-svg-92FLo6IZS6wlFRdB .section--1 text{fill:#ffffff;}#mermaid-svg-92FLo6IZS6wlFRdB .node-icon--1{font-size:40px;color:#ffffff;}#mermaid-svg-92FLo6IZS6wlFRdB .section-edge--1{stroke:hsl(240, 100%, 76.2745098039%);}#mermaid-svg-92FLo6IZS6wlFRdB .edge-depth--1{stroke-width:17;}#mermaid-svg-92FLo6IZS6wlFRdB .section--1 line{stroke:hsl(60, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-92FLo6IZS6wlFRdB .disabled,#mermaid-svg-92FLo6IZS6wlFRdB .disabled circle,#mermaid-svg-92FLo6IZS6wlFRdB .disabled text{fill:lightgray;}#mermaid-svg-92FLo6IZS6wlFRdB .disabled text{fill:#efefef;}#mermaid-svg-92FLo6IZS6wlFRdB .section-0 rect,#mermaid-svg-92FLo6IZS6wlFRdB .section-0 path,#mermaid-svg-92FLo6IZS6wlFRdB .section-0 circle,#mermaid-svg-92FLo6IZS6wlFRdB .section-0 polygon,#mermaid-svg-92FLo6IZS6wlFRdB .section-0 path{fill:hsl(60, 100%, 73.5294117647%);}#mermaid-svg-92FLo6IZS6wlFRdB .section-0 text{fill:black;}#mermaid-svg-92FLo6IZS6wlFRdB .node-icon-0{font-size:40px;color:black;}#mermaid-svg-92FLo6IZS6wlFRdB .section-edge-0{stroke:hsl(60, 100%, 73.5294117647%);}#mermaid-svg-92FLo6IZS6wlFRdB .edge-depth-0{stroke-width:14;}#mermaid-svg-92FLo6IZS6wlFRdB .section-0 line{stroke:hsl(240, 100%, 83.5294117647%);stroke-width:3;}#mermaid-svg-92FLo6IZS6wlFRdB .disabled,#mermaid-svg-92FLo6IZS6wlFRdB .disabled circle,#mermaid-svg-92FLo6IZS6wlFRdB .disabled text{fill:lightgray;}#mermaid-svg-92FLo6IZS6wlFRdB .disabled text{fill:#efefef;}#mermaid-svg-92FLo6IZS6wlFRdB .section-1 rect,#mermaid-svg-92FLo6IZS6wlFRdB .section-1 path,#mermaid-svg-92FLo6IZS6wlFRdB .section-1 circle,#mermaid-svg-92FLo6IZS6wlFRdB .section-1 polygon,#mermaid-svg-92FLo6IZS6wlFRdB .section-1 path{fill:hsl(80, 100%, 76.2745098039%);}#mermaid-svg-92FLo6IZS6wlFRdB .section-1 text{fill:black;}#mermaid-svg-92FLo6IZS6wlFRdB .node-icon-1{font-size:40px;color:black;}#mermaid-svg-92FLo6IZS6wlFRdB .section-edge-1{stroke:hsl(80, 100%, 76.2745098039%);}#mermaid-svg-92FLo6IZS6wlFRdB .edge-depth-1{stroke-width:11;}#mermaid-svg-92FLo6IZS6wlFRdB .section-1 line{stroke:hsl(260, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-92FLo6IZS6wlFRdB .disabled,#mermaid-svg-92FLo6IZS6wlFRdB .disabled circle,#mermaid-svg-92FLo6IZS6wlFRdB .disabled text{fill:lightgray;}#mermaid-svg-92FLo6IZS6wlFRdB .disabled text{fill:#efefef;}#mermaid-svg-92FLo6IZS6wlFRdB .section-2 rect,#mermaid-svg-92FLo6IZS6wlFRdB .section-2 path,#mermaid-svg-92FLo6IZS6wlFRdB .section-2 circle,#mermaid-svg-92FLo6IZS6wlFRdB .section-2 polygon,#mermaid-svg-92FLo6IZS6wlFRdB .section-2 path{fill:hsl(270, 100%, 76.2745098039%);}#mermaid-svg-92FLo6IZS6wlFRdB .section-2 text{fill:#ffffff;}#mermaid-svg-92FLo6IZS6wlFRdB .node-icon-2{font-size:40px;color:#ffffff;}#mermaid-svg-92FLo6IZS6wlFRdB .section-edge-2{stroke:hsl(270, 100%, 76.2745098039%);}#mermaid-svg-92FLo6IZS6wlFRdB .edge-depth-2{stroke-width:8;}#mermaid-svg-92FLo6IZS6wlFRdB .section-2 line{stroke:hsl(90, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-92FLo6IZS6wlFRdB .disabled,#mermaid-svg-92FLo6IZS6wlFRdB .disabled circle,#mermaid-svg-92FLo6IZS6wlFRdB .disabled text{fill:lightgray;}#mermaid-svg-92FLo6IZS6wlFRdB .disabled text{fill:#efefef;}#mermaid-svg-92FLo6IZS6wlFRdB .section-3 rect,#mermaid-svg-92FLo6IZS6wlFRdB .section-3 path,#mermaid-svg-92FLo6IZS6wlFRdB .section-3 circle,#mermaid-svg-92FLo6IZS6wlFRdB .section-3 polygon,#mermaid-svg-92FLo6IZS6wlFRdB .section-3 path{fill:hsl(300, 100%, 76.2745098039%);}#mermaid-svg-92FLo6IZS6wlFRdB .section-3 text{fill:black;}#mermaid-svg-92FLo6IZS6wlFRdB .node-icon-3{font-size:40px;color:black;}#mermaid-svg-92FLo6IZS6wlFRdB .section-edge-3{stroke:hsl(300, 100%, 76.2745098039%);}#mermaid-svg-92FLo6IZS6wlFRdB .edge-depth-3{stroke-width:5;}#mermaid-svg-92FLo6IZS6wlFRdB .section-3 line{stroke:hsl(120, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-92FLo6IZS6wlFRdB .disabled,#mermaid-svg-92FLo6IZS6wlFRdB .disabled circle,#mermaid-svg-92FLo6IZS6wlFRdB .disabled text{fill:lightgray;}#mermaid-svg-92FLo6IZS6wlFRdB .disabled text{fill:#efefef;}#mermaid-svg-92FLo6IZS6wlFRdB .section-4 rect,#mermaid-svg-92FLo6IZS6wlFRdB .section-4 path,#mermaid-svg-92FLo6IZS6wlFRdB .section-4 circle,#mermaid-svg-92FLo6IZS6wlFRdB .section-4 polygon,#mermaid-svg-92FLo6IZS6wlFRdB .section-4 path{fill:hsl(330, 100%, 76.2745098039%);}#mermaid-svg-92FLo6IZS6wlFRdB .section-4 text{fill:black;}#mermaid-svg-92FLo6IZS6wlFRdB .node-icon-4{font-size:40px;color:black;}#mermaid-svg-92FLo6IZS6wlFRdB .section-edge-4{stroke:hsl(330, 100%, 76.2745098039%);}#mermaid-svg-92FLo6IZS6wlFRdB .edge-depth-4{stroke-width:2;}#mermaid-svg-92FLo6IZS6wlFRdB .section-4 line{stroke:hsl(150, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-92FLo6IZS6wlFRdB .disabled,#mermaid-svg-92FLo6IZS6wlFRdB .disabled circle,#mermaid-svg-92FLo6IZS6wlFRdB .disabled text{fill:lightgray;}#mermaid-svg-92FLo6IZS6wlFRdB .disabled text{fill:#efefef;}#mermaid-svg-92FLo6IZS6wlFRdB .section-5 rect,#mermaid-svg-92FLo6IZS6wlFRdB .section-5 path,#mermaid-svg-92FLo6IZS6wlFRdB .section-5 circle,#mermaid-svg-92FLo6IZS6wlFRdB .section-5 polygon,#mermaid-svg-92FLo6IZS6wlFRdB .section-5 path{fill:hsl(0, 100%, 76.2745098039%);}#mermaid-svg-92FLo6IZS6wlFRdB .section-5 text{fill:black;}#mermaid-svg-92FLo6IZS6wlFRdB .node-icon-5{font-size:40px;color:black;}#mermaid-svg-92FLo6IZS6wlFRdB .section-edge-5{stroke:hsl(0, 100%, 76.2745098039%);}#mermaid-svg-92FLo6IZS6wlFRdB .edge-depth-5{stroke-width:-1;}#mermaid-svg-92FLo6IZS6wlFRdB .section-5 line{stroke:hsl(180, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-92FLo6IZS6wlFRdB .disabled,#mermaid-svg-92FLo6IZS6wlFRdB .disabled circle,#mermaid-svg-92FLo6IZS6wlFRdB .disabled text{fill:lightgray;}#mermaid-svg-92FLo6IZS6wlFRdB .disabled text{fill:#efefef;}#mermaid-svg-92FLo6IZS6wlFRdB .section-6 rect,#mermaid-svg-92FLo6IZS6wlFRdB .section-6 path,#mermaid-svg-92FLo6IZS6wlFRdB .section-6 circle,#mermaid-svg-92FLo6IZS6wlFRdB .section-6 polygon,#mermaid-svg-92FLo6IZS6wlFRdB .section-6 path{fill:hsl(30, 100%, 76.2745098039%);}#mermaid-svg-92FLo6IZS6wlFRdB .section-6 text{fill:black;}#mermaid-svg-92FLo6IZS6wlFRdB .node-icon-6{font-size:40px;color:black;}#mermaid-svg-92FLo6IZS6wlFRdB .section-edge-6{stroke:hsl(30, 100%, 76.2745098039%);}#mermaid-svg-92FLo6IZS6wlFRdB .edge-depth-6{stroke-width:-4;}#mermaid-svg-92FLo6IZS6wlFRdB .section-6 line{stroke:hsl(210, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-92FLo6IZS6wlFRdB .disabled,#mermaid-svg-92FLo6IZS6wlFRdB .disabled circle,#mermaid-svg-92FLo6IZS6wlFRdB .disabled text{fill:lightgray;}#mermaid-svg-92FLo6IZS6wlFRdB .disabled text{fill:#efefef;}#mermaid-svg-92FLo6IZS6wlFRdB .section-7 rect,#mermaid-svg-92FLo6IZS6wlFRdB .section-7 path,#mermaid-svg-92FLo6IZS6wlFRdB .section-7 circle,#mermaid-svg-92FLo6IZS6wlFRdB .section-7 polygon,#mermaid-svg-92FLo6IZS6wlFRdB .section-7 path{fill:hsl(90, 100%, 76.2745098039%);}#mermaid-svg-92FLo6IZS6wlFRdB .section-7 text{fill:black;}#mermaid-svg-92FLo6IZS6wlFRdB .node-icon-7{font-size:40px;color:black;}#mermaid-svg-92FLo6IZS6wlFRdB .section-edge-7{stroke:hsl(90, 100%, 76.2745098039%);}#mermaid-svg-92FLo6IZS6wlFRdB .edge-depth-7{stroke-width:-7;}#mermaid-svg-92FLo6IZS6wlFRdB .section-7 line{stroke:hsl(270, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-92FLo6IZS6wlFRdB .disabled,#mermaid-svg-92FLo6IZS6wlFRdB .disabled circle,#mermaid-svg-92FLo6IZS6wlFRdB .disabled text{fill:lightgray;}#mermaid-svg-92FLo6IZS6wlFRdB .disabled text{fill:#efefef;}#mermaid-svg-92FLo6IZS6wlFRdB .section-8 rect,#mermaid-svg-92FLo6IZS6wlFRdB .section-8 path,#mermaid-svg-92FLo6IZS6wlFRdB .section-8 circle,#mermaid-svg-92FLo6IZS6wlFRdB .section-8 polygon,#mermaid-svg-92FLo6IZS6wlFRdB .section-8 path{fill:hsl(150, 100%, 76.2745098039%);}#mermaid-svg-92FLo6IZS6wlFRdB .section-8 text{fill:black;}#mermaid-svg-92FLo6IZS6wlFRdB .node-icon-8{font-size:40px;color:black;}#mermaid-svg-92FLo6IZS6wlFRdB .section-edge-8{stroke:hsl(150, 100%, 76.2745098039%);}#mermaid-svg-92FLo6IZS6wlFRdB .edge-depth-8{stroke-width:-10;}#mermaid-svg-92FLo6IZS6wlFRdB .section-8 line{stroke:hsl(330, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-92FLo6IZS6wlFRdB .disabled,#mermaid-svg-92FLo6IZS6wlFRdB .disabled circle,#mermaid-svg-92FLo6IZS6wlFRdB .disabled text{fill:lightgray;}#mermaid-svg-92FLo6IZS6wlFRdB .disabled text{fill:#efefef;}#mermaid-svg-92FLo6IZS6wlFRdB .section-9 rect,#mermaid-svg-92FLo6IZS6wlFRdB .section-9 path,#mermaid-svg-92FLo6IZS6wlFRdB .section-9 circle,#mermaid-svg-92FLo6IZS6wlFRdB .section-9 polygon,#mermaid-svg-92FLo6IZS6wlFRdB .section-9 path{fill:hsl(180, 100%, 76.2745098039%);}#mermaid-svg-92FLo6IZS6wlFRdB .section-9 text{fill:black;}#mermaid-svg-92FLo6IZS6wlFRdB .node-icon-9{font-size:40px;color:black;}#mermaid-svg-92FLo6IZS6wlFRdB .section-edge-9{stroke:hsl(180, 100%, 76.2745098039%);}#mermaid-svg-92FLo6IZS6wlFRdB .edge-depth-9{stroke-width:-13;}#mermaid-svg-92FLo6IZS6wlFRdB .section-9 line{stroke:hsl(0, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-92FLo6IZS6wlFRdB .disabled,#mermaid-svg-92FLo6IZS6wlFRdB .disabled circle,#mermaid-svg-92FLo6IZS6wlFRdB .disabled text{fill:lightgray;}#mermaid-svg-92FLo6IZS6wlFRdB .disabled text{fill:#efefef;}#mermaid-svg-92FLo6IZS6wlFRdB .section-10 rect,#mermaid-svg-92FLo6IZS6wlFRdB .section-10 path,#mermaid-svg-92FLo6IZS6wlFRdB .section-10 circle,#mermaid-svg-92FLo6IZS6wlFRdB .section-10 polygon,#mermaid-svg-92FLo6IZS6wlFRdB .section-10 path{fill:hsl(210, 100%, 76.2745098039%);}#mermaid-svg-92FLo6IZS6wlFRdB .section-10 text{fill:black;}#mermaid-svg-92FLo6IZS6wlFRdB .node-icon-10{font-size:40px;color:black;}#mermaid-svg-92FLo6IZS6wlFRdB .section-edge-10{stroke:hsl(210, 100%, 76.2745098039%);}#mermaid-svg-92FLo6IZS6wlFRdB .edge-depth-10{stroke-width:-16;}#mermaid-svg-92FLo6IZS6wlFRdB .section-10 line{stroke:hsl(30, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-92FLo6IZS6wlFRdB .disabled,#mermaid-svg-92FLo6IZS6wlFRdB .disabled circle,#mermaid-svg-92FLo6IZS6wlFRdB .disabled text{fill:lightgray;}#mermaid-svg-92FLo6IZS6wlFRdB .disabled text{fill:#efefef;}#mermaid-svg-92FLo6IZS6wlFRdB .section-root rect,#mermaid-svg-92FLo6IZS6wlFRdB .section-root path,#mermaid-svg-92FLo6IZS6wlFRdB .section-root circle,#mermaid-svg-92FLo6IZS6wlFRdB .section-root polygon{fill:hsl(240, 100%, 46.2745098039%);}#mermaid-svg-92FLo6IZS6wlFRdB .section-root text{fill:#ffffff;}#mermaid-svg-92FLo6IZS6wlFRdB .section-root span{color:#ffffff;}#mermaid-svg-92FLo6IZS6wlFRdB .section-2 span{color:#ffffff;}#mermaid-svg-92FLo6IZS6wlFRdB .icon-container{height:100%;display:flex;justify-content:center;align-items:center;}#mermaid-svg-92FLo6IZS6wlFRdB .edge{fill:none;}#mermaid-svg-92FLo6IZS6wlFRdB .mindmap-node-label{dy:1em;alignment-baseline:middle;text-anchor:middle;dominant-baseline:middle;text-align:center;}#mermaid-svg-92FLo6IZS6wlFRdB :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 记账本请求
浏览器
GET列表
POST添加
GET删除
Express
urlencoded
静态资源
Mongoose
accounts集合
MongoDB
account-project库
【代码注释】(mindmap)
- 读路径:
GET /account→find→index.ejs;写路径:POST /account/create→create(req.body)。 - 删路径:
GET /account/delete/:id→deleteOne({ _id })(课堂用 GET,生产改 POST)。 - 静态资源与 API 同域,datepicker 无需 CORS。
【实战要点】
- 经典应用场景:内部 OA、个人记账、简易 CMS------服务端渲染 + 文档库足够快上线。
- 常见坑 :
app.js里先listen再连库,首请求find报错;必须在bin/www的open里启服。 - 性能与最佳实践 :列表
find加sort({ time: -1 });数据量大时加userid+time复合索引(多用户版)。
【面试考点】
Q1:为什么记账本要把 mongoose.connect 放在 bin/www 而不是 app.js?
A:分离「应用配置」与「进程入口」;保证 DB ready → HTTP listen 顺序,避免竞态。Express 生成器默认把 listen 放 bin/www,连库逻辑与之同级最清晰。
【本章小结】
| 分层 | 文件 | 职责 |
|---|---|---|
| 进程入口 | bin/www |
先连库,open 后再 listen |
| 应用配置 | app.js |
挂中间件、注册路由,不连库 |
| 数据层 | models/accounts.js |
定义 Schema 与 Model |
| 控制层 | routes/account.js |
处理 HTTP,调用 Model |
| 视图层 | views/account/*.ejs |
渲染列表 / 表单 / 结果页 |
记忆口诀 :「入口连库、配置挂件、模型定结构、路由跑逻辑、视图出页面 」。一条铁律------数据库连接成功后再启动 HTTP ,否则首个请求 find 时库还没就绪,必 500。
8. 性能监控与故障排查
8.1 性能监控工具
MongoDB 内置监控:
javascript
// 数据库状态监控
const dbStatus = await mongoose.connection.db.admin().serverStatus();
// 连接状态监控
const connStats = mongoose.connection.collection('users').stats();
// 慢查询分析
const slowQueries = mongoose.connection.db.collection('system.profile').find({
millis: { $gt: 100 } // 超过100ms的查询
}).sort({ ts: -1 }).limit(10);
【代码注释】
serverStatus()查看连接数、操作计数、内存等,运维巡检常用。collection('users').stats()看集合文档数与平均文档大小,评估是否需归档。system.profile为慢查询日志集合,需先db.setProfilingLevel(1)开启 profiling 才有数据。millis: { $gt: 100 }筛选超过 100ms 的语句,按ts倒序取最近 10 条优化。
8.2 常见问题排查
连接问题排查:
javascript
// 连接事件监听
mongoose.connection.on('error', (err) => {
console.error('MongoDB 连接错误:', err);
// 常见错误:
// - 连接超时:检查网络连接和防火墙设置
// - 认证失败:检查用户名和密码
// - 服务器不可用:检查MongoDB服务状态
});
mongoose.connection.on('disconnected', () => {
console.log('MongoDB 连接断开');
// 自动重连机制会启动
});
【代码注释】
error事件常见:ECONNREFUSED(mongod 未启)、认证失败、超时、DNS 解析失败(Atlas)。- 排查顺序:本机
mongosh能否连 → URI 是否正确 → 防火墙/云安全组 → 用户权限。 disconnected后 Mongoose 默认会尝试重连;仍应检查网络与 MongoDB 进程是否存活。
性能问题排查:
javascript
// 慢查询日志
mongoose.set('debug', (collectionName, method, query, doc) => {
console.log(`${collectionName}.${method}`, JSON.stringify(query));
});
// 查询分析
const explanation = await User.find({ username: 'john' }).explain();
console.log('查询计划:', explanation.queryPlanner);
【代码注释】
mongoose.set('debug', fn)开发环境打印每条查询的集合名、方法、条件,定位 N+1 与多余查询。explain()查看是否走索引(IXSCANvsCOLLSCAN)、扫描文档数totalDocsExamined。- 若
totalDocsExamined远大于返回条数,考虑加索引或改写查询条件。 - 上线前关闭 debug,避免日志泄露与性能损耗。
慢查询排查决策流:
#mermaid-svg-wtw3DThUjslneJyB{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-wtw3DThUjslneJyB .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-wtw3DThUjslneJyB .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-wtw3DThUjslneJyB .error-icon{fill:#552222;}#mermaid-svg-wtw3DThUjslneJyB .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-wtw3DThUjslneJyB .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-wtw3DThUjslneJyB .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-wtw3DThUjslneJyB .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-wtw3DThUjslneJyB .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-wtw3DThUjslneJyB .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-wtw3DThUjslneJyB .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-wtw3DThUjslneJyB .marker{fill:#333333;stroke:#333333;}#mermaid-svg-wtw3DThUjslneJyB .marker.cross{stroke:#333333;}#mermaid-svg-wtw3DThUjslneJyB svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-wtw3DThUjslneJyB p{margin:0;}#mermaid-svg-wtw3DThUjslneJyB .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-wtw3DThUjslneJyB .cluster-label text{fill:#333;}#mermaid-svg-wtw3DThUjslneJyB .cluster-label span{color:#333;}#mermaid-svg-wtw3DThUjslneJyB .cluster-label span p{background-color:transparent;}#mermaid-svg-wtw3DThUjslneJyB .label text,#mermaid-svg-wtw3DThUjslneJyB span{fill:#333;color:#333;}#mermaid-svg-wtw3DThUjslneJyB .node rect,#mermaid-svg-wtw3DThUjslneJyB .node circle,#mermaid-svg-wtw3DThUjslneJyB .node ellipse,#mermaid-svg-wtw3DThUjslneJyB .node polygon,#mermaid-svg-wtw3DThUjslneJyB .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-wtw3DThUjslneJyB .rough-node .label text,#mermaid-svg-wtw3DThUjslneJyB .node .label text,#mermaid-svg-wtw3DThUjslneJyB .image-shape .label,#mermaid-svg-wtw3DThUjslneJyB .icon-shape .label{text-anchor:middle;}#mermaid-svg-wtw3DThUjslneJyB .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-wtw3DThUjslneJyB .rough-node .label,#mermaid-svg-wtw3DThUjslneJyB .node .label,#mermaid-svg-wtw3DThUjslneJyB .image-shape .label,#mermaid-svg-wtw3DThUjslneJyB .icon-shape .label{text-align:center;}#mermaid-svg-wtw3DThUjslneJyB .node.clickable{cursor:pointer;}#mermaid-svg-wtw3DThUjslneJyB .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-wtw3DThUjslneJyB .arrowheadPath{fill:#333333;}#mermaid-svg-wtw3DThUjslneJyB .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-wtw3DThUjslneJyB .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-wtw3DThUjslneJyB .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wtw3DThUjslneJyB .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-wtw3DThUjslneJyB .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wtw3DThUjslneJyB .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-wtw3DThUjslneJyB .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-wtw3DThUjslneJyB .cluster text{fill:#333;}#mermaid-svg-wtw3DThUjslneJyB .cluster span{color:#333;}#mermaid-svg-wtw3DThUjslneJyB div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-wtw3DThUjslneJyB .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-wtw3DThUjslneJyB rect.text{fill:none;stroke-width:0;}#mermaid-svg-wtw3DThUjslneJyB .icon-shape,#mermaid-svg-wtw3DThUjslneJyB .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wtw3DThUjslneJyB .icon-shape p,#mermaid-svg-wtw3DThUjslneJyB .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-wtw3DThUjslneJyB .icon-shape .label rect,#mermaid-svg-wtw3DThUjslneJyB .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wtw3DThUjslneJyB .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-wtw3DThUjslneJyB .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-wtw3DThUjslneJyB :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} COLLSCAN
IXSCAN
否,差距大
是
接口变慢
对慢查询跑 explain('executionStats')
stage 是什么?
缺索引 → 建合适索引
totalDocsExamined
≈ nReturned ?
索引选择性差 → 调整复合索引顺序
查询本身没问题 → 查网络 / 连接池 / 数据量
【代码注释】(排查决策图)排查慢查询有固定套路,不靠猜。第一步永远是 explain('executionStats'):看到 COLLSCAN 就是缺索引,建上即可;看到 IXSCAN 还慢,就比 totalDocsExamined(检查了多少文档)和 nReturned(返回了多少)------两者接近说明索引高效,差距悬殊说明索引区分度差 或字段顺序不对,要重新设计复合索引。两步都正常还慢,问题就不在查询本身,而在网络、连接池耗尽或数据量本身过大(该分页 / 归档了)。市面应用:这套「先 explain、再看扫描比」的流程是所有 MongoDB DBA 的日常;线上慢查询告警触发后,第一个动作就是把那句查询捞出来跑 explain。
【实战要点】
- 经典应用场景 :上线前用
mongoose.set('debug', true)抓出页面里隐藏的 N+1 查询;用db.setProfilingLevel(1)开慢查询 profiling,定期捞system.profile里>100ms的语句优化。 - 常见坑 :① 把
mongoose.set('debug', true)带上生产,海量日志拖慢服务还可能泄露查询条件;② 只看接口耗时不看explain,凭感觉加索引,越加越乱;③disconnected后以为会自动恢复就不管,实际网络持续异常时仍需告警。 - 性能与最佳实践:建立「慢接口 → explain → 看 stage 与扫描比 → 对症下药」的固定流程;监控连接数、慢查询数、复制延迟三个核心指标;生产环境务必关闭 debug。
【本章小结】
| 工具 / 字段 | 用途 | 健康标志 |
|---|---|---|
explain('executionStats') |
看查询计划 | stage: IXSCAN |
totalDocsExamined / nReturned |
看索引效率 | 两者接近 |
mongoose.set('debug') |
抓全部查询语句 | 仅开发期开启 |
system.profile |
慢查询日志 | 需先开 profiling |
serverStatus() |
实例整体状态 | 连接数、内存可控 |
记忆口诀 :排错只认证据不靠猜------「先 explain、看 stage、比扫描、再下药」。COLLSCAN 加索引,扫描比悬殊调索引,都正常查外部。
【面试考点】
Q1:线上一个接口突然变慢,你怎么定位是不是数据库的问题?
A:先把对应的查询捞出来跑 explain('executionStats')。看 stage:是 COLLSCAN 就是全表扫描、缺索引;是 IXSCAN 再比 totalDocsExamined 和 nReturned,差距大说明索引区分度差或复合索引顺序不对。如果查询计划本身健康,就转去查连接池是否耗尽、网络延迟、数据量是否已大到该分页归档。核心是用 explain 拿证据,不靠猜着加索引。
Q2:mongoose.set('debug', true) 是做什么的?能用在生产吗?
A:开启后 Mongoose 会在控制台打印每一条查询的集合名、方法和条件,开发期用来发现「页面渲染一次竟发了几十条查询」这类 N+1 问题非常有用。但不能用于生产 :高频日志会拖慢服务、占满磁盘,查询条件里可能含敏感数据造成泄露。生产环境定位慢查询应改用 db.setProfilingLevel() 配合 system.profile 集合。
9. 核心案例速查与知识点归纳
9.1 三层概念(必背)
| MongoDB | 类比 | 说明 |
|---|---|---|
| database | 仓库 | 一个实例多个库 |
| collection | 表 | 无固定列,存文档 |
| document | 行 | BSON 对象,含 _id |
9.2 Shell CRUD 速查
| 操作 | 命令 |
|---|---|
| 建/切库 | use dbName |
| 插入 | db.col.insertOne({}) |
| 查 | db.col.find({}) |
| 改 | db.col.updateOne({}, { $set: {} }) |
| 删 | db.col.deleteOne({}) |
9.3 Mongoose 五步速查
require('mongoose')+connect(uri)connection.on('open', ...)new Schema({ ... })mongoose.model('name', schema)Model.create/find/updateOne/deleteOne
9.4 可运行 HTML:BSON 类型对照
保存为 mongo-bson-types-demo.html,理解 JSON 与 BSON 扩展类型差异(纯前端,不连库):
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>BSON 类型对照</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 640px; margin: 2rem auto; padding: 0 1rem; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
th { background: #f4f4f4; }
</style>
</head>
<body>
<h1>JSON 能表达 vs BSON 额外支持</h1>
<table>
<thead><tr><th>类型</th><th>JSON</th><th>BSON / MongoDB</th></tr></thead>
<tbody id="rows"></tbody>
</table>
<script>
const types = [
['String', '✓', '✓'],
['Number', '✓', '✓(Int32/Int64/Double)'],
['Boolean', '✓', '✓'],
['Array / Object', '✓', '✓'],
['Null', '✓', '✓'],
['Date', '字符串 ISO', 'BSON Date 类型'],
['ObjectId', '✗', '12 字节 _id'],
['Decimal128', '✗', '高精度金额']
];
document.getElementById('rows').innerHTML = types.map(
t => `<tr><td>${t[0]}</td><td>${t[1]}</td><td>${t[2]}</td></tr>`
).join('');
</script>
</body>
</html>
【代码注释】
- MongoDB 存的是 BSON ,不是纯 JSON;
ObjectId、Date在导出 JSON 时会变成特殊对象或字符串。 - Mongoose 会把 JS
Date自动转为 BSON Date;_id默认ObjectId。 - 面试常问「为什么需要 BSON」:在 JSON 之上增加二进制类型与长度前缀,解析更快、类型更精确。
9.5 可运行 HTML:理解 JSON 文档与查询条件
保存为 mongo-query-demo.html,在浏览器打开(纯前端演示 BSON/JSON 结构,不连真实库):
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>MongoDB 文档与查询演示</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 720px; margin: 2rem auto; padding: 0 1rem; }
pre { background: #f4f4f4; padding: 12px; overflow: auto; border-radius: 6px; }
button { margin: 8px 8px 8px 0; padding: 8px 14px; cursor: pointer; }
.ok { color: #0a0; }
.bad { color: #c00; }
</style>
</head>
<body>
<h1>文档型数据演示</h1>
<p>模拟集合 <code>accounts</code> 中的两条账单,用 JS 过滤(对应 <code>find({ type: 1 })</code>)。</p>
<button type="button" id="all">全部</button>
<button type="button" id="income">仅收入 type=1</button>
<button type="button" id="expense">仅支出 type=0</button>
<pre id="out"></pre>
<script>
const accounts = [
{ _id: '1', title: '工资', type: 1, account: 8000, time: '2024-01-01' },
{ _id: '2', title: '午餐', type: 0, account: 35, time: '2024-01-02' },
{ _id: '3', title: '兼职', type: 1, account: 500, time: '2024-01-03' }
];
const out = document.getElementById('out');
function render(list) {
out.textContent = JSON.stringify(list, null, 2);
}
document.getElementById('all').onclick = () => render(accounts);
document.getElementById('income').onclick = () => render(accounts.filter(a => a.type === 1));
document.getElementById('expense').onclick = () => render(accounts.filter(a => a.type === 0));
render(accounts);
</script>
</body>
</html>
【代码注释】
- 纯前端演示,不连接 MongoDB;用内存数组模拟
accounts集合中的文档。 filter(a => a.type === 1)对应 Shelldb.accounts.find({ type: 1 })的语义。type在课堂表单中为数字,此处 1=收入、0=支出,与记账本 EJS 一致。- 真实库中
_id为ObjectId;演示用'1'、'2'字符串便于阅读 JSON。 - 保存为
.html后用浏览器打开即可;用于理解文档结构与条件过滤,再对照 mongosh 操作。
9.6 常见坑
| 现象 | 原因 | 处理 |
|---|---|---|
show dbs 看不到新库 |
库内无集合 | 先 insertOne 再 show |
| Mongoose 连不上 | mongod 未启动 |
先起服务再跑 Node |
req.body 写库为空 |
未挂 urlencoded |
app.use(express.urlencoded()) |
| 查询无结果 | 类型不一致(字符串 vs 数字) | Schema 与表单 type 统一 |
| 严格查询警告 | Mongoose 7+ | mongoose.set('strictQuery', false) |
9.7 行业应用场景归纳
| 行业 | 用法 |
|---|---|
| 电商 | 商品 SKU、购物车、评价嵌套文档 |
| 社交 | 动态流、关注关系、消息会话 |
| 物联网 | 设备上报时序数据,TTL 索引过期 |
| 内容平台 | 文章 + 标签数组 + 全文索引 |
| 内部工具 | 记账、审批流、配置中心(本章记账本) |
9.8 可运行 HTML:Mongoose Schema 校验模拟
Mongoose 的核心价值之一是保存前自动校验 。下面用纯前端复现 required、enum、min 三类校验规则。保存为 schema-validate-demo.html 放课程目录,双击运行:
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Mongoose Schema 校验模拟</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 640px; margin: 2rem auto; padding: 0 1rem; }
label { display: block; margin: 10px 0 4px; }
input, select { width: 100%; padding: 7px; box-sizing: border-box; }
button { margin-top: 14px; padding: 8px 18px; cursor: pointer; }
.err { color: #c0392b; } .ok { color: #27ae60; }
pre { background: #f4f4f4; padding: 12px; border-radius: 6px; }
</style>
</head>
<body>
<h1>提交前的 Schema 校验</h1>
<p>模拟 <code>new Account(doc).save()</code> 触发的校验流程。</p>
<label>标题 title(required)</label>
<input id="title" value="午餐" />
<label>类型 type(enum: -1 / 1)</label>
<select id="type">
<option value="1">收入(1)</option>
<option value="-1">支出(-1)</option>
<option value="0">非法(0)</option>
</select>
<label>金额 account(required, min: 0)</label>
<input id="account" type="number" value="35" />
<button type="button" id="save">save()</button>
<pre id="out"></pre>
<script>
// 模拟 accountSchema 的校验规则
const schema = {
title: { required: true },
type: { required: true, enum: [-1, 1] },
account: { required: true, min: 0 }
};
function validate(doc) {
const errors = [];
for (const [field, rule] of Object.entries(schema)) {
const v = doc[field];
if (rule.required && (v === '' || v === undefined || v === null))
errors.push(`${field}:不能为空(required)`);
if (rule.enum && v !== '' && !rule.enum.includes(Number(v)))
errors.push(`${field}:必须是 ${rule.enum.join(' 或 ')}(enum)`);
if (rule.min !== undefined && v !== '' && Number(v) < rule.min)
errors.push(`${field}:不能小于 ${rule.min}(min)`);
}
return errors;
}
document.getElementById('save').onclick = () => {
const doc = {
title: document.getElementById('title').value,
type: document.getElementById('type').value,
account: document.getElementById('account').value
};
const errors = validate(doc);
const out = document.getElementById('out');
if (errors.length) {
out.className = 'err';
out.textContent = 'ValidationError:\n- ' + errors.join('\n- ');
} else {
out.className = 'ok';
out.textContent = '校验通过,文档已写入:\n' + JSON.stringify(doc, null, 2);
}
};
</script>
</body>
</html>
【代码注释】这个演示把 Mongoose 的校验过程「拆开演示」:schema 对象就是简化版的 accountSchema,validate() 函数逐字段检查 required(非空)、enum(取值范围)、min(数值下限)三类规则,命中即收集成 errors 数组。把 type 选成非法的 0、或把金额改成负数,点 save() 就会看到一条 ValidationError ------这正是真实 Mongoose 在 save() 时抛出的错误类型。为什么这样写 :让初学者明白「校验发生在 save 之前、在应用层」,而不是数据库拒绝。市面应用:注册表单的「用户名必填、邮箱格式、年龄 ≥ 0」,订单的「状态只能是枚举值」,全靠 Schema 校验在脏数据进库前拦截;前端校验可被绕过,Schema 校验是服务端最后一道防线。
总结
#mermaid-svg-A8LH4UxgadF34PzD{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-A8LH4UxgadF34PzD .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-A8LH4UxgadF34PzD .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-A8LH4UxgadF34PzD .error-icon{fill:#552222;}#mermaid-svg-A8LH4UxgadF34PzD .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-A8LH4UxgadF34PzD .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-A8LH4UxgadF34PzD .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-A8LH4UxgadF34PzD .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-A8LH4UxgadF34PzD .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-A8LH4UxgadF34PzD .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-A8LH4UxgadF34PzD .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-A8LH4UxgadF34PzD .marker{fill:#333333;stroke:#333333;}#mermaid-svg-A8LH4UxgadF34PzD .marker.cross{stroke:#333333;}#mermaid-svg-A8LH4UxgadF34PzD svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-A8LH4UxgadF34PzD p{margin:0;}#mermaid-svg-A8LH4UxgadF34PzD .edge{stroke-width:3;}#mermaid-svg-A8LH4UxgadF34PzD .section--1 rect,#mermaid-svg-A8LH4UxgadF34PzD .section--1 path,#mermaid-svg-A8LH4UxgadF34PzD .section--1 circle,#mermaid-svg-A8LH4UxgadF34PzD .section--1 polygon,#mermaid-svg-A8LH4UxgadF34PzD .section--1 path{fill:hsl(240, 100%, 76.2745098039%);}#mermaid-svg-A8LH4UxgadF34PzD .section--1 text{fill:#ffffff;}#mermaid-svg-A8LH4UxgadF34PzD .node-icon--1{font-size:40px;color:#ffffff;}#mermaid-svg-A8LH4UxgadF34PzD .section-edge--1{stroke:hsl(240, 100%, 76.2745098039%);}#mermaid-svg-A8LH4UxgadF34PzD .edge-depth--1{stroke-width:17;}#mermaid-svg-A8LH4UxgadF34PzD .section--1 line{stroke:hsl(60, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-A8LH4UxgadF34PzD .disabled,#mermaid-svg-A8LH4UxgadF34PzD .disabled circle,#mermaid-svg-A8LH4UxgadF34PzD .disabled text{fill:lightgray;}#mermaid-svg-A8LH4UxgadF34PzD .disabled text{fill:#efefef;}#mermaid-svg-A8LH4UxgadF34PzD .section-0 rect,#mermaid-svg-A8LH4UxgadF34PzD .section-0 path,#mermaid-svg-A8LH4UxgadF34PzD .section-0 circle,#mermaid-svg-A8LH4UxgadF34PzD .section-0 polygon,#mermaid-svg-A8LH4UxgadF34PzD .section-0 path{fill:hsl(60, 100%, 73.5294117647%);}#mermaid-svg-A8LH4UxgadF34PzD .section-0 text{fill:black;}#mermaid-svg-A8LH4UxgadF34PzD .node-icon-0{font-size:40px;color:black;}#mermaid-svg-A8LH4UxgadF34PzD .section-edge-0{stroke:hsl(60, 100%, 73.5294117647%);}#mermaid-svg-A8LH4UxgadF34PzD .edge-depth-0{stroke-width:14;}#mermaid-svg-A8LH4UxgadF34PzD .section-0 line{stroke:hsl(240, 100%, 83.5294117647%);stroke-width:3;}#mermaid-svg-A8LH4UxgadF34PzD .disabled,#mermaid-svg-A8LH4UxgadF34PzD .disabled circle,#mermaid-svg-A8LH4UxgadF34PzD .disabled text{fill:lightgray;}#mermaid-svg-A8LH4UxgadF34PzD .disabled text{fill:#efefef;}#mermaid-svg-A8LH4UxgadF34PzD .section-1 rect,#mermaid-svg-A8LH4UxgadF34PzD .section-1 path,#mermaid-svg-A8LH4UxgadF34PzD .section-1 circle,#mermaid-svg-A8LH4UxgadF34PzD .section-1 polygon,#mermaid-svg-A8LH4UxgadF34PzD .section-1 path{fill:hsl(80, 100%, 76.2745098039%);}#mermaid-svg-A8LH4UxgadF34PzD .section-1 text{fill:black;}#mermaid-svg-A8LH4UxgadF34PzD .node-icon-1{font-size:40px;color:black;}#mermaid-svg-A8LH4UxgadF34PzD .section-edge-1{stroke:hsl(80, 100%, 76.2745098039%);}#mermaid-svg-A8LH4UxgadF34PzD .edge-depth-1{stroke-width:11;}#mermaid-svg-A8LH4UxgadF34PzD .section-1 line{stroke:hsl(260, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-A8LH4UxgadF34PzD .disabled,#mermaid-svg-A8LH4UxgadF34PzD .disabled circle,#mermaid-svg-A8LH4UxgadF34PzD .disabled text{fill:lightgray;}#mermaid-svg-A8LH4UxgadF34PzD .disabled text{fill:#efefef;}#mermaid-svg-A8LH4UxgadF34PzD .section-2 rect,#mermaid-svg-A8LH4UxgadF34PzD .section-2 path,#mermaid-svg-A8LH4UxgadF34PzD .section-2 circle,#mermaid-svg-A8LH4UxgadF34PzD .section-2 polygon,#mermaid-svg-A8LH4UxgadF34PzD .section-2 path{fill:hsl(270, 100%, 76.2745098039%);}#mermaid-svg-A8LH4UxgadF34PzD .section-2 text{fill:#ffffff;}#mermaid-svg-A8LH4UxgadF34PzD .node-icon-2{font-size:40px;color:#ffffff;}#mermaid-svg-A8LH4UxgadF34PzD .section-edge-2{stroke:hsl(270, 100%, 76.2745098039%);}#mermaid-svg-A8LH4UxgadF34PzD .edge-depth-2{stroke-width:8;}#mermaid-svg-A8LH4UxgadF34PzD .section-2 line{stroke:hsl(90, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-A8LH4UxgadF34PzD .disabled,#mermaid-svg-A8LH4UxgadF34PzD .disabled circle,#mermaid-svg-A8LH4UxgadF34PzD .disabled text{fill:lightgray;}#mermaid-svg-A8LH4UxgadF34PzD .disabled text{fill:#efefef;}#mermaid-svg-A8LH4UxgadF34PzD .section-3 rect,#mermaid-svg-A8LH4UxgadF34PzD .section-3 path,#mermaid-svg-A8LH4UxgadF34PzD .section-3 circle,#mermaid-svg-A8LH4UxgadF34PzD .section-3 polygon,#mermaid-svg-A8LH4UxgadF34PzD .section-3 path{fill:hsl(300, 100%, 76.2745098039%);}#mermaid-svg-A8LH4UxgadF34PzD .section-3 text{fill:black;}#mermaid-svg-A8LH4UxgadF34PzD .node-icon-3{font-size:40px;color:black;}#mermaid-svg-A8LH4UxgadF34PzD .section-edge-3{stroke:hsl(300, 100%, 76.2745098039%);}#mermaid-svg-A8LH4UxgadF34PzD .edge-depth-3{stroke-width:5;}#mermaid-svg-A8LH4UxgadF34PzD .section-3 line{stroke:hsl(120, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-A8LH4UxgadF34PzD .disabled,#mermaid-svg-A8LH4UxgadF34PzD .disabled circle,#mermaid-svg-A8LH4UxgadF34PzD .disabled text{fill:lightgray;}#mermaid-svg-A8LH4UxgadF34PzD .disabled text{fill:#efefef;}#mermaid-svg-A8LH4UxgadF34PzD .section-4 rect,#mermaid-svg-A8LH4UxgadF34PzD .section-4 path,#mermaid-svg-A8LH4UxgadF34PzD .section-4 circle,#mermaid-svg-A8LH4UxgadF34PzD .section-4 polygon,#mermaid-svg-A8LH4UxgadF34PzD .section-4 path{fill:hsl(330, 100%, 76.2745098039%);}#mermaid-svg-A8LH4UxgadF34PzD .section-4 text{fill:black;}#mermaid-svg-A8LH4UxgadF34PzD .node-icon-4{font-size:40px;color:black;}#mermaid-svg-A8LH4UxgadF34PzD .section-edge-4{stroke:hsl(330, 100%, 76.2745098039%);}#mermaid-svg-A8LH4UxgadF34PzD .edge-depth-4{stroke-width:2;}#mermaid-svg-A8LH4UxgadF34PzD .section-4 line{stroke:hsl(150, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-A8LH4UxgadF34PzD .disabled,#mermaid-svg-A8LH4UxgadF34PzD .disabled circle,#mermaid-svg-A8LH4UxgadF34PzD .disabled text{fill:lightgray;}#mermaid-svg-A8LH4UxgadF34PzD .disabled text{fill:#efefef;}#mermaid-svg-A8LH4UxgadF34PzD .section-5 rect,#mermaid-svg-A8LH4UxgadF34PzD .section-5 path,#mermaid-svg-A8LH4UxgadF34PzD .section-5 circle,#mermaid-svg-A8LH4UxgadF34PzD .section-5 polygon,#mermaid-svg-A8LH4UxgadF34PzD .section-5 path{fill:hsl(0, 100%, 76.2745098039%);}#mermaid-svg-A8LH4UxgadF34PzD .section-5 text{fill:black;}#mermaid-svg-A8LH4UxgadF34PzD .node-icon-5{font-size:40px;color:black;}#mermaid-svg-A8LH4UxgadF34PzD .section-edge-5{stroke:hsl(0, 100%, 76.2745098039%);}#mermaid-svg-A8LH4UxgadF34PzD .edge-depth-5{stroke-width:-1;}#mermaid-svg-A8LH4UxgadF34PzD .section-5 line{stroke:hsl(180, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-A8LH4UxgadF34PzD .disabled,#mermaid-svg-A8LH4UxgadF34PzD .disabled circle,#mermaid-svg-A8LH4UxgadF34PzD .disabled text{fill:lightgray;}#mermaid-svg-A8LH4UxgadF34PzD .disabled text{fill:#efefef;}#mermaid-svg-A8LH4UxgadF34PzD .section-6 rect,#mermaid-svg-A8LH4UxgadF34PzD .section-6 path,#mermaid-svg-A8LH4UxgadF34PzD .section-6 circle,#mermaid-svg-A8LH4UxgadF34PzD .section-6 polygon,#mermaid-svg-A8LH4UxgadF34PzD .section-6 path{fill:hsl(30, 100%, 76.2745098039%);}#mermaid-svg-A8LH4UxgadF34PzD .section-6 text{fill:black;}#mermaid-svg-A8LH4UxgadF34PzD .node-icon-6{font-size:40px;color:black;}#mermaid-svg-A8LH4UxgadF34PzD .section-edge-6{stroke:hsl(30, 100%, 76.2745098039%);}#mermaid-svg-A8LH4UxgadF34PzD .edge-depth-6{stroke-width:-4;}#mermaid-svg-A8LH4UxgadF34PzD .section-6 line{stroke:hsl(210, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-A8LH4UxgadF34PzD .disabled,#mermaid-svg-A8LH4UxgadF34PzD .disabled circle,#mermaid-svg-A8LH4UxgadF34PzD .disabled text{fill:lightgray;}#mermaid-svg-A8LH4UxgadF34PzD .disabled text{fill:#efefef;}#mermaid-svg-A8LH4UxgadF34PzD .section-7 rect,#mermaid-svg-A8LH4UxgadF34PzD .section-7 path,#mermaid-svg-A8LH4UxgadF34PzD .section-7 circle,#mermaid-svg-A8LH4UxgadF34PzD .section-7 polygon,#mermaid-svg-A8LH4UxgadF34PzD .section-7 path{fill:hsl(90, 100%, 76.2745098039%);}#mermaid-svg-A8LH4UxgadF34PzD .section-7 text{fill:black;}#mermaid-svg-A8LH4UxgadF34PzD .node-icon-7{font-size:40px;color:black;}#mermaid-svg-A8LH4UxgadF34PzD .section-edge-7{stroke:hsl(90, 100%, 76.2745098039%);}#mermaid-svg-A8LH4UxgadF34PzD .edge-depth-7{stroke-width:-7;}#mermaid-svg-A8LH4UxgadF34PzD .section-7 line{stroke:hsl(270, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-A8LH4UxgadF34PzD .disabled,#mermaid-svg-A8LH4UxgadF34PzD .disabled circle,#mermaid-svg-A8LH4UxgadF34PzD .disabled text{fill:lightgray;}#mermaid-svg-A8LH4UxgadF34PzD .disabled text{fill:#efefef;}#mermaid-svg-A8LH4UxgadF34PzD .section-8 rect,#mermaid-svg-A8LH4UxgadF34PzD .section-8 path,#mermaid-svg-A8LH4UxgadF34PzD .section-8 circle,#mermaid-svg-A8LH4UxgadF34PzD .section-8 polygon,#mermaid-svg-A8LH4UxgadF34PzD .section-8 path{fill:hsl(150, 100%, 76.2745098039%);}#mermaid-svg-A8LH4UxgadF34PzD .section-8 text{fill:black;}#mermaid-svg-A8LH4UxgadF34PzD .node-icon-8{font-size:40px;color:black;}#mermaid-svg-A8LH4UxgadF34PzD .section-edge-8{stroke:hsl(150, 100%, 76.2745098039%);}#mermaid-svg-A8LH4UxgadF34PzD .edge-depth-8{stroke-width:-10;}#mermaid-svg-A8LH4UxgadF34PzD .section-8 line{stroke:hsl(330, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-A8LH4UxgadF34PzD .disabled,#mermaid-svg-A8LH4UxgadF34PzD .disabled circle,#mermaid-svg-A8LH4UxgadF34PzD .disabled text{fill:lightgray;}#mermaid-svg-A8LH4UxgadF34PzD .disabled text{fill:#efefef;}#mermaid-svg-A8LH4UxgadF34PzD .section-9 rect,#mermaid-svg-A8LH4UxgadF34PzD .section-9 path,#mermaid-svg-A8LH4UxgadF34PzD .section-9 circle,#mermaid-svg-A8LH4UxgadF34PzD .section-9 polygon,#mermaid-svg-A8LH4UxgadF34PzD .section-9 path{fill:hsl(180, 100%, 76.2745098039%);}#mermaid-svg-A8LH4UxgadF34PzD .section-9 text{fill:black;}#mermaid-svg-A8LH4UxgadF34PzD .node-icon-9{font-size:40px;color:black;}#mermaid-svg-A8LH4UxgadF34PzD .section-edge-9{stroke:hsl(180, 100%, 76.2745098039%);}#mermaid-svg-A8LH4UxgadF34PzD .edge-depth-9{stroke-width:-13;}#mermaid-svg-A8LH4UxgadF34PzD .section-9 line{stroke:hsl(0, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-A8LH4UxgadF34PzD .disabled,#mermaid-svg-A8LH4UxgadF34PzD .disabled circle,#mermaid-svg-A8LH4UxgadF34PzD .disabled text{fill:lightgray;}#mermaid-svg-A8LH4UxgadF34PzD .disabled text{fill:#efefef;}#mermaid-svg-A8LH4UxgadF34PzD .section-10 rect,#mermaid-svg-A8LH4UxgadF34PzD .section-10 path,#mermaid-svg-A8LH4UxgadF34PzD .section-10 circle,#mermaid-svg-A8LH4UxgadF34PzD .section-10 polygon,#mermaid-svg-A8LH4UxgadF34PzD .section-10 path{fill:hsl(210, 100%, 76.2745098039%);}#mermaid-svg-A8LH4UxgadF34PzD .section-10 text{fill:black;}#mermaid-svg-A8LH4UxgadF34PzD .node-icon-10{font-size:40px;color:black;}#mermaid-svg-A8LH4UxgadF34PzD .section-edge-10{stroke:hsl(210, 100%, 76.2745098039%);}#mermaid-svg-A8LH4UxgadF34PzD .edge-depth-10{stroke-width:-16;}#mermaid-svg-A8LH4UxgadF34PzD .section-10 line{stroke:hsl(30, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-A8LH4UxgadF34PzD .disabled,#mermaid-svg-A8LH4UxgadF34PzD .disabled circle,#mermaid-svg-A8LH4UxgadF34PzD .disabled text{fill:lightgray;}#mermaid-svg-A8LH4UxgadF34PzD .disabled text{fill:#efefef;}#mermaid-svg-A8LH4UxgadF34PzD .section-root rect,#mermaid-svg-A8LH4UxgadF34PzD .section-root path,#mermaid-svg-A8LH4UxgadF34PzD .section-root circle,#mermaid-svg-A8LH4UxgadF34PzD .section-root polygon{fill:hsl(240, 100%, 46.2745098039%);}#mermaid-svg-A8LH4UxgadF34PzD .section-root text{fill:#ffffff;}#mermaid-svg-A8LH4UxgadF34PzD .section-root span{color:#ffffff;}#mermaid-svg-A8LH4UxgadF34PzD .section-2 span{color:#ffffff;}#mermaid-svg-A8LH4UxgadF34PzD .icon-container{height:100%;display:flex;justify-content:center;align-items:center;}#mermaid-svg-A8LH4UxgadF34PzD .edge{fill:none;}#mermaid-svg-A8LH4UxgadF34PzD .mindmap-node-label{dy:1em;alignment-baseline:middle;text-anchor:middle;dominant-baseline:middle;text-align:center;}#mermaid-svg-A8LH4UxgadF34PzD :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Day12 MongoDB
安装
mongod
mongosh
Shell
CRUD
运算符
Mongoose
Schema
Model
01至05脚本
工程
记账本
bin先连库
生产
索引
备份
认证
【代码注释】(知识回顾图)
- 左支打基础,右支进项目;中间 Mongoose 是从 Shell 过渡到 Express 的桥梁。
- 生产项(索引、备份、认证)在开发通过后按需加深,不必第一天全配齐。
本文从 安装与服务 到 Shell CRUD 、Mongoose ODM 、记账本 Express 实战,形成完整学习路径:
- 核心概念:database / collection / document、BSON 类型、NoSQL 与 SQL 对比
- 架构设计:WiredTiger、索引类型与使用场景
- 操作实战 :
mongosh库集合文档、运算符$gt$in$or - Mongoose :Schema → Model、中间件、01~05 五步脚本与
data.json批量插入 - 生产实践:连接池、事务、备份、认证 URI
- 实战项目:记账本 Model + 路由 + 先连库后启服
- 速查归纳:命令表、常见坑、可运行 HTML 演示
高频面试题速查
把全文各章【面试考点】汇成一张速查表,面试前 10 分钟过一遍:
| # | 问题 | 一句话答案要点 |
|---|---|---|
| 1 | MongoDB 与 MySQL 核心区别? | 文档 vs 行;Schema 灵活 vs 固定列;分片原生 vs JOIN 成熟 |
| 2 | BSON 和 JSON 是什么关系? | 二进制 JSON,加长度前缀(O(1) 跳过)+ 类型标签(多出 Date/ObjectId) |
| 3 | ObjectId 由几部分组成? | 12 字节 = 时间戳 4 + 随机 5 + 计数器 3;客户端生成、近似有序 |
| 4 | 写操作如何兼顾安全与速度? | WAL(journal)顺序写日志 + Checkpoint 约 60 秒批量刷盘 |
| 5 | 复合索引最左前缀是什么? | {a,b} 索引先按 a 再按 b 排,缺最左 a 则索引失效;顺序按 ESR |
| 6 | Shell 为什么不能用 age > 18? |
查询是 BSON 匹配,比较须用 $gt/$lt 等算子 |
| 7 | Schema 与 Model 的区别? | Schema 是蓝图;model() 生成 Model 操作集合;methods/statics 分挂文档/模型 |
| 8 | 何时用聚合而非 find? |
需分组、求和、关联、派生字段时用 aggregate,计算下推到数据库 |
| 9 | $match 为何要尽量前置? |
流水线先过滤再计算,放后面是「先全量算再丢弃」,差几十倍 |
| 10 | 为什么要用连接池? | Node 单线程但 I/O 并发,多查询同时在途各需一条连接,复用免握手 |
| 11 | 事务的前提条件? | 需副本集 / 分片集群,单机 standalone 不支持 |
| 12 | 记账本为何把 connect 放 bin/www? | 保证 DB ready → HTTP listen 顺序,避免首请求竞态 |
| 13 | 接口变慢如何定位数据库问题? | explain('executionStats') 看 stage 与 totalDocsExamined/nReturned |
学习建议
- 先跑通再深究:按「Shell CRUD → Mongoose 01~05 → 记账本」三步上手,每步亲手敲一遍,再回头读对应章节的【概念与底层原理】。
- 把每个可运行 HTML 都存盘打开:BSON 类型、ObjectId 解析、索引扫描、聚合统计、Schema 校验五个演示,是把抽象概念变直觉的最快路径。
- 养成
explain习惯 :从第一天起,写完查询就跑一次explain,别等线上慢查询告警才学。 - 下一步 :掌握本章后继续学 会话控制(Session / Cookie) 实现多用户登录,再学 Mongoose 关联查询(populate) 把账单与用户表关联起来。
MongoDB 适合 Schema 频繁演进的 Web 应用;与 Express 组合是 Node 全栈常见技术栈。
延伸阅读: