图解 MongoDB 08|ESR 原则:复合索引的字段顺序怎么定

作者:十三Tech日期:2026/6/23

复合索引是 MongoDB 性能优化里最常用、也最容易用错的工具。很多人建复合索引的方式是「查询用到哪几个字段,就按想到的顺序建一个」,结果发现索引只用了第一个字段,查询照样慢。问题不在「有没有建索引」,而在字段顺序

复合索引的字段顺序,决定了它能服务哪些查询、能用上几个字段。同样的三个字段 {a, b, c},排成 {a, b, c}{c, b, a} 是两棵完全不同的 B-tree,能加速的查询也完全不同。这一篇讲清楚复合索引字段排序的核心原则——ESR(Equality, Sort, Range),这是 MongoDB 索引设计里最值钱的一条经验。

先把机制边界说清楚

复合索引在底层是一棵 B-tree,它的「键」是多个字段值的拼接。{a:1, b:1} 这棵树,先按 a 排序,a 相同再按 b 排序。这带来一个关键性质:复合索引是前缀有序的

  • 能用 {a:1, b:1} 服务 {a:5}(前缀匹配)。
  • 能用它服务 {a:5, b:3}(完整匹配)。
  • 不能用它单独服务 {b:3}(缺了前导字段 a,b 在树里是无序的)。

这个「前缀有序」性质,是 ESR 原则的物理基础。我们要做的,就是把查询里的字段,按「让索引尽量多用、尽量别中断」的目标排序。

ESR:等值、排序、范围

ESR 是复合索引字段顺序的黄金法则:Equality(等值)→ Sort(排序)→ Range(范围)。这个顺序不是拍脑袋定的,是 B-tree「前缀有序 + 范围中断」性质推导出来的必然结果。

E · 等值字段放最前

等值条件({status: "paid"}{userId: ObjectId(...)})是最精确的定位。把它放在复合索引最前面,是因为它能把候选集一次砍到最小:树里直接跳到 status="paid" 这个子区间,后续字段只在这个小区间内继续。

等值字段的选择性越高(唯一性越强),砍掉的范围越大。userId 这种近乎唯一的字段放最前,效果远好于 status 这种只有几个枚举值的字段。所以多个等值字段时,选择性高的排前面

S · 排序字段居中

排序条件(.sort({createdAt: -1}))放等值之后、范围之前。原因是:索引本身是有序的,如果排序字段在索引里的顺序和查询要求一致,就能直接用索引的顺序,省掉内存排序

内存排序(explain 里的 SORT 阶段)有两个坏处:一是慢,要把候选文档全部读进内存排;二是有内存上限(find().sort() 默认 32MB,聚合 $sort 默认 100MB),候选集太大直接报错 QueryExceededMemoryLimitNoDiskUseAllowed。让排序走索引(IXSCAN 直接返回有序结果),是避免这类问题的正解。

排序字段必须在范围字段之前,是因为范围字段会「打散」后续字段的有序性。一旦索引用到范围条件,它访问的是树里一段连续区间,区间内后续字段就不再全局有序了,排序就没法靠索引完成。

R · 范围字段放最后

范围条件({amount: {$gt: 100}}{createdAt: {$gte: ...}})放最后,是因为它会中断后续字段的索引使用。范围访问的是树里一段区间,这段区间里后续字段的值是跳跃的,没法再用来做等值定位或排序。

所以范围字段一定要放在等值和排序之后,让前面的字段先把范围砍到最小、排序需求被索引满足,再用范围做最后的过滤。范围字段之后不要再接需要索引支持的字段(接了也用不上)。

一个完整的例子

把 ESR 用到一个真实查询上:

1// 查询:已支付、金额大于100、按时间倒序
2db.orders.find({
3  status: "paid",
4  amount: { $gt: 100 }
5}).sort({ createdAt: -1 })
6

按 ESR 分析:

  • E(等值):status
  • S(排序):createdAt
  • R(范围):amount

所以正确的复合索引是 { status: 1, createdAt: -1, amount: 1 }。注意 createdAt 的方向要和查询的 .sort({createdAt: -1}) 一致(都是 -1),否则索引顺序和排序要求不符,还是得内存排序。

如果建错了,比如 { amount: 1, status: 1, createdAt: 1 },把范围字段 amount 放最前:索引只能用 amount 范围扫描一段大区间,statuscreatedAt 的有序性都被打散,排序退回内存 SORT,查询既慢又可能报错。

