☰
MongoDB集合索引查看详解:getIndexes与索引调优实战
2026/10/8 3:27:48 网站建设 项目流程

写这个系列教程到第32篇,后台私信里问得最多的反而是一个听起来很基础的问题:怎么查看一个集合里到底有哪些索引?很多朋友在定位慢查询的时候,第一反应是去看song语句、去看explain(),但往往忽略了一个前置动作——先把集合上的索引清单拉出来,看看自己手上这些索引建对了没有、建了几颗、有没有重复。其实在MongoDB里查看集合索引是一件非常直接的事,一条getIndexes()就搞定了,但里面涉及的字段含义、不同入口的差异、以及查看之后怎么解读,还是有不少细节值得掰开揉碎讲一讲。这篇文章我就从实际排查经验出发,把“如何查看集合中的索引”这件事讲透,无论你是刚上手MongoDB的新人,还是已经写过一段时间聚合查询的老手,看完应该都能完全掌握,并且能把这些方法直接用到自己的故障排查和性能调优里。

1. 为什么我们要去“看”集合的索引

1.1 索引是数据库的钱包,得经常对账

很多人对索引的态度是“建了就完事”,但索引从来不是一劳永逸的东西。你可以把集合里的每个索引想象成一张银行卡:它能让你在查询时快速取到钱(加速读),但每张卡都有年费、管理成本(占用磁盘和内存),每次往卡里存钱(写入、更新)也要额外做一次记账操作(维护索引结构)。如果一个集合上挂了七八个索引,写入性能一定会肉眼可见地下降。所以定期查看集合上有哪些索引,本质上是在“对账”——搞清楚你到底开了哪些卡、哪些卡有用、哪些卡该注销。

我在实际项目里见过最夸张的一次,是一个订单集合上叠了十几个单字段索引,其中userId、orderNo、status各自建了单独索引,还有三个不同顺序的复合索引。乍一看每个索引都有道理,但统统列出来之后才发现,很多索引的查询模式高度重叠,完全可以把两个单字段索引合并成一个复合索引。这种问题不到getIndexes()里把清单拉出来看,光靠代码走查很难发现,因为每个索引都是在不同版本、不同需求背景下由不同人加的。

1.2 排查慢查询的第一步不是explain,而是看索引清单

MongoDB慢查询日志(慢查询阈值由profileMatcher或slowms控制)中,如果看到COLLSCAN这个关键词,说明这条查询走了全集合扫描。但“全集合扫描”只是一个现象,根因可能是三种:集合上压根没有对应索引、有索引但索引选择器认为它不高效、或者查询条件写法让索引字段失效。要区分这三种情况,第一步就是把集合的索引清单拉出来,对照查询语句里用到的字段,看看到底有没有能命中的索引。

我之前排查过一个真实案例:一个日志集合的查询在某个时间段突然变慢,explain()结果显示COLLSCAN,但代码里明明建过createTime的索引。拉出getIndexes()一看,索引确实存在,但索引定义里的字段是create_time(下划线命名),而查询代码用的是createTime(驼峰命名)。这种字段名不一致的问题,光看代码很难一眼发现,但把索引清单和查询字段放在一起比对,问题立刻暴露。所以我的习惯是:查慢查询,先看索引清单,再看explain(),最后才是改代码。

2. MongoDB查看索引的三个常用入口

2.1 getIndexes():最核心、最标准的方法

查看集合索引最正统的方式就是通过MongoDB Shell执行db.collection.getIndexes()。这个方法在一个集合上调用,返回一个数组,数组里每个元素对应一个索引描述文档。即使是刚创建的集合,也会有一个默认的_id索引,除非你用手动方式去删掉了它(生产环境强烈不建议这么做)。所以正常情况下,getIndexes()至少会返回一条记录。

