资讯动态

MongoDB进阶实战:查询优化、权限管理与聚合管道全解析

发布时间:2026/10/14 10:57:43 来源:尧图企业网站定制
要不要先看看执行计划再决定怎么写查询这是我在教团队新人调 MongoDB 时经常问的第一句话。很多人写查询就是find({...})一把梭等接口慢了才想起来有个东西叫索引等数据被误删了才意识到账户权限一直是裸奔状态等统计报表要跨表汇总了才开始翻 aggregate 的文档。这篇就把这三件事串起来讲清楚查询优化怎么用执行计划对症下药、账户权限怎么从零搭一套够用的体系、aggregate 聚合管道怎么从只会$match$group进化到能处理真实业务里的复杂统计。适合正在用 MongoDB 做实际项目、对增删改查已经比较熟、想往进阶走的开发者。本文不涉及嵌入式开发或集群运维的深度内容但凡是单机或副本集环境下常用的优化、鉴权、聚合手段都会写到位。1. 查询优化先看懂 MongoDB 是怎么执行你的查询1.1 explain 的三种模式先别急着建索引遇到慢查询第一步不是去加索引而是搞清楚 MongoDB 到底怎么跑的这条查询。执行计划explain就是我们手里的 X 光机。MongoDB 的 explain 支持三种模式分别对应不同的信息深度。queryPlanner只分析并返回查询计划不真正执行查询。最快的模式适合初筛阶段。executionStats执行查询并返回实际执行统计比如扫描了多少文档、命中了多少索引、实际耗时多少。最常用。allPlansExecution对查询优化器考虑的每一种候选计划都执行并统计。适合疑难杂症。实际操作中我用得最多的是executionStats。在 mongo shell 里直接执行db.users.find({ status: active, age: { $gt: 18 } }).explain(executionStats)返回结果里重点关注几个字段。winningPlan.stage是最直观的COLLSCAN表示全表扫描IXSCAN表示走了索引FETCH表示根据索引回表取文档。如果看到winningPlan.inputStage.stage是COLLSCAN那这条查询大概率就是慢查询的元凶。executionStats里还要看两个关键数字totalDocsExamined是实际扫描的文档数totalKeysExamined是扫描的索引键数量。理想情况下这两个数字应该和你最终返回的结果数差不多。如果totalDocsExamined是几十万返回结果只有十几条那就是典型的全表扫描索引根本没起作用。再补充一个容易忽略的点explain 出来的executionTimeMillis受当前负载影响较大测试时尽量在低峰期或者用db.collection.explain(executionStats).find(...)多跑几次取稳定值。生产环境不建议直接跑 explain(executionStats)尤其是大表因为它是真实执行的我一般会先跑queryPlanner模式看计划确认没问题再决定下一步。1.2 复合索引设计的 ESR 原则比想象中更重要给单个字段加索引谁都会真正拉开差距的是复合索引的设计。我在代码评审里见过最多的问题是明明建了复合索引查询计划却显示COLLSCAN或者虽然走了索引但totalKeysExamined大得吓人。这往往是因为字段顺序设计不合理。MongoDB 官方给的最佳实践是 ESR 原则Equality等值匹配字段放最前Sort排序字段其次Range范围查询字段放最后。这个顺序不是拍脑袋定的它决定了索引前缀的利用效率。举一个真实场景。某个订单系统的查询条件是status: paid要求按createTime倒序同时amount在某个区间内。按照 ESR 原则复合索引应该设计成db.orders.createIndex({ status: 1, createTime: -1, amount: 1 })status是等值条件放最前面createTime用于排序放第二位注意方向和查询里的排序方向保持一致amount是范围条件放最后。这样做的好处是MongoDB 可以先通过status精确锁定数据块然后利用createTime的有序性直接按顺序读取不需要额外做SORT阶段最后再对amount做范围过滤。如果顺序反了比如{ amount: 1, status: 1, createTime: -1 }那么范围条件amount会占据索引前缀后面的status和createTime就没办法高效利用可能出现扫描大量索引键的情况。还有一个常踩的坑给单个字段分别建两个索引和建一个复合索引是两回事。单字段索引无法满足复合查询的最优执行覆盖索引Covered Query就更无从谈起。所谓覆盖索引就是查询所需的全部字段都包含在索引里MongoDB 可以直接从索引返回结果不需要回表 FETCH。如果一个查询频繁出现且返回字段固定可以设计专门用于覆盖查询的复合索引这招在高频读接口里效果非常明显。// 对只返回 title、price、status 的查询可以建这个索引 db.products.createIndex({ status: 1, category: 1 }, { name: cover_query_idx })1.3 profiling 慢查询日志不靠猜靠数据很多开发者意识不到 MongoDB 自带慢查询日志还在拍脑袋猜可能是这条查询慢吧。MongoDB 的 profiling 功能会把超过指定耗时的操作记录到system.profile集合里直接能用。开启方式很简单// 记录所有耗时超过 100ms 的操作 db.setProfilingLevel(1, { slowms: 100 }) // 查看当前 profiling 状态 db.getProfilingStatus()profiling 级别有 3 档0 表示关闭1 表示只记录慢操作2 表示记录所有操作。生产环境我建议设成 1而且slowms要根据业务来调。如果业务本身响应要求高可以设成 50ms如果表量大设成 200-300ms 也行原则是能暴露出真实问题又不至于日志泛滥。查看慢查询记录db.system.profile.find({ op: query }).sort({ ts: -1 }).limit(20).pretty()这里有条经验值得分享system.profile本身也是一个集合如果 profiling 级别设为 2会导致这个集合急剧膨胀。我见过某开发环境把 profiling 开到 2 跑了一周把磁盘空间直接打满的案例。所以要么定期清理要么就老老实实用级别 1。另外db.currentOp()也是排查线上慢操作的好帮手。当业务突然卡顿可以用它看看当前正在执行哪些操作、执行了多久db.currentOp({ active: true, secs_running: { $gt: 5 } })这个命令会列出运行超过 5 秒的操作非常适合应急排查。配合 killOp 可以强制终止异常操作不过要小心killOp 是强杀可能留下中间状态我一般只在确认操作卡死时才用。2. 账户权限设置不要再用 root 干活了2.1 权限模型先理解角色、库、认证库之间的关系MongoDB 的账户权限体系核心就三件事用户、角色、资源。一个用户在某个认证库下创建被赋予一个或多个角色每个角色定义了在指定资源上可以执行哪些操作。这里最容易被新手误解的是认证库Authentication Database的概念。MongoDB 创建用户时必须指定一个库用户就是在那个库下注册的。登录时也要指定认证库比如用db.auth(username, pwd)或者连接串里的authSourceadmin。用户的权限可以在多个库上生效但认证时查找用户的数据库是固定的。我见过不少人踩过这个坑在业务库app_db下创建了一个用户角色给了readWrite权限但连接串里写的是authSourceadmin结果一直验证失败。因为认证库写错了服务器自然找不到这个用户。另一个基础认知MongoDB 启动鉴权需要先创建至少一个管理员用户。这个管理员用户要在admin库下创建并授予userAdminAnyDatabase角色。没有这一步你根本没法开启鉴权因为开启后你连创建用户的入口都没了。2.2 内置角色怎么选read 和 readWrite 是底线MongoDB 自带了很多内置角色我按实际使用频率排个序read只读适合报表、BI 查询专用账号。readWrite读写适合常规业务应用。dbAdmin库管理能管理索引、集合校验等适合 DBA 用。userAdmin库级用户管理适合管理业务库的人员。dbOwner库的完全控制是readWrite dbAdmin userAdmin的组合。readAnyDatabase/readWriteAnyDatabase/userAdminAnyDatabase/dbAdminAnyDatabase全局版本一般只给运维或平台账号用。clusterAdmin集群管理副本集和分片集群的运维必须。我的建议是应用程序账号能不给dbOwner就不给绝大多数业务只需要在对应库上的readWrite。如果应用里只用到了查询、插入、更新、删除没有建索引之类的管理操作readWrite就是最合适的角色。千万别图省事一个超级账号走天下。对于开发环境的初始化我会单独建一个app_dev账号只给当前开发库的readWrite再给 DBA 一个dbOwner账号这样即使开发账号误操作影响面也控制在单个库。线上环境再加一个只读账号拿去做数据校验和报表查询天然隔离。这套设计成本极低但能让权限边界清晰很多。2.3 用户管理实操创建、更新、锁定、删除从一个空实例开始第一步是在admin库创建管理员use admin db.createUser({ user: adminUser, pwd: 一个足够复杂的密码, roles: [{ role: userAdminAnyDatabase, db: admin }] })第二步是设置 MongoDB 启用鉴权。如果是配置文件启动在mongod.conf里加security: authorization: enabled重启后所有连接都必须认证。创建一个业务应用账号use app_db db.createUser({ user: app_service, pwd: StrongPassword_2024, roles: [ { role: readWrite, db: app_db }, { role: read, db: report_db } ] })这个示例展示了一个很重要的能力一个用户可以拥有多个库上的不同角色。实际应用中业务服务可能需要读写自己主库的数据同时只读地访问基础配置库的内容完全可以在一个账号上组合。日常管理用得最多的是这几个命令// 修改密码或角色 db.updateUser(app_service, { pwd: NewPassword_2024, roles: [{ role: readWrite, db: app_db }] }) // 查看用户信息注意 roles 数组里每个角色都带着 db db.getUser(app_service) // 删除用户 db.dropUser(app_service)还有两个不常提但非常实用的操作。一是锁定用户不用删除就能让账号失效db.updateUser(app_service, { authenticationRestrictions: [{ clientSource: [127.0.0.1] }] })二是禁用用户密码登录只能通过证书或外部认证一般企业环境用x.509证书比较多。对于一个小团队来说先掌握密码认证下的用户生命周期管理就够用了。2.4 自定义角色权限不够用时的正确姿势内置角色覆盖了大多数场景但有些需求是内置角色做不到的。比如允许某个用户只读取某几个特定集合而不让他看库里的其他集合。内置read角色是库级别的做不到这种精细化控制。这时就需要自定义角色。自定义角色的核心是定义privileges数组每个权限由resource和actions组成。resource可以是某个库的某个集合也可以是整个库或整个集群actions是具体的操作类型比如find、insert、update、remove、createIndex等。举个例子给数据分析同事创建一个只能读取orders和users两个集合的只读账号use admin db.createRole({ role: orderAnalyst, privileges: [ { resource: { db: app_db, collection: orders }, actions: [find, count] }, { resource: { db: app_db, collection: users }, actions: [find, count] } ], roles: [] })然后把这个角色赋予用户use admin db.createUser({ user: analyst_user, pwd: Analyst2024, roles: [{ role: orderAnalyst, db: admin }] })注意自定义角色自身也可以继承其他角色放在roles数组里。我建议在非必要情况下不要做角色继承因为一旦权限出现问题排查看多层继承关系太痛苦了。自定义角色还有一个天然优势当业务需要从只能查两个集合扩展到还要能查第三个集合时只需修改角色定义而不需要动所有用户的配置管理和审计都方便很多。3. aggregate 聚合管道复杂统计的核心武器3.1 先吃透这几个基础 stagematch、group、project、sortaggregate 聚合管道是把多个处理阶段串联在一起数据流像流水线一样经过每个阶段都会被加工一次。这个设计非常契合真实业务里先过滤、再分组、后投影的统计逻辑。用生活化类比来解释它就像做菜先洗菜过滤、再切菜变换、然后下锅分组计算、最后装盘输出格式。最常见的四个基础阶段是$match过滤数据作用类似find的查询条件。注意一个优化原则$match尽量放在管道最前面它能尽早减少后续阶段处理的数据量。而且$match如果命中了索引和普通查询一样也会走索引。$group分组聚合是group by的 SQL 对应物。_id字段指定分组的键可以是一个字段也可以是一个由多个字段组成的复合键。聚合表达式可以放在其他字段位置上。$project控制输出字段可以对字段做重命名、算术运算、字符串截取、条件表达式等。很多人只把它理解为选择要输出的字段其实$project的能力远不止于此。$sort排序和其他数据库没什么区别但有一点要注意如果在$group之前做$sort且排序字段命中了索引MongoDB 可以直接利用索引顺序省掉内存中的大排序。来看一个最常见的组合用法。假设我们要统计每个城市的活跃用户数db.users.aggregate([ { $match: { status: active } }, { $group: { _id: $city, count: { $sum: 1 } } }, { $sort: { count: -1 } }, { $project: { _id: 0, city: $_id, count: 1 } } ])每个阶段都值得细说。$match先过滤掉非活跃用户从几十万文档减到几万$group按城市分组用$sum: 1累加每组的文档数$sort按计数降序排列热门城市排在最前$project最后把_id重命名为city并隐藏_id字段让输出更干净。这个管道的执行顺序很关键。如果先$group再$match$group阶段会处理全表数据性能差别是数量级的。我见过有人把$match写在后面跑出来的结果还没错但慢得让人怀疑人生。3.2 $unwind 和 $lookup处理数组字段和跨表关联真实业务数据经常带数组字段。比如用户表里有一个tags数组存着用户的标签。如果我们要统计每个标签有多少用户直接$group是做不到的因为数组字段整体是一个值。$unwind就是用来把数组拆分成多条文档的工具。db.users.aggregate([ { $unwind: $tags }, { $group: { _id: $tags, count: { $sum: 1 } } }, { $sort: { count: -1 } } ])$unwind会把一条包含tags: [a, b]的文档拆成两条一条tags: a一条tags: b然后再分组统计。有一个细节需要注意如果文档里的tags字段为空数组或不存在$unwind默认会丢弃这条文档。如果不想丢可以用preserveNullAndEmptyArrays: true保留。$lookup是 MongoDB 实现连表查询的方式对应 SQL 里的 left outer join。这个功能解决了我以前最头疼的问题数据分散在不同集合里统计时要先把数据拉到内存用代码关联。现在一条聚合管道就能解决。比如订单集合里存了userId用户的基本信息比如城市在用户集合里。想按城市统计订单金额db.orders.aggregate([ { $lookup: { from: users, localField: userId, foreignField: _id, as: userInfo } }, { $unwind: $userInfo }, { $group: { _id: $userInfo.city, totalAmount: { $sum: $amount }, orderCount: { $sum: 1 } }}, { $sort: { totalAmount: -1 } } ])$lookup的执行逻辑是对orders的每条文档拿localField这里是userId去users集合里匹配foreignField这里是_id匹配结果以数组形式放进userInfo字段。因为每个订单对应用户只有一条所以紧接着$unwind把数组拆回单条文档方便后续处理。$lookup的性能一直是被讨论的焦点。默认情况下它走的是全表扫描匹配如果要关联的数据量很大建议给foreignField建索引。另外 MongoDB 新版本支持在$lookup里用管道pipeline语法可以在关联时先过滤、分组再拼结果能显著减少数据传输量。数据量大时需要仔细评估执行方式避免出现严重的性能瓶颈。3.3 聚合管道的优化内存限制、allowDiskUse 和 stage 顺序聚合管道有一个容易被忽略的限制默认情况下单个 stage 最多使用 100MB 内存。如果某个$group或$sort处理的数据量超过了这个限制会直接报错。这时可以在命令后面加上allowDiskUse: true允许 MongoDB 把临时数据写到磁盘。但是allowDiskUse不是银弹。磁盘 I/O 比内存慢几个数量级而且实际测试中一旦启用磁盘写入管道执行时间通常会成倍增长。所以正确的思路是尽量优化管道结构避免数据量被撑到内存上限而不是一上来就开磁盘。优化管道结构的核心原则就几条$match能早放就早放把数据量减到最小。$sort在$group之前并且利用索引避免内存大排序。$project尽量删掉不需要的大字段减少管道里传递的数据体积。$limit在数据量可控的前提下可以配合$sort实现取 Top N的高效计算。还有一个很实用的写法用$facet在一个管道里并行执行多个子管道适用于同一份原始数据要出多个统计结果的场景。比如既要总订单量又要按状态的分布还要 top 10 用户一个$facet就能搞定比跑三条管道更好。db.orders.aggregate([ { $match: { paid: true } }, { $facet: { totalStats: [{ $count: totalOrders }], byStatus: [{ $group: { _id: $status, count: { $sum: 1 } } }], topUsers: [ { $group: { _id: $userId, total: { $sum: $amount } } }, { $sort: { total: -1 } }, { $limit: 10 } ] } } ])这套写法在统计报表类接口中非常实用。需要注意的是$facet的各个子管道是并行执行的任何一条子管道出错都会导致整条命令失败所以每个子管道里的逻辑要单独验证过再合到一起。4. 聚合与查询的实战案例一份可复用的统计需求实现4.1 需求按小时统计订单量、销售额和客单价先看一个我最近在一个模拟项目中处理的真实需求。业务方想要一张分时交易趋势图横轴是小时0-23纵轴是订单量、销售额、客单价。数据在orders集合里有一个createTime字段存的是 ISODate还有一个amount字段存订单金额。第一版代码是业务方自己写的逻辑是const hours []; for (let hour 0; hour 24; hour) { const result await db.orders.find({ createTime: { $gte: startOfHour(hour), $lt: endOfHour(hour) } }).toArray(); // 然后遍历数组算总额和单量 }这个写法在数据量只有几千条时看起来没啥问题但一旦订单量涨到几十万就是灾难一天内要把订单表全表扫描 24 次。优化思路很简单把循环换成一条聚合管道db.orders.aggregate([ { $match: { createTime: { $gte: ISODate(2024-01-01T00:00:00Z), $lt: ISODate(2024-01-02T00:00:00Z) } } }, { $group: { _id: { $hour: $createTime }, orderCount: { $sum: 1 }, totalAmount: { $sum: $amount } } }, { $project: { _id: 0, hour: $_id, orderCount: 1, totalAmount: 1, avgOrderAmount: { $divide: [$totalAmount, $orderCount] } } }, { $sort: { hour: 1 } } ])这个管道的巧妙之处在于$hour这个日期运算符它可以直接从ISODate字段里提取出小时数。这里补充一些常用的日期运算符$year、$month、$dayOfMonth、$dayOfWeek、$hour、$minute组合它们可以按任意时间粒度分组比如按天、按月、按季度统计。$match里的时间范围条件最好能命中createTime上的普通单字段索引否则还是全表扫描。这也是一个高频场景看似用了聚合管道但没有索引支撑照样慢。4.2 需求统计每个用户的消费等级还有一个典型需求根据用户的累计消费金额划分消费等级比如大于 10000 是 VIP 用户5000 到 10000 是普通会员否则是普通用户。这个需求在 SQL 里用CASE WHEN就行在 MongoDB 里可以用$cond和$bucket实现。更通用的写法是$bucket它专门用来做分箱统计db.orders.aggregate([ { $group: { _id: $userId, totalSpent: { $sum: $amount } } }, { $bucket: { groupBy: $totalSpent, boundaries: [0, 5000, 10000, Infinity], default: unknown, output: { userCount: { $sum: 1 }, avgSpent: { $avg: $totalSpent } } } } ])$bucket会把totalSpent落在boundaries定义的分箱边界里。边界数组是左闭右开区间即[0, 5000)在第一箱[5000, 10000)在第二箱[10000, Infinity)在第三箱。Infinity能兜住所有超大值是我非常推荐写的边界否则超出最大值的数据会全掉进default桶里统计结果就不准了。如果要做细粒度的等宽分箱$bucketAuto更省事它会自动根据数据的分布情况把数据尽可能平均地分到指定数量的箱子里不需要手工指定边界。适合我要分 10 段每段数据量尽量接近这类探索性分析需求。4.3 需求跨集合关联后的留存分析留存分析是运营场景里的经典难题。有一个users集合记录用户注册时间还有一个user_events集合记录用户每天的登录事件。要统计某一天注册的用户在注册后第 1 天、第 7 天、第 30 天还有多少活跃写法可以是这样db.users.aggregate([ { $match: { registerDate: { $gte: ISODate(2024-05-01), $lt: ISODate(2024-05-08) } } }, { $lookup: { from: user_events, let: { userId: $_id, regDate: $registerDate }, pipeline: [ { $match: { $expr: { $and: [ { $eq: [$userId, $$userId] }, { $eq: [$type, login] } ] } } }, { $project: { dayDiff: { $trunc: { $divide: [ { $subtract: [$eventDate, $$regDate] }, 86400000 ] } } } }, { $match: { dayDiff: { $in: [1, 7, 30] } } } ], as: activeDays } }, { $project: { _id: 1, activeDays: 1 } } ])这段的重点是$lookup的管道式用法let声明了可以在子管道里引用的变量$expr负责做字段间的动态比较$subtract计算两个日期的毫秒差$divide除以一天的毫秒数得到天数差$trunc取整。这套组合拳解决了关联查询时要做字段计算和过滤的场景是比普通 localField/foreignField 更强大但也更复杂的用法。实际跑这个管道时有个性能感受非常明显如果user_events表的数据量很大管道式$lookup的每一层都要仔细设计索引。userId上的索引几乎是必须的eventDate上的索引也能帮上忙。我最后跑的方案是先给user_events建了复合索引{ userId: 1, eventDate: 1 }执行时间比无索引版本下降了一个数量级。5. 常见问题与排查技巧实录5.1 问题速查表现象常见原因解决思路查询慢explain 显示 COLLSCAN没建索引或索引未被命中查看查询条件字段是否建了复合索引使用 ESR 原则重建索引明明存在却没用上复合索引字段顺序不合理或查询里对索引字段做了函数操作按 ESR 原则调整字段顺序避免对索引字段使用$expr等非普通表达式创建用户后登录失败认证库写错检查连接串里的authSource是否与用户创建时的库一致开了鉴权后连不上数据库没有提前创建管理员用户先以本地免鉴权方式启动在 admin 库创建userAdminAnyDatabase用户aggregate 报内存超限某 stage 数据量超过 100MB优化管道顺序必要时allowDiskUse: true$lookup 太慢关联字段无索引给 foreignField 建索引或改用管道式 $lookup 先过滤再关联profiling 开启后磁盘暴涨级别设为 2导致记录所有操作改为级别 1定期清理 system.profile删除用户后连接还在工作已有连接不会被立即踢掉需要重启服务或手动 kill 对应连接5.2 排查过程实录一次索引已建但查询还是慢的定位有一次我排查一个慢查询现象非常典型开发同事说status和createTime都建了索引但查询还是很慢。我先跑 explain看到winningPlan.inputStage.stage是IXSCAN不是COLLSCAN说明确实走了索引。但totalDocsExamined有 8 万返回结果只有 500 条。这个数字对比说明问题出在索引选择性上。查询条件是{ status: active, createTime: { $gt: 某时间点 } }而status字段取值只有两三种大量文档都集中在 active 上。单字段索引status虽然命中但扫描出的候选集巨大回表检查createTime时才过滤掉大部分文档等于索引帮了倒忙。解决方案还是复合索引但要重新设计顺序。status的区分度太低应该把createTime放前面db.events.createIndex({ createTime: 1, status: 1 })这次调整让totalDocsExamined从 8 万降到了几百。这个场景给我的教训是建索引不能只看有没有字段还得看字段的区分度cardinality。区分度高的字段如时间戳、唯一 ID更适合放在索引前缀位置区分度低的字段如状态、布尔值放后面。5.3 权限安全方面的几个补充提醒权限相关的问题我在实际支持中见过太多。列举几个容易踩但经常被忽视的密码强度。MongoDB 默认使用 SCRAM-SHA-256 认证弱密码在暴力破解面前没有防御力。生产环境密码尽量用随机生成的强密码并定期轮换。给应用账号开通不必要的全局权限。一个普通业务应用账号被赋予了root或clusterAdmin一旦应用被注入攻击数据库等于门户大开。多个环境共用一套账号体系。开发库和线上库的账号应该完全隔离避免开发环境误操作影响到生产数据。忘记密码的处理。如果真的忘了需要用本地异常登录的方式在未开启鉴权的模式下启动一次 MongoDB重置用户信息后再恢复正常模式。这个过程有操作门槛需要提前在运维文档里记录流程免得紧急时手忙脚乱。关于第一个管理员账号还有一种常见误区有人以为开启鉴权后原来未鉴权状态下创建的用户会自动有管理员权限。其实不然如果创建用户时没有显式赋予userAdminAnyDatabase之类的角色那这个用户登录后什么管理操作都做不了。所以初始化授权的第一步要确认清楚角色。5.4 聚合管道查错技巧写聚合管道最怕的是逻辑写复杂了出错了还不知道错在哪。我的习惯是先小后大逐段验证。拿到需求后不要一口气把整条管道写完而是先把$match单独跑一遍确认过滤条件能查出正确数量的数据。然后加$group看分组键和统计值是否符合直觉用一小段时间范围验证。每组阶段验证通过后再串起来这样才能准确判断是哪一步出了问题。另外一个实用技巧是临时在管道中间加$limit限制输出的文档数量。比如$lookup返回的数组可能巨大先用$limit: 5观察结果确认字段对应关系无误后再放开限制。这能避免调试时被海量中间数据淹没。$project 阶段也经常被用来调试如果你想确认某个字段在管道里到底是什么值完全可以先$project把它原样输出查看后再做真正的变换。这种边查边改的方式比对着结果猜表达式要高效得多。写在最后我自己在使用 MongoDB 的过程中最大的体会是这三个主题——查询优化、账户权限、聚合管道——不是孤立的知识点而是同一个工程问题的不同侧面。查询优化解决的是能不能扛住数据量增长账户权限解决的是系统安不安全聚合管道解决的是能不能高效满足数据需求。一个新项目从设计阶段就把这三件事想清楚后续基本不会遇到线上慢查询被删库统计需求写不出来这类灾难性问题。最后再分享一个小技巧在正式环境调整索引或跑特别重的聚合管道之前先在副本集的隐藏节点上验证一遍。隐藏节点不会承接线上读流量用它来跑 explain 和性能测试既不打扰用户又能拿到接近真实数据的执行计划。这个方法我用了很久帮我在线上规避过好几次潜在的隐患。后续如果你的数据量、并发量继续涨建议花时间研究一下 MongoDB 的索引下推、分片集群的聚合优化这些都是建立在今天这三块基础之上的进阶方向。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