几个容易踩的边界

排序方向必须和索引一致。 {a:1, b:-1} 的索引,能服务 .sort({a:1, b:-1}),也能服务 .sort({a:-1, b:1})(反向全用),但不能服务 .sort({a:1, b:1})(方向不一致)。复合索引里每个字段的方向都要匹配。

$in 算半个范围。 {status: {$in: ["paid", "shipped"]}} 虽然看起来像等值,但实际是多个等值的并集,行为接近范围,会削弱后续字段的使用。大量 $in 的字段,位置要往后放。

前缀索引能复用。 建了 {a:1, b:1, c:1},它能同时服务 {a}{a,b}{a,b,c} 三种查询的前缀。所以设计复合索引时,让多个查询共享一个前缀,能减少索引总数。

不要为了排序硬加字段。 如果查询的等值条件已经把候选集砍到很小(比如几十条),排序用内存也很快,不必为了省掉 SORT 在索引里塞排序字段。ESR 是优化方向,不是死规矩。

判断框架

把 ESR 收敛成几条可执行的步骤:

  1. 把查询条件分成三类:等值(E)、排序(S)、范围(R)。
  2. 等值字段按选择性从高到低排在前。
  3. 排序字段紧跟等值,方向和 .sort() 一致。
  4. 范围字段放最后。
  5. 多个查询共享前缀,尽量用一个复合索引覆盖多个查询。
  6. 建完后用 explain 验证:看走了几个字段、有没有 SORTtotalDocsExamined 是否接近返回数。

ESR 原则的价值,是让复合索引从「拍脑袋排顺序」变成「按 B-tree 性质推导顺序」。下一篇会讲怎么用 explain 验证索引到底用上了几个字段。


关于十三Tech

All in AI Agent 方向的架构师,专注 AI 工程实践。

相信 AI 是程序员的最佳搭档,帮助每一位开发者驾驭 AI。

公众号搜索「十三Tech」

本文首发:rubyfun.cn/posts/%E5%9…


图解 MongoDB 08|ESR 原则:复合索引的字段顺序怎么定》 是转载文章,点击查看原文


相关推荐


古法编程秘籍(七):互联网到底是什么?把两台电脑怎么说话搞懂就够了
JustHappy2026/6/15

Hi!这里是 JustHappy 这是专为编程初学者准备的专栏。这次我们来“上网”,但是互联网不是网页,也不只是 HTTP,它本质上是不同机器上的程序按规则交换数据。看懂客户端、服务器、协议、操作系统、网卡这条链,再分清 HTTP 的“一问一答”和 WebSocket 的“持续通信”,你的网络世界观才算真正搭起来。 上一篇我们讲到这里: 代码会执行、会发起 IO、硬件会工作、中断会通知CPU、操作系统会处理。 最后程序收到事件,再继续执行。 写到这里,很多人会自然冒出一个新问题: 如果 IO


Java MyBatis-Plus 实战指南:用 BaseMapper、Wrapper 和分页写好数据层
唐青枫2026/6/8

简介 MyBatis-Plus 是一个基于 MyBatis 的增强工具。 它经常被简称为 MP。 它的核心定位是: 只做增强,不改变 MyBatis 原有能力。 普通 MyBatis 项目里,哪怕只是做一张表的增删改查,也经常要写: Mapper 接口 Mapper XML insert SQL delete SQL update SQL select SQL 分页 SQL 条件 SQL MyBatis-Plus 把这些单表常规操作封装成了通用方法。 最常见的写法是: public inte


基因泰克:检测级虚拟细胞基准!大语言模型+智能体
Omics Pro2026/6/1

摘要 机器学习与大规模生物数据的进展重新激发了构建虚拟细胞(预测细胞行为的计算模型,可加速生物学发现)的研究前景。该愿景的核心应用是体外表型筛选,即模型预测细胞扰动在未知生物场景下的效应,该任务融合异质文本输入与多样表型输出,高度适配大语言模型与智能体系统。但目前该任务缺乏标准化基准,现有研究仅聚焦分子层面读数,与真实药物研发流程中的表型终点脱节。本研究推出基于1,920个公开CRISPR筛选构建的表型筛选预测基准AssayBench,覆盖5大类细胞表型;将筛选预测任务定义为单筛选基因排序任务


