做后端这些年,关系型数据库用了不少,但真正让我把“非关系型数据库”这个概念彻底摸清的,还是 MongoDB。MongoDB 的名头不小,生态也热闹,几乎每个谈 NoSQL 的场合都会提到它。但很多人对它的认知停留在“一个存 JSON 的数据库”这个层面,等到真要设计集合、写聚合、调查询、排查运行时崩溃的时候,才发现坑比想象中多。
这篇文章我会从 MongoDB 的定位、安装、数据建模、嵌套数组查询、聚合统计、安全认证到常见错误,一套走完。写的是我实际项目中踩过的坑和验证过的做法,适合刚接触 MongoDB 的开发者,也适合已经用了一阵子但想系统把概念补扎实的人。下面直接开聊。
1. MongoDB 是什么:非关系型数据库的定位与核心价值
1.1 关系型与非关系型的差异在哪里
关系型数据库,比如 MySQL、PostgreSQL,核心是“表 + 行 + 外键”。你在设计阶段就要把表结构定下来,订单关联用户,用户关联地址,全部通过 JOIN 联系起来。这种模式适合强事务、强一致、结构固定的业务,比如财务、订单、库存。它的缺点是:一旦业务字段频繁变化,ALTER TABLE 就成了日常,表一大锁表时间就长,团队还要为“字段加不加索引”“要不要冗余”反复沟通。
MongoDB 走的是另一条路:没有表,只有集合(collection);没有固定的行结构,只有文档(document)。文档本质上是 BSON,一种二进制 JSON 的扩展格式。一个集合里的文档可以长不一样,订单文档可以带用户信息嵌套在里头,也可以不带。这就把“联合查询”的压力转嫁到了“按需冗余”的模式上。
举个例子:在关系型数据库里,你查“订单 + 用户昵称”往往要 JOIN 两张表;在 MongoDB 里,你可以直接在订单文档里冗余一个userName字段,单表查询即出结果。这么做的代价是,需要自己维护数据一致性。这就是非关系型数据库的设计思路:用空间和一致性约束,换查询速度和灵活度。
1.2 核心概念:数据库、集合、文档
MongoDB 有三个核心概念,面试天天问,实际开发也绕不开:数据库(database)、集合(collection)、文档(document)。
你可以把数据库理解成 MySQL 里那个 database,一个 MongoDB 实例可以建多个库,比如shop_db、log_db。集合对应 MySQL 的表,但它没有固定列。文档对应一行记录,但它没有固定字段。这种一一对应的关系,让熟悉关系型数据库的人迁移起来很顺手,但要注意两个观念上的转变:
- 文档是“自包含”的,它不应该只存基本类型,还可以存数组、嵌套对象、数组嵌套对象。
- 集合不强制结构统一,但实际项目里你应该自己约定结构,否则维护成本会爆炸。
实际操作中,我会用一个 JSON 文档来描述业务对象。比如一个用户集合里的文档可能是:
{ "_id": ObjectId("6572a1f0b71a2c1a2b3c4d5e"), "name": "小李", "age": 28, "tags": ["backend", "python"], "address": { "city": "上海", "district": "浦东" }, "orders": [ { "orderId": 1001, "amount": 299 }, { "orderId": 1002, "amount": 59 } ] }这里的_id是主键,默认由 MongoDB 自动生成,类型是 ObjectId。tags是数组,address是嵌套对象,orders是数组嵌套对象。这就是 MongoDB 的典型文档结构,也直接决定了后面的查询、聚合写法跟 SQL 有很大区别。
1.3 什么样的项目适合 MongoDB
MongoDB 不是万能药,选型错了后面很痛苦。从我实践经验来看,它最适合这几类场景:
- 业务字段变动频繁,比如 CMS 系统、配置中心、埋点数据,文档结构说改就改,不用先迁移表结构。
- 需要“就近存储”关联数据的场景,比如详情页展示、订单快照。把商品信息快照直接塞进订单文档里,省掉 JOIN。
- 日志类、IoT 类高写入场景,MongoDB 写入性能不错,配合 TTL 索引自动清理过期数据很舒服。
- 原型快速迭代阶段,文档模型天然贴近 JSON,前后端联调成本低。
不适合的场景也很明确:强事务、多表关联复杂查询、对账结算类业务。MongoDB 4.0 之后虽然支持多文档事务,但性能开销和心智负担都比 MySQL 高。选型这种事,我不太迷信“哪个数据库更强”,更看重业务形态和前期的数据建模能力。
2. 安装与基础配置:从零到能连上
2.1 不同平台的安装方式
安装 MongoDB 本身不复杂,但很多人卡在“装完起不来”。先分平台说下我验证过的安装方式。
Windows 上最简单的是下载 MSI 安装包。安装时选 “Complete” 然后一路下一步,安装器会帮你注册 Windows 服务,顺便装好 Compass。装完之后打开服务管理器,找到 MongoDB 服务右键启动。如果不想用系统服务,也可以下载 ZIP 版解压,手动启动:
mongod --dbpath D:\mongodb\data这个命令要求D:\mongodb\data目录必须存在,否则 mongod 会直接退出。
macOS 上用 Homebrew 最省事:
brew tap mongodb/brew brew install mongodb-community brew services start mongodb-communityLinux 上用 apt 或 yum 都比较常规,以 Ubuntu 为例,先导入官方 GPG 密钥再 apt 安装。如果是 CentOS,用 yum 装mongodb-org包。这里建议不要用系统自带源里的老版本,除非你对兼容性有明确要求,否则版本太老会缺失一些新语法特性。
安装完成后,验证一下版本:
mongod --version mongosh --version注意:新版本官方 Shell 改名为mongosh,老命令mongo在新版本里已经不带了,别在命令行里敲mongo发现没有这个命令就一脸懵。
2.2 安装失败:最常见的坑与排查
我见过最多的安装失败场景有两类:一类是 Windows 服务启动失败,另一类是端口占用导致 mongod 退出。
Windows 上 MSI 安装完成后服务自动启动,极容易报 “Service ‘MongoDB Server’ failed to start” 或 “data directory not found”。原因通常是安装器默认把 dbpath 指向C:\Program Files\MongoDB\Server\...\data,但这个目录可能没有创建权限。解决办法是自己在某个盘根目录建一个db文件夹,然后手动修改服务参数,或者干脆用 ZIP 版、手动启动:
mongod --dbpath D:\mongodb\data --logpath D:\mongodb\log\mongod.log --fork--fork参数在 Windows 上不支持,Windows 下建议注册服务。注册服务命令:
mongod --dbpath D:\mongodb\data --logpath D:\mongodb\log\mongod.log --install之后再用net start MongoDB启动。
端口占用很常见。默认 27017 端口,被谁占了,执行命令看:
netstat -ano | findstr 27017如果发现已有进程占用,要么杀进程,要么给 mongod 指定别的端口。开发环境我试过用--port 27018起第二个实例,配置主从调试,比来回杀进程舒服很多。
还要注意存储引擎报错:WiredTiger 的数据文件版本和 mongod 版本不匹配。比如你之前用 4.x 的实例启动了数据目录,再用 6.x 的 mongod 去读,会直接报 “Unsupported WiredTiger file version”。这种问题没法修复,只能把数据目录删掉重新初始化,或者把版本对齐。所以升级大版本前一定要把数据导出备份。
2.3 启动服务与基础连通性验证
服务正常启动后,下一步就是用 Shell 连上。新版本统一用 mongosh:
mongosh --host 127.0.0.1 --port 27017连上后先执行几个基础命令看看环境:
db.version() db.runCommand({ ping: 1 }) show dbs返回{ ok: 1 }说明连接正常。如果配置了认证,连接时要带上用户名密码:
mongosh mongodb://admin:password@127.0.0.1:27017/admin这里提醒一句,命令行里直接写密码会被 shell history 记录,临时测试无所谓,生产环境建议用--username交互方式或配置连接串文件。
基础连通没问题之后,设置开机自启和服务化管理。Windows 上用服务,Linux 上用 systemd。官方 tgz 包安装时不会自动注册 systemd 服务,需要手动写 unit 文件。很多人在这一步偷懒,服务器一重启 mongod 就没了,数据目录锁还被残留进程占着,二次启动报 “Address already in use”,非常狼狈。我建议花五分钟把 systemd 服务配好,省得后面反复折腾。
3. 数据建模与文档操作:嵌套 List 查询与删除
3.1 文档建模的核心思想
MongoDB 建模和 MySQL 建模是两种思维方式。MySQL 讲究“规范化”,拆表、建关系、保持一致性;MongoDB 讲究“按查询设计文档结构”,你怎么查,数据就怎么存。从来没有绝对正确的模型,只有适不适合当前业务查询的模型。
我经常拿一个例子讲给学生和刚入行的同事:做一个订单列表页,要显示订单号、下单时间、用户昵称、商品名、商品图片、价格。MySQL 可能要查订单表、用户表、商品表三张表,MongoDB 里我直接把用户昵称、商品名、图片、单价冗余进订单文档。列表查询一次搞定,不用 JOIN。
好处是查询快,坏处是同步更新麻烦。比如用户改了昵称,历史订单里的冗余昵称是旧的。处理方式有两种:接受不一致,专门跑一个批量更新脚本;或者只冗余不会变的字段,比如商品快照。订单快照场景下,商品价格、名称本来就该是历史值,冗余恰好是合理的。
数组也是建模重点。MongoDB 文档里数组用得很频繁,标签列表、订单列表、评论列表都是数组。数组可以存基本类型:
{ "tags": ["javascript", "mongodb", "vue"] }也可以存对象数组:
{ "items": [{ "name": "键盘", "price": 199 }, { "name": "显示器", "price": 1299 }] }查 list 嵌套 list,说白了就是查数组里的对象或数组里的数组。语法跟普通字段查询很像,但有坑。
3.2 查 list 嵌套 list:数组查询详解
在线实训平台上经常有这样的任务:文档数据在 MongoDB 中的查询和删除。查询里面最容易被卡住的,就是如何查 list 嵌套 list。
假设集合里有这样一个文档:
{ "_id": 1, "title": "A 项目", "teams": [ { "name": "前端组", "members": ["小明", "小红"] }, { "name": "后端组", "members": ["小刚", "小李"] } ] }现在想查members里包含“小明”的文档。很多人第一反应是:
db.project.find({ "teams.members": "小明" })这个写法能查出结果,因为 MongoDB 会针对数组字段做元素匹配,在teams数组里找到对象,再看members数组里是否有“小明”。但这里有个精度问题:如果我想查“后端组成员里包含小李”的文档,上面写法其实也能匹配到,只要整个teams.members里任意一个元素是“小李”就行,不会去管它是哪个组的。
要精确匹配“数组里的某个对象同时满足多个条件”,就得用$elemMatch:
db.project.find({ "teams": { $elemMatch: { "name": "后端组", "members": "小李" } } })这个写法的含义是:在teams数组里,至少存在一个元素,它既满足name等于“后端组”,又满足members数组包含“小李”。这才是准确表达业务逻辑的方式。
还有一类场景是查“数组中数组的元素”。比如:
{ "categories": [["前端", "Vue"], ["后端", "Node"]] }想查包含“Vue”的文档,直接find({ "categories": "Vue" })是查不到的,因为categories数组的元素本身也是数组。用点号位置匹配也不好写。我一般用两种方式:一是借助聚合$unwind展开,二是用 JavaScript 表达式:
db.collection.find({ $expr: { $in: ["Vue", { $arrayElemAt: ["$categories", 0] }] } })这只能匹配第一层子数组,更复杂的嵌套建议先把数据模型简化。模型一旦嵌套超过两层,查询和索引都很难受,该拆集合时就拆集合。
3.3 查询与删除操作实战
查询操作先记住一个原则:能带条件的查询一定要把条件写到find里,不要查出全部再在代码里过滤。尤其在生产环境,全表扫描 + 内存过滤是性能毒瘤。
基础查询语法:
db.order.find({ "customerId": 123 }) db.order.find({ "amount": { $gt: 100, $lte: 500 } }) db.order.find({ "status": { $in: ["pending", "paid"] } }) db.order.find({ "items.name": "键盘" })排序和分页:
db.order.find({ "customerId": 123 }).sort({ "createTime": -1 }).limit(20).skip(40)注意skip深分页的问题:page 越大 skip 越多,性能越差。如果分页超过几十页,建议改用_id或时间戳游标方案:
db.order.find({ "customerId": 123, "createTime": { $lt: lastSeenTime } }).sort({ "createTime": -1 }).limit(20)删除操作同样分情况。删除一条:
db.order.deleteOne({ "_id": ObjectId("...") })删除多条:
db.order.deleteMany({ "customerId": 123 })删除并且需要拿到被删文档:
db.order.findOneAndDelete({ "_id": ObjectId("...") })这里要注意:deleteOne如果条件匹配多条,只删第一条,匹配顺序并非严格可控,最好的方式是条件里带上_id,保证精确命中。deleteMany({})会清空集合,我先说明这个操作非常危险,没有备份别乱敲。另外remove()是老 API,能跑但是不推荐,统一用deleteOne/deleteMany更规范。
4. 聚合框架:从统计需求到 Aggregation Pipeline
4.1 聚合查询的基本套路:$match + $group
MongoDB 的聚合框架,简单理解就是一条流水线:数据一个 Stage 一个 Stage 地往下传,每个 Stage 做一次加工。最核心的两个 Stage 是$match和$group。$match做过滤,$group做分组。它们的顺序决定性能:一定先$match过滤掉大部分数据,再做$group。
举个例子,统计每个用户的订单总金额:
db.order.aggregate([ { $match: { "status": "paid" } }, { $group: { _id: "$customerId", totalAmount: { $sum: "$amount" }, orderCount: { $sum: 1 } } }, { $sort: { totalAmount: -1 } } ])这条聚合做的事:先筛出已支付订单,再按customerId分组,每组对amount求和、计数,最后按总金额降序排列。输出结果里_id是分组键,totalAmount和orderCount是自己起的字段名。
$group常见操作符还有:$avg求平均值,$min/$max求最值,$push把字段值攒成数组,$addToSet和$push类似但会去重。
统计平均订单金额:
db.order.aggregate([ { $match: { "status": "paid" } }, { $group: { _id: null, avgAmount: { $avg: "$amount" } } } ])_id: null表示不分组,所有数据合成一组,适合全局统计。
4.2 常用聚合统计案例与实现
先来一个带日期的统计:按天统计新增订单数。要用$dateToString格式化时间字段。
db.order.aggregate([ { $match: { "createTime": { $gte: ISODate("2024-12-01") } } }, { $group: { _id: { $dateToString: { format: "%Y-%m-%d", date: "$createTime" } }, count: { $sum: 1 } } }, { $sort: { "_id": 1 } } ])再来一个嵌套数组展开统计:订单里有items数组,每个 item 有商品名称和销量,统计每个商品的总销量。这时必须用$unwind把数组拆成多行。
db.order.aggregate([ { $unwind: "$items" }, { $group: { _id: "$items.name", totalSales: { $sum: "$items.quantity" } } }, { $sort: { totalSales: -1 } }, { $limit: 10 } ])$unwind是把数组元素打散,文档数量翻倍,数据量大时开销不小。如果统计场景很固定,可以在写入时维护一个冗余统计集合,用事务或定时任务同步,查询时直接读统计结果,又快又省。这是 MongoDB 里非常实用的“反规范化”思路。
再看一个多阶段联用的场景:统计每个城市的用户人均消费金额,并按均值降序排列。
db.order.aggregate([ { $lookup: { from: "user", localField: "customerId", foreignField: "_id", as: "userInfo" } }, { $unwind: "$userInfo" }, { $group: { _id: "$userInfo.address.city", avgSpend: { $avg: "$amount" } } }, { $sort: { avgSpend: -1 } }, { $limit: 5 } ])$lookup类似 SQL 的 LEFT JOIN,性能不差但也没好到哪去。能用冗余字段解决就别用$lookup,能用两次简单聚合解决也别用它。这是我踩了很多次坑之后总结的经验。
4.3 聚合性能优化与易错点
聚合查询写起来爽,跑起来慢的时候你就难受了。最容易踩的坑有这几个。
第一,$match里的字段必须建索引。比如上面的status、customerId、createTime,如果过滤条件命中大量文档,没有索引就会全集合扫描。
db.order.createIndex({ status: 1, createTime: -1 })组合索引的顺序很重要,等值条件字段放前面,范围排序字段放后面。
第二,$unwind越晚用越好。先$match把无关文档过滤掉,再展开数组,能大幅减少展开后的文档总数。
第三,$lookup关联的from集合也要有对应索引。$lookup在底层逻辑上和普通find一样,localField 对应 foreignField 的字段,不建索引就是全表扫描。之前我调一个聚合,数据量 100 万,不建索引跑了十几秒,建完索引秒回。
第四,聚合结果超过 16MB 的限制。这是 MongoDB 的单文档大小限制,$group结果太大就会报错。解决方式是在聚合末尾加$out或者$merge,把结果写入一个新集合:
db.order.aggregate([ { $group: { _id: "$customerId", totalAmount: { $sum: "$amount" } } }, { $out: "order_stat_result" } ])写入新集合后,查询走普通find,后续处理也方便,还能给结果集合做索引。
5. 数据库安全与常见报错处理
5.1 认证与授权:不要裸奔
很多开发者本地起 MongoDB,默认不开启认证,开发爽快,也无所谓。但一旦服务器上部署,不开启认证等于数据库裸奔,扫描器一抓一个准。这个问题在实训平台的安全任务里经常出现,核心不复杂:创建用户、分配角色、开启认证。
先创建管理员用户:
use admin db.createUser({ user: "admin", pwd: "StrongPassword123", roles: [{ role: "root", db: "admin" }] })然后修改 mongod 配置文件,加上:
security: authorization: enabled重启 mongod,再连接时必须认证:
mongosh mongodb://admin:StrongPassword123@127.0.0.1:27017/admin授权粒度方面,业务应用不要用 root 角色,给最小权限。比如只读用户:
use shop_db db.createUser({ user: "app_readonly", pwd: "ReadOnlyPass123", roles: [{ role: "read", db: "shop_db" }] })常用角色区分如下:
| 角色 | 权限范围 | 使用场景 |
|---|---|---|
| read | 只读指定数据库 | 报表、监控账号 |
| readWrite | 读写指定数据库 | 业务应用账号 |
| dbAdmin | 管理索引、集合 | 运维管理账号 |
| root | 跨库全部权限 | 初始化引导、紧急运维 |
5.2 fassert() 的作用与触发场景
MongoDB 服务端代码里有一个fassert()函数,作用是“致命错误直接退出进程”。当你看到日志里出现类似Fatal Assertion 40414这样的字眼,往往就是 mongod 因为某些不可恢复的错误主动自杀了。这是 MongoDB 的一种自我保护机制:与其带病运行,不如立刻停止。
常见触发场景有这么几个:
- 数据文件损坏。比如磁盘写入异常、异常断电导致 WiredTiger 数据文件不一致。
- 索引文件损坏。某个索引遍历时报 checksum 错误,mongod 直接 fassert。
- 数据库版本跨大版本升级后,旧的数据文件不兼容。
- 配置文件里有人为设置冲突的参数,启动阶段就可能触发。
遇到 fassert 别慌,先做的事是备份现有的 data 目录,再尝试用--repair启动一次:
mongod --dbpath /var/lib/mongodb --repair如果 repair 能修复,再正常启动。如果修复失败,就得从最近的备份恢复。没备份就只能认栽。这也是为什么我一直强调,数据库必须开启定期备份,最好备份文件和数据库放到不同机器上。
另外一个容易被混淆的是:在 mongosh 里执行assert()这类 JS 函数,它不是服务端 fassert,只是 Shell 脚本断言失败,报错但不影响 mongod。生产环境里看到 fassert 字样,基本都是服务端消息,别把它当普通脚本错误处理。
5.3 常见异常、备份与日常维护
先做一份日常运维速查表,都是我实际碰过的场景。
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| 27017 端口无法连接 | mongod 未启动或已被防火墙拦截 | 检查服务状态,放开端口或改用内网连接 |
| “Address already in use” | 上一个 mongod 残留进程还在运行 | 用 `ps -ef |
| “Not primary and replication is disabled” | 客户端连到了副本集从节点 | 使用readPreference=secondaryPreferred或连接 primary |
| “Unauthorized” 或认证失败 | 角色或密码不匹配 | 检查连接串、库名和用户角色 |
| “Command not found: $group/push” | 聚合操作符写错或版本过老 | 确认 mongod 版本支持该操作符 |
| “Operation exceeded time limit” | 查询慢且超过最大执行时间 | 优化索引,使用maxTimeMS限制超时 |
| 磁盘空间不足导致写入失败 | 数据文件增长过快 | 清理数据、扩容磁盘、设置 TTL 索引自动清理 |
备份我用两种方式。一是mongodump逻辑备份,适合中小型数据量和灾难恢复演练:
mongodump --uri "mongodb://user:pass@127.0.0.1:27017/shop_db" --out /backup/mongodb-20250101 mongorestore --uri "mongodb://user:pass@127.0.0.1:27017/shop_db" /backup/mongodb-20250101/shop_db二是文件系统快照备份,适合大数据量,直接复制 WiredTiger 数据目录。做快照前要先做一次db.fsyncLock()和db.fsyncUnlock(),保证数据文件处于一致状态。做法:
use admin db.fsyncLock()然后复制/var/lib/mongodb整个目录,复制完再执行db.fsyncUnlock()。注意锁库期间数据库不能正常写入,必须在低峰期操作。
日常维护我还习惯用 cron 定时执行mongodump,保留最近 7 天数据,定期清理:
0 2 * * * mongodump --uri "mongodb://user:pass@127.0.0.1:27017" --out /backup/mongodb-dayly-$(date +\%F)在一个真实项目里,我曾经因为没有开启 auth 导致服务器上的 MongoDB 被外部程序扫描入侵,数据全部被加密勒索,教训非常深刻。后来我把这套认证、备份和恢复流程补上,再没出过类似问题。
6. 常用客户端工具与效率技巧
6.1 官方 Compass 与 NoSQLBooster 的选择
命令行 mongosh 是基本盘,日常看数据、调试查询效率更高的是可视化客户端。官方 MongoDB Compass 免费且更新勤快,功能足够:可视化查看集合、文档,直接可视条件建查询,也能运行 Aggregation Pipeline,每一步执行结果都能看到。对于新手理解聚合过程很有帮助。
NoSQLBooster 也是我经常用的工具,它的查询编辑器支持智能提示,查询结果可以用表格方式展示,写聚合脚本也更灵活。它对于一些自动化脚本任务比较方便,可以保存脚本片段反复用。需要说明的是,这个工具是商业软件,我建议用试用版或购买授权,网上那些所谓“破解版”往往捆绑了不明代码,开发机器上运行风险很大,为省几十块钱把项目代码和工作环境置于险境,完全不划算。
工具选型我的建议是:如果只是配合教学和日常维护,Compass 完全够用;如果写复杂聚合查询频率高,可以配一个带代码补全的工具。但最终都要能回到 mongosh,因为服务器上不一定有 GUI,排查问题时最有把握的还是命令行。
6.2 提升日常开发效率的实操心得
分享几个我实际开发中觉得特别提效的小习惯。
第一,脚本化初始化库表。不要每次手动创建集合和索引,把初始化指令写成 JS 文件:
db.createCollection("user") db.user.createIndex({ "email": 1 }, { unique: true }) db.user.createIndex({ "createTime": -1 }) db.order.createIndex({ "customerId": 1, "createTime": -1 })然后统一执行:
mongosh mongodb://127.0.0.1:27017/shop_db init.js这样换环境、给同事拷贝项目,只要跑一遍脚本就能把索引和数据模型搭好,省时还避免漏建索引。
第二,生产查询之前先在测试库跑一次explain。用explain("executionStats")查看是否命中索引、扫描了多少文档,确认查询计划合理再上线。比如:
db.order.find({ "customerId": 123 }).explain("executionStats")看totalDocsExamined和totalKeysExamined,如果两者差距太大,说明索引没覆盖字段或者写法有问题。
第三,养成给所有时间字段建索引的习惯。按时间排序、按时间范围过滤,在业务里几乎必然出现。建立组合索引时把等值条件放前面、时间范围放后面,效果最好。
第四,写聚合管道时先用小数据集验证中间结果。把$group前加一个$limit: 10,快速看$match和$unwind后的数据结构对不对,再逐步去掉限制。这么调几次,错误定位会快很多。
还有一个很多团队忽略的细节:文档字段名别用大写开头,不要包含空格,不要用点号。字段名一旦定了,改起来涉及大量数据迁移,成本极高。命名规范在初期就要定好,比如统一驼峰或统一 snake_case,别混用。
我个人在实际操作中的体会是,学习 MongoDB 最快的方式,不是先背命令,而是先建立一个“文档如何被查询”的思维。你建出来的集合、字段、数组嵌套,决定了后续每条查询和聚合的复杂度。多花时间在设计文档模型上,比后面反复优化查询要省力得多。数据模型的微小差别,在数据量大时会放大成性能和维护的巨大差别,这也是我从一个照搬 MySQL 建表习惯的新手,到现在能够根据业务自由设计文档结构的最大转变。
最后再分享一个调试技巧:真遇到复杂聚合查不出结果时,把 Pipeline 拆成一段一段执行。先单独执行$match,看返回数据是否符合预期;再加上$unwind,看数组展开是否正确;最后加$group。这么一步步推进,基本能定位到具体在哪一步就把数据滤掉了。这个方法陪我解决了大量线上问题,希望对你有用。