这个方法有几个特点值得注意。第一,它返回的是“索引定义”,即索引建在哪些字段上、顺序如何、名字是什么、有什么特殊属性,而不是索引数据本身。第二,它不返回索引大小、索引使用次数这类运行时指标,想看那些信息需要另找工具(后面会专门讲)。第三,它支持在事务里调用吗?严格说getIndexes()是管理命令,通常在事务外执行。日常开发和排查基本都在Shell里敲,不用纠结事务场景。

在Shell里执行的效果如下,我先建一个简单的用户集合,然后调用getIndexes():

use testdb db.users.drop() db.users.insertOne({ name: "张三", age: 30, email: "zhangsan@example.com" }) db.users.getIndexes()

返回结果是:

[ { "v": 2, "key": { "_id": 1 }, "name": "_id_", "ns": "testdb.users" } ]

这里就能看到一个最基本的_id索引。v: 2是索引版本号,key是索引键和排序方向,name是索引名称,ns是索引所属的命名空间,格式是“数据库名.集合名”。关于这几个字段的详细解读,我在第3章里会逐个展开。

2.2 通过系统集合system.indexes查询

除了getIndexes(),MongoDB每个数据库里还有一个隐藏的系统集合叫system.indexes,它记录了当前数据库下所有集合的索引信息。在较早的版本中,你可以直接通过db.system.indexes.find()来查看。这在当年是一个很实用的“土办法”,一条find()就能把库内所有索引一网打尽,不用挨个集合执行getIndexes()。

在较新的MongoDB版本(4.x之后)中,系统集合的可见性和访问方式做过调整,system.indexes的读取行为也有变化。不同版本下,你可能需要用db.getSiblingDB('testdb').system.indexes.find()这种方式来指定数据库。不过在绝大多数版本里,db.system.indexes.find()依然可以作为一个“总览”手段,前提是你的数据库用户具备对应的权限。

举个实际用法,如果你想看看某个库下面总共有哪些索引、分布在哪几个集合上,可以这样查:

// 切到目标数据库 use testdb // 列出当前库下所有集合的索引 db.system.indexes.find().pretty()

输出会包含所有集合的索引记录,每条记录里有v、key、name、ns等字段。当你管理的库很小、集合很少时,这个方法非常直观。但如果库里有上百个集合,输出会很长,这时候建议用getIndexes()逐集合查看,或者在find()后面加查询条件做过滤。

2.3 从驱动与客户端工具里查

生产环境中,我们不一定总有机会登录到服务器上敲Shell。更多时候是使用各类编程语言的MongoDB驱动,或者在图形化管理工具里操作。这时候查看索引的方式各不相同,但原理都建立在getIndexes()之上。

以最常用的Python驱动pymongo为例,查看一个集合的索引信息可以这样写:

from pymongo import MongoClient client = MongoClient("mongodb://localhost:27017") db = client["testdb"] users = db["users"] # 查看索引信息,返回字典,键是索引名,值是索引定义 index_info = users.index_information() print(index_info)

这里有一个和Shell不太一样的地方:index_information()返回的是一个字典,而不是数组,字典的键就是索引名,值是对应的索引定义文档。所以你要先看索引名,再根据索引名去找详细字段。如果你习惯Shell的数组结构,可以在脑海中把字典的items()想象成一个“索引名+定义”的二元组列表。

面对一批集合,可以用循环批量查看:

for coll_name in db.list_collection_names(): print(coll_name, db[coll_name].index_information())

如果是使用MongoDB Compass这类图形化工具,通常直接在集合的“Indexes”标签页里就能看到索引列表,界面上一行一个索引,会展示索引名称、索引键(字段和排序方向)、索引属性(唯一、稀疏、TTL等)。虽然界面展示和Shell返回的结构不太一样,但底层读的还是同一份索引元数据。图形化工具胜在直观,但论信息的完整性,还是getIndexes()返回的原始文档最可靠,所以我建议你在任何图形化工具里看到可疑索引时,都切到Shell用getIndexes()确认一遍,防止GUI做了字段裁剪或省略显示。

3. 索引结果每一列到底在说什么