Spring MVC 的核心知识点梳理
huohuopro2026/5/11

MVC 是什么 MVC 不是 Spring 发明的,而是一种设计模式,目的是“解耦”。 M(Model,模型):数据 + 业务逻辑。比如 Teacher 类,TeacherService。V(View,视图):展示数据的界面。比如 JSP、Thymeleaf 模板,或者是现代返回 JSON 的前端页面。C(Controller,控制器):接收用户请求,调用 Model,最后选择 View 来展示。 流程:用户点击一个链接 → Controller 拿到请求 → 调 Service 拿到数据(Mo


精准医学的数据平台化与Python编程实战(中)
Allen_Lyb2026/5/1

第五章:高性能数据处理与分析 5.1 使用Pandas进行临床数据清洗与特征工程 import pandas as pd import numpy as np from sklearn.impute import SimpleImputer from sklearn.preprocessing import StandardScaler, OneHotEncoder # 加载模拟临床数据 df = pd.read_csv('clinical_cohort.csv') # 处理缺失值 nu


每天刷十几个平台的热榜太累了?我用一个页面全部搞定
扑克中的黑桃A2026/4/22

这里写目录标题 前言1.什么是DailyHot?2.在飞牛OS上部署DailyHot3.docker部署DailyHot4.使用DailyHot5.安装cpolar内网穿透6.配置公网地址7.保留固定公网地址总结 前言 每天看热点,你要打开几个 App? 微博、知乎、Hacker News、V2EX、豆瓣、少数派……每个平台都有热榜,每个平台都得点进去刷一遍。等把这些都刷完,半个小时过去了,真正有用的信息反而被稀释了。 而且还有个更现实的问题——你在公司想看看知乎热榜发生了啥,


LangGraph 入门到精通0x02:基础 API (二)
chaors2026/4/14

前言 今天继续学习一些 LangGraph 常用到的基础 API。废话少说上干货。 .点语法 Graph 的创建可以使用看着更简洁的 .语法。 graph = ( StateGraph(State) .add_node(a) .add_node(b) .add_edge(START, "a") .add_edge("a", "b") .add_edge("b", END) .compile() ) 步骤序列 StateGraph(St


你的 Android App 可能白白损失了 35% 的性能——R8 全模式配置详解
陆业聪2026/4/6

字节跳动的工程师优化启动速度时,可能花了数周分析 trace、改代码;Monzo 的团队却只改了一行配置,性能指标全线提升了 35%。这不是段子,是 Google 官方 blog 2026 年 3 月底发出来的案例。 问题来了:你的项目,是不是也开着 R8,但根本没用对? R8 到底做了什么——大多数人理解是错的 很多人对 R8 的理解停留在「代码混淆 + 压缩」。打开 minifyEnabled true,觉得任务完成了。 但 R8 实际上分两种工作模式: • 兼容模式(Compatibili


核心概念层——深入理解 Agent 是什么
想打游戏的程序猿2026/3/28

1 Agent vs ChatBot:从根本上理解区别 1.1 一个直观的例子 假设你对 AI 说:"帮我分析一下我们公司上周的销售数据,找出表现最好的产品,并给团队发一封总结邮件"。 ChatBot 的反应: ChatBot: "要分析销售数据,你可以按以下步骤操作: 1. 打开数据库,执行 SQL 查询获取上周的销售记录 2. 使用 Excel 或 Python 进行数据汇总 3. 找出销售额最高的产品 4. 撰写邮件总结发送给团队 你需要我帮你写 SQL 查询语句吗?" → ChatB


配置钉钉龙虾OpenClaw机器人调用OpenMetadata
光于前裕于后2026/3/20

目录 一、前言1️⃣钉钉(DingTalk)2️⃣OpenClaw3️⃣OpenMetadata4️⃣MCP(Model Context Protocol) 二、安装OpenClaw三、配置OpenClaw钉钉机器人四、调用OpenMetadata MCP 一、前言 先介绍下这四个工具/协议的定位与核心能力,本文将从零开始配置。 1️⃣钉钉(DingTalk) 阿里巴巴旗下的企业协作平台,2014年上线,是中国市场份额最大的企业即时通讯与办公套件之一。 核心能

首页编辑器站点地图

本站内容在 CC BY-SA 4.0 协议下发布

Copyright © 2026 聚合阅读