ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

MongoDB集合索引查看详解:getIndexes与索引调优实战

MongoDB集合索引查看详解:getIndexes与索引调优实战 写这个系列教程到第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: zhangsanexample.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_-1MongoDB可能用status单字段索引先过滤再在内存里排序后果就是executionStats里出现SORT阶段并且totalKeysExamined明显大于totalDocsExamined对大数据集来说就是灾难。而有了刚才创建的复合索引explain()结果里会直接看到indexName: status_1_createdAt_-1SORT阶段消失。这不只是“看到”索引而是真正把索引用起来了。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()先把索引“家底”摸清再做其他操作。这习惯帮我在很多性能问题里少走了弯路。希望这篇文章里的方法和踩坑记录也能让大家的排查路径更直一些。
返回列表