3.1 key:索引键与排序方向

getIndexes()返回的文档中,最核心的字段就是key。它的值是一个嵌套文档,格式像{"字段名": 1}或{"字段名": 1, "字段名2": -1}。这里的1和-1代表索引键的排序方向:1表示升序,-1表示降序。注意,这个排序方向不是查询结果集的排序顺序,而是索引B+树内部存储键值的方向。

很多人一开始会混淆这一点,看到{"age": 1}就以为查询结果会自动按age升序排序。实际上索引方向影响的是:当查询同时需要排序时,MongoDB能否直接利用索引顺序来避免SORT阶段。比如你有一个{"age": -1}的索引,当查询使用sort({age: -1})时,MongoDB可以直接按索引顺序读取结果,无需额外的内存排序;如果查询使用sort({age: 1}),就可能需要在内存中把结果反转或者做排序操作,性能就会有损耗。

对于复合索引,key里的字段顺序也极其重要。比如{"status": 1, "createTime": -1}表示先按status升序,再按createTime降序。这意味该索引能支持“等值条件status+ 范围/排序条件createTime”的查询,但如果查询条件反过来(先过滤createTime再按status排序),这个索引很可能就用不上。查看索引清单时,我建议你拿着key里的字段顺序和业务查询一一比对,看是否对得上。

3.2 name:索引名称的命名规则

getIndexes()返回的每个索引文档都会有一个name字段。这个字段在创建索引时可以显式指定,也可以由MongoDB自动生成。自动生成规则很简单:把所有索引键按照“字段名_方向”的方式拼接,多个键之间用下划线连接。例如{"userId": 1, "createTime": -1},自动生成的索引名就是userId_1_createTime_-1。

默认命名规则有一个隐含问题:如果索引键字段名很长,或者复合索引的字段很多,自动生成的索引名会非常长。而MongoDB对索引名是有长度限制的,历史版本中索引名最长不能超过127字节,超过会报错。所以早期经常有人在创建索引时收到类似“索引名称太长”的错误提示,然后一脸懵地问我为什么。其实解决方式很简单:在createIndex时显式传入一个短的name选项。查看索引时,一个清晰、简短的索引名能让你在日志、监控、explain()结果里更容易认出这个索引到底服务哪个查询模式。

我在团队里习惯用一种规则命名索引:前缀idx_+ 字段名列表 + 关键属性缩写。例如idx_userId_createTime_desc这种。虽然自动命名也能用,但一旦索引数量多起来,自定义名称的辨识度优势是碾压级的。查看getIndexes()时,你可以先扫一眼name列表,一个好的命名让你一眼就能看出哪个索引是为哪类查询建的。

3.3 v、ns、partialIndexExpression等隐藏信息

除了key和name,每次查看索引时你还会看到v和ns,索引版本号v在绝大多数情况下是2。v: 1是早期MongoDB使用的索引格式,现在已经极少见。如果在某个集合上看到v: 1,建议你评估一下是否需要在维护窗口内重建索引以确保兼容性,不过这种事情在真实生产环境发生的概率已经很低,除非你还在跑远古版本。

ns含义很简单,就是“命名空间”(namespace),完整写法是“数据库名.集合名”。它告诉你这个索引锁定的目标集合。在join多数据库监控数据时,ns字段可以用来做分组排查,比如统计每个集合的索引数量。

如果索引创建时带了一些特殊选项,getIndexes()的返回结果里还会出现对应的附加字段。我列几个常见的:

附加字段含义典型场景
unique: true唯一索引,字段值不允许重复保证业务主键、手机号等唯一性
sparse: true稀疏索引,只对存在该字段的文档建索引字段可能缺失,且不希望缺失值占索引项
expireAfterSecondsTTL索引,数据自动过期删除日志、验证码、会话等临时数据
partialFilterExpression部分索引,只对满足表达式的文档建索引只索引状态为“待处理”的文档,缩小索引体积
weights文本索引的权重配置全文检索场景
language/default_language文本索引分词语言配置多语言全文检索

