MongoDB 深度解析:从原理到实践的完整指南

一篇面向实战的 NoSQL 博客:从 安装与服务Shell CRUDMongoose ODM ,到 记账本 Express 项目 的完整链路。示例可独立运行,不依赖外部讲义路径。

官方文档:mongodb.com/docs | Mongoose:mongoosejs.com/docs | 中文社区:mongoosejs.net

目录

  • 零、导读与学习价值
    • [0.1 案例覆盖清单](#0.1 案例覆盖清单)
    • [0.2 核心名词速查](#0.2 核心名词速查)
    • [0.3 为什么要学本篇](#0.3 为什么要学本篇)
  • 导读:知识架构与权威参考
  • [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 dbsuse、集合与文档增删改查、比较运算符 §0~§3
Mongoose 脚本 01 strictQueryconnectopen 内 Schema/Model、users 单条 create §4.6 ①
Mongoose 脚本 02 songs 集合、data.jsonsong_list 批量 create §4.6 ②
Mongoose 脚本 03 deleteOne / deleteMany §4.6 ④
Mongoose 脚本 04 updateOne / updateMany §4.6 ④
Mongoose 脚本 05 findOnefindByIdselectsortskiplimitexec §4.6 ③
记账本 Express bin/www 先连库再 listenmodels/accountsroutes/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 应用链路。

导读:知识架构与权威参考

本文解决什么问题

阶段 你会掌握 典型产出
安装服务 mongodmongosh、数据目录 本地 27017 可连
Shell CRUD useinsertOnefindupdateOne 命令行验证数据
运算符 $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 dbsdb.users.find() 等。
  • dbpath:数据文件存放目录,不存在时服务启动失败。

Windows / macOS 安装要点:

  • 社区版下载:MongoDB Community Download
  • 默认端口 27017 ;可将 bin 目录加入 PATH,全局使用 mongodmongosh

启动服务:

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

【代码注释】(退出命令)

  • exitCtrl+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_booktest)。
  • 一个 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 自带的时间戳给文档「免费」加创建时间------日志、订单等只读历史数据可省去 createdAtsort({ _id: -1 }) 即「最新优先」。
  • 常见坑:把超大内容(整段富文本、Base64 图片)直接塞进文档,逼近 16MB 上限后更新极慢、内存飙升;图片 / 文件应走对象存储或 GridFS,文档里只存 URL。
  • 性能与最佳实践 :字段名也会逐文档存进 BSON,海量小文档时 userNameun 多占的字节会被放大 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 没有的 DateObjectIdDecimal128Int32/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') 返回里要重点看三个字段:stageCOLLSCAN (全表扫描,坏)还是 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') 抽查核心查询,确保 IXSCANtotalDocsExamined ≈ 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=adminauthSource 为校验库)。
  • 连接失败:确认 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 关系,等价 SQL WHERE 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」游标方案。
  • 链式调用顺序建议:findsortskiplimit
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] } } 等价 SQL IN (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",库里是数字 1find({ 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/$orsort/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 未传字段时写入默认值。
  • 嵌套 profilestats 对应 BSON 子文档;location 为 GeoJSON 结构,配 2dsphere 索引。
  • Schema 选项 timestamps: true 自动维护 createdAt/updatedAttoJSON.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 上,函数内 thisUser 模型,可 return this.findOne(...)
  • 静态方法适合封装常用查询(按用户名、邮箱、角色),路由层代码更简洁。
  • findByUsername 返回 Query,可继续链式 .select();需文档实例时再 await
  • 与实例方法分工:静态负责「查谁」,实例负责「当前这条数据做什么」。

4.6 Mongoose 五步实战:01~05 脚本对照

五个 Node 脚本共用同一骨架,仅 Model 名、集合、CRUD 方法 不同。下表便于对照运行顺序:

脚本 集合 核心 API 数据准备
01-操作步骤 users create 单条 手写对象(高小乐)
02-批量插入数据 songs create(数组) 同目录 data.jsonsong_list
03-删除数据 songs deleteOne / deleteMany 先跑 02 再有数据
04-更新数据 songs updateOne / updateMany author 批量改名
05-查询数据 songs find 链式 + exec skip(90).limit(10) 分页实验

运行前:npm install mongoosemongod 已启动,库名 project04connect 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') 库名 project0401-操作步骤 脚本一致,可改。
  • 必须在 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 里定义 songsSchemasongsModel,集合名为 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: '高小乐' }) 返回单条或 nullfindById(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 仍可能连上,但在 opencreate 会竞态失败;业务写在 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 } 在索引层保证唯一;与 Schema unique 二选一或配合使用,避免重复建索引。
  • 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 }) 数组形式插入日志表,与账户更新同事务提交。
  • finallyendSession() 释放会话,避免泄漏。

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 库执行 createUserroles: ["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.4 pre('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.typeundefined 时不加类型条件,即查全部收支。

统计账单:

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 调用,省略会报错或告警。
  • $grouptype 分组,$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-projectmongodb://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 内完成 normalizePortcreateServerlisten 全套逻辑,端口默认 3000
  • require('../app') 只注册中间件与路由,app.jsconnect,避免重复连接。
  • 连接失败 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/createtitletimetypeaccountremarks
  • static 托管 public/ 下 Bootstrap、jQuery、bootstrap-datepickermain.js
  • 路由前缀 /account 对应 routes/account.jsrouter.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.jsPOST /create 对应(路由挂载在 /account 前缀下)。
  • 五个 name 属性决定 req.body 的键名,须与 Mongoose Schema 字段完全一致。
  • 静态脚本路径以 /js//css/ 开头,由 express.static('public') 映射到 public/jspublic/css
  • 页面还引入 bootstrap.cssbootstrap-datepicker.css(完整模板在记账本添加页中)。
javascript 复制代码
$('#time').datepicker({
  format: 'yyyy-mm-dd',
  language: 'zh-CN',
  autoclose: true,
  todayBtn: true,
  todayHighlight: true
});

【代码注释】

  • 表单 nameaccountsSchema 字段一一对应;POSTaccountsModel.create(req.body) 直接入库。
  • typevalue-1 / 1 数字字符串,入库后 Mongoose 转为 Number(与 Schema 一致)。
  • 日期选择器把 time 格式化为 yyyy-mm-dd 字符串写入库;列表页 index.ejs 遍历 data 展示。
  • 成功/失败页 success.ejsfail.ejs 通过 titleurl 变量跳转回列表。

第二步: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 (复数);_id 24 位 hex,删除链接 /delete/:idreq.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.ejsdata,模板里 forEach 渲染表格。
  • GET /create 只渲染空表单,不写库。
  • POST /createreq.body 来自 express.urlencoded(),字段名须与表单 name、Schema 一致。
  • create(req.body) 一次插入;成功/失败分别渲染 success / fail 页,带返回链接 url
  • GET /delete/:iddeleteOne({ _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 /accountfindindex.ejs;写路径:POST /account/createcreate(req.body)
  • 删路径:GET /account/delete/:iddeleteOne({ _id })(课堂用 GET,生产改 POST)。
  • 静态资源与 API 同域,datepicker 无需 CORS。

【实战要点】

  • 经典应用场景:内部 OA、个人记账、简易 CMS------服务端渲染 + 文档库足够快上线。
  • 常见坑app.js 里先 listen 再连库,首请求 find 报错;必须在 bin/wwwopen 里启服。
  • 性能与最佳实践 :列表 findsort({ 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() 查看是否走索引(IXSCAN vs COLLSCAN)、扫描文档数 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 再比 totalDocsExaminednReturned,差距大说明索引区分度差或复合索引顺序不对。如果查询计划本身健康,就转去查连接池是否耗尽、网络延迟、数据量是否已大到该分页归档。核心是用 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 五步速查

  1. require('mongoose') + connect(uri)
  2. connection.on('open', ...)
  3. new Schema({ ... })
  4. mongoose.model('name', schema)
  5. 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;ObjectIdDate 在导出 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) 对应 Shell db.accounts.find({ type: 1 }) 的语义。
  • type 在课堂表单中为数字,此处 1=收入、0=支出,与记账本 EJS 一致。
  • 真实库中 _idObjectId;演示用 '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 的核心价值之一是保存前自动校验 。下面用纯前端复现 requiredenummin 三类校验规则。保存为 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 对象就是简化版的 accountSchemavalidate() 函数逐字段检查 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 CRUDMongoose ODM记账本 Express 实战,形成完整学习路径:

  1. 核心概念:database / collection / document、BSON 类型、NoSQL 与 SQL 对比
  2. 架构设计:WiredTiger、索引类型与使用场景
  3. 操作实战mongosh 库集合文档、运算符 $gt $in $or
  4. Mongoose :Schema → Model、中间件、01~05 五步脚本与 data.json 批量插入
  5. 生产实践:连接池、事务、备份、认证 URI
  6. 实战项目:记账本 Model + 路由 + 先连库后启服
  7. 速查归纳:命令表、常见坑、可运行 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 全栈常见技术栈。


延伸阅读:

相关推荐
一 乐1 小时前
幼儿园管理系统|基于springboot + vue幼儿园管理系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·论文·毕设·幼儿园管理系统
tiancaijiben1 小时前
阿里云日志服务SLS全流程对接与深度使用指南
网络·数据库
云计算磊哥@1 小时前
运维开发宝典028-MySQL04数据库热备
数据库·adb·运维开发
五阿哥永琪2 小时前
正则表达式
数据库·mysql·正则表达式
LaughingZhu2 小时前
Product Hunt 每日热榜 | 2026-06-13
数据库·mysql
c238562 小时前
GDB 进程概念详解(下篇)—— 多进程与进阶调试能力
linux·服务器·数据库
tiancaijiben2 小时前
阿里云云备份(Cloud Backup)全量对接与使用指南
数据库·oracle
sulikey3 小时前
数据库中等值连接与自然连接的区别。为什么不建议使用自然连接?
数据库·sql·mysql·等值连接·自然连接
IT策士3 小时前
Redis 从入门到精通:分布式锁 —— 从 SETNX 到 Redlock
数据库·redis·分布式