举个例子,创建一个部分索引后,getIndexes()会返回类似这样的文档:

db.articles.createIndex( { status: 1 }, { partialFilterExpression: { status: "published" } } ) db.articles.getIndexes()

返回的第三条索引记录大致是:

{ "v": 2, "key": { "status": 1 }, "name": "status_1", "partialFilterExpression": { "status": "published" }, "ns": "testdb.articles" }

partialFilterExpression直接嵌在索引定义里,你一看就知道这个索引不是给全表建索引,而是只给满足条件的文档建索引,所以体积会小很多,写入维护成本也低。这类“隐藏信息”如果不去查看,时间久了很容易被后来维护的人误当作一个普通索引,甚至可能被误删。定期查看并记录这些附加属性,是对数据库资产负责的表现。

4. 实操:手把手在Shell里查看集合索引

4.1 准备测试集合与数据

讲完原理,我们直接进Shell实操。我在本地用一个名为blog的数据库来做演示,先创建articles集合并插入几条模拟文章数据,这样后面创建索引和查看索引才有“实物”参考。

use blog // 清空集合,保证环境干净 db.articles.drop() // 插入一批测试文档 db.articles.insertMany([ { title: "MongoDB索引实战", author: "张三", createdAt: new Date("2024-01-05T10:00:00Z"), status: "published", views: 120 }, { title: "Redis缓存设计", author: "李四", createdAt: new Date("2024-02-10T12:30:00Z"), status: "draft", views: 30 }, { title: "消息队列选型", author: "王五", createdAt: new Date("2024-03-15T08:45:00Z"), status: "published", views: 89 } ])

插入完成后,先执行一次db.articles.getIndexes(),你会看到只有_id索引。这就是“零自定义索引”状态下的基线。任何集合一创建出来就带_id索引,因为这是MongoDB为了保证文档主键唯一性自动建的唯一索引,它不能被修改,除非删除后重新创建整个集合。

4.2 创建几种常见索引并查看

现在给articles集合创建几种有代表性的索引:单字段索引、复合索引、部分索引,以及一个文本索引(用于全文检索演示)。这样getIndexes()的输出会比较丰满。

// 单字段索引:根据作者查文章 db.articles.createIndex({ author: 1 }) // 复合索引:按状态筛选,再按创建时间倒序排列 db.articles.createIndex({ status: 1, createdAt: -1 }) // 部分索引:只索引已发布的文章,减少索引体积 db.articles.createIndex( { views: 1 }, { partialFilterExpression: { status: "published" } } ) // 文本索引:支持标题和作者的全文搜索 db.articles.createIndex( { title: "text", author: "text" } )

创建完成后,执行查看命令:

db.articles.getIndexes().forEach(function(idx) { print("索引名: " + idx.name); print(" 键: " + JSON.stringify(idx.key)); print(" 命名空间: " + idx.ns); if (idx.partialFilterExpression) { print(" 部分索引条件: " + JSON.stringify(idx.partialFilterExpression)); } print("---"); })

这段脚本会在Shell里逐行打印每个索引的关键信息,比直接返回一堆JSON更易读。实际输出看起来像这样:

索引名: _id_ 键: {"_id":1} 命名空间: blog.articles --- 索引名: author_1 键: {"author":1} 命名空间: blog.articles --- 索引名: status_1_createdAt_-1 键: {"status":1,"createdAt":-1} 命名空间: blog.articles --- 索引名: views_1 键: {"views":1} 命名空间: blog.articles 部分索引条件: {"status":"published"} --- 索引名: title_text_author_text 键: {"title":"text","author":"text"} 命名空间: blog.articles ---

从输出里能清楚看到每个索引的名称、键和额外属性。这里特别提醒一下:文本索引的key里会出现"text"字符串而不是数字,这是它的特征。以后看到key里有"text",就知道它是为全文检索服务的。

4.3 给索引起名字的实操细节

在上面创建复合索引时,MongoDB自动把索引名生成了status_1_createdAt_-1。这个名称虽然还能读,但一旦索引字段增加到三四个,默认名称就会变得长到让人崩溃。我在生产环境强烈建议在createIndex时显式指定name。

db.articles.createIndex( { status: 1, createdAt: -1 }, { name: "idx_status_createdAt_desc" } )

显式起名有几个好处:第一,getIndexes()结果更清晰,日志里看到idx_status_createdAt_desc就知道索引的用途;第二,避免了索引名过长导致创建失败的问题;第三,后续通过dropIndex("索引名")删除索引时,名字短好敲命令。

还有一个小细节:索引名在集合内是唯一标识。也就是说,同一个集合下不能有两个同名的索引。如果你在开发环境试过创建两个同名字但不同键的索引,会直接得到一个错误。所以规范命名显得更加重要,能有效避免你在不同环境里创建出一批“名不副实”的索引。

5. 常见问题与排查技巧实录

5.1 为什么我的getIndexes()返回空数组

有人会遇到getIndexes()执行后返回[],或者只看到_id索引。这里要区分两种情况。第一种,集合本身刚创建且没有自定义索引,那么返回数组里至少会有一条_id索引,不会是空数组。如果你的返回真的是空数组,说明这个集合是“无主键索引”的极端情况,通常是因为有人显式删除了_id索引并且集合是在特殊配置下创建的,生产环境极其罕见。

第二种更常见的情况是:你以为自己在某个集合上查看索引,实际却连错了库或者连错了集合。在Shell里,db.getCollectionNames()可以列出当前库下所有集合,先确认集合名拼写正确。我见过太多次因为大小写、单复数搞错导致看了半天空气的例子。另一个可能性是权限不足,当前数据库用户没有访问索引元数据的权限,这时getIndexes()会直接抛异常而不是返回空数组,报错信息类似“not authorized on xxx to execute command”。如果遇到权限报错,找管理员给账号授予dbAdmin角色即可。

5.2 索引名称重复了怎么办

创建索引时,如果指定的索引名在当前集合已存在,但索引键不同,MongoDB会拒绝创建。例如你先创建了{author: 1},索引名叫author_1,然后想创建{author: 1, views: 1},也指定名字为author_1,会得到一个IndexKeySpecsConflict之类的错误。解决方案很直接:先查看现有索引名,再起一个不冲突的名字。

如果你想先删掉旧索引再创建新索引,可以用:

// 先查看当前索引名 db.articles.getIndexes().forEach(idx => print(idx.name)) // 删除指定名称的索引 db.articles.dropIndex("author_1") // 重新创建 db.articles.createIndex( { author: 1, views: 1 }, { name: "idx_author_views" } )

dropIndex也接受一个对象作为参数,例如dropIndex({author: 1}),但用名字更精确,避免误删。索引删除是元数据操作,在生产环境要格外谨慎,先看清单再动手。

5.3 查看索引大小和占用空间的思路

getIndexes()只返回索引定义,不返回索引大小。想知道一个集合的索引到底占了多少磁盘空间,可以用stats()命令:

db.articles.stats()

返回文档里有两个跟索引相关的关键字段:indexSizes和totalIndexSize。indexSizes是一个子文档,里面以索引名为键、以字节数为值,挨个列出每个索引的存储大小;totalIndexSize则是所有索引大小的总和。我一般最先看totalIndexSize,如果它占整个集合适配存储的比例过高,说明索引可能建多了或者建宽了。

更进一步,我们还可以通过聚合管道操作符$indexStats查看每个索引的使用统计:

db.articles.aggregate([{ $indexStats: {} }]).forEach(function(stat) { print(stat.name + ": 访问次数=" + stat.accesses.ops + ", 命中率=" + stat.accesses.since); })

$indexStats会输出每个索引的访问次数(accesses.ops),以及从统计开始时间到现在的记录。这个功能对“找出从未被使用的索引”非常有帮助。如果一个索引创建了几周,accesses.ops还是0,那它就是典型的“僵尸索引”,占着磁盘空间、拖慢每次写入,却从来不服务任何查询。这种索引排查出来后,可以在业务低峰期先和团队确认,再执行dropIndex()清理。

5.4 从explain()反推索引是否真的被使用

查看索引的最终目的是让查询高效,所以在getIndexes()拿到“应然”列表之后,还需要用explain()验证“实然”。方法非常简单,在查询语句前加explain("executionStats")即可:

db.articles.find({ status: "published", createdAt: { $gt: new Date("2024-01-01") } }) .sort({ createdAt: -1 }) .explain("executionStats")

在返回的queryPlanner部分,winningPlan.inputStage如果显示IXSCAN表示走了索引扫描,indexName字段会直接告诉你命中了哪个索引;如果显示COLLSCAN则说明走了全表扫描。把explain()里的indexName和getIndexes()里的name对照,就能验证索引是否如预期生效。这一对照动作其实是在做“索引清单 vs 查询计划”的匹配,能帮你发现很多索引设计上的盲区。

比如上面的示例,如果没有复合索引status_1_createdAt_-1,MongoDB可能用status单字段索引先过滤,再在内存里排序,后果就是executionStats里出现SORT阶段,并且totalKeysExamined明显大于totalDocsExamined,对大数据集来说就是灾难。而有了刚才创建的复合索引,explain()结果里会直接看到indexName: "status_1_createdAt_-1",SORT阶段消失。这不只是“看到”索引,而是真正把索引用起来了。

6. 多环境下的索引查看建议

6.1 开发、测试、生产环境的差异

很多人在本地Shell里执行getIndexes()很熟练,一到生产环境就手足无措。生产环境的MongoDB通常有多个副本集节点,索引在哪个节点上建?这些索引在各节点之间如何同步?其实在副本集架构下,你连接任意一个从节点执行getIndexes(),看到的索引清单跟主节点是一致的,因为索引元数据会通过oplog同步到所有节点。所以生产环境查看索引,不需要挨个节点去连,连接其中一个可执行读的节点即可。

但要注意,如果你用的是mongos代理连接分片集群,情况会略有差异。分片集群中,每个分片上的集合索引是各自维护的。你在mongos上执行getIndexes(),它会帮你汇总各分片的索引信息,但如果你直接连接某个分片,只能看到该分片上的索引。所以生产环境先确认自己连的是哪个入口(mongos还是分片副本集),再决定怎么解读索引清单。

6.2 索引查看的脚本化与自动化

当你管理的实例一多,手动敲getIndexes()就太累了。我习惯写一小段脚本,批量收集所有集合的索引清单,输出成文件,方便定期审计。比如下面这段Python脚本,能用最短时间把指定库的索引一览无余:

from pymongo import MongoClient client = MongoClient("mongodb://localhost:27017/") db = client["blog"] for coll in db.list_collection_names(): idx_list = db[coll].index_information() print(f"=== 集合: {coll},索引数量: {len(idx_list)} ===") for name, definition in idx_list.items(): print(f" - {name}: {definition['key']}")

这类脚本可以作为巡检工具的一部分,每周定时跑一次,把索引清单存档,对比版本之间是否有索引变化,及时发现有人不小心在测试环境建了违规索引,或者生产环境多了几个“僵尸索引”。做数据库管理的人都知道,环境越复杂,越需要这种“自动化抄表”的手段去盯着元数据的变化。

查看索引这件事,看似只是MongoDB运维里一个不起眼的小操作,但它背后串着索引设计、慢查询分析、空间管理、环境差异等多个深层问题。我个人的习惯是:不管查什么问题,走到数据库面前第一件事永远是getIndexes(),先把索引“家底”摸清,再做其他操作。这习惯帮我在很多性能问题里少走了弯路。希望这篇文章里的方法和踩坑记录,也能让大家的排查路径更直一些。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询