MongoDB 4.x——索引介绍
MongoDB 4.x索引介绍1、索引简述1.1、索引是什么1.2、索引的分类2、单键、复合索引2.1、单字段索引2.2、复合索引3、数组索引4、地理空间索引5、唯一性约束5.1、复合索引的唯一性5.2、嵌套文档的唯一性5.3、数组的唯一性5.4、使用约束6、TTL索引6.1、可变的过期时间6.2、使用约束7、其他索引特性7.1、条件索引7.2、稀疏索引sparsetrue7.3、文本索引7.4、模糊索引8、使用explain命令验证优化1、索引简述1.1、索引是什么索引在数据库技术体系中占据了非常重要的位置其主要表现为一种目录式的数据结构用来实现快速的数据查询。通常在实现上索引是对数据库表集合中的某些字段进行抽取、排列之后形成的一种非常易于遍历读取的数据集合。目前绝大多数的数据库对于索引技术都有非常强大且稳定的支持。索引的作用非常类似于一本书的目录如图所示。通过目录中的关键词和页码阅读者可以快速找到自己感兴趣的书籍内容。索引也是如此其主要是通过缩短查询数据的路径来提升效率的这同时也是成本最低的一种性能优化手段。我们几乎很难想象数据库离开了索引会变成什么样子。1.2、索引的分类按照索引包含的字段数量可以分为单键索引和组合索引或复合索引。按照索引字段的类型可以分为主键索引和非主键索引。按照索引节点与物理记录的对应方式来分可以分为聚簇索引和非聚簇索引其中聚簇索引是指索引节点上直接包含了数据记录而后者则仅仅包含一个指向数据记录的指针。按照索引的特性不同又可以分为唯一索引、稀疏索引、文本索引、地理空间索引等。2、单键、复合索引在MongoDB中我们可以对集合中的某个字段或某几个字段创建索引以book实体为例代码如下{_id:ObjectId(6a5cbf88fcf45abb4d8df387),title:book-0,publishedDate:ISODate(2026-07-19T12:14:00.469Z),type:sociality,tags:[document,developer,popular],favCount:73,author:{name:zale,age:35}}2.1、单字段索引如果经常使用标题title这个字段进行搜索我们可以为它创建一个单字段的索引代码如下db.book.createIndex({title:1}){numIndexesBefore:1,numIndexesAfter:2,createdCollectionAutomatically:false,ok:1}这里的“title1”中的1表示索引采用的是升序排列。然而在单字段的索引中使用升序和降序并没有什么差别。当然我们也可以对内嵌的文档字段创建索引比如根据作者的名称进行索引代码如下db.book.createIndex({author.name:1})2.2、复合索引复合索引是多个字段组合而成的索引其性质和单字段索引类似。但不同的是复合索引中字段的顺序、字段的升降序对查询性能有直接的影响因此在设计复合索引时则需要考虑不同的查询场景。如果需要频繁地查询某分类下的book文档排名那么可以按照分类、收藏数量创建一个复合索引代码如下db.book.createIndex({type:1,favCount:1}){numIndexesBefore:2,numIndexesAfter:3,createdCollectionAutomatically:false,ok:1}3、数组索引数组索引也被称为多值索引multikey index当我们对数组型的字段创建索引时这个索引就是多值的。根据前面的文档模型可以按如下场景建立索引db.book.createIndex({tags:1}){numIndexesBefore:3,numIndexesAfter:4,createdCollectionAutomatically:false,ok:1}由于标签是一个数组字段因此这个索引自然就是多值索引。多值索引在使用上与普通索引并没有什么不同只是在索引键上会同时产生多个值比如下面的文档{_id:0,tags:[t1,t2,t3,t4,t5]}按标签建立索引后将会产生如下的索引结构tagst1-id0,tagst2-id0,tagst3-id0,tagst4-id0,tagst5-id0,数组索引必然会使索引的条目和体积发生膨胀。比如一个book文档中存在20个标签那么就会产生20个索引条目这些条目同时指向同一个文档。为了避免失控有必要在文档的设计上做出一些限制。复合的多值索引多值索引很容易与复合索引产生混淆复合索引是多个字段的组合而多值索引则仅仅是在一个字段上出现了多值multi key。而实质上多值索引也可以出现在复合字段上代码如下db.book.createIndex({type:1,tags:1}){numIndexesBefore:4,numIndexesAfter:5,createdCollectionAutomatically:false,ok:1}然而MongoDB并不支持一个复合索引中同时出现多个数组字段比如下面的定义是不被允许的db.book.createIndex({tags:1,versions:1})这里假设了versions也是一个数组字段。4、地理空间索引在移动互联网时代基于地理位置的检索LBS功能几乎是所有应用系统的标配。MongoDB为地理空间检索提供了非常方便的功能。地理空间索引2dsphere index就是专门用于实现位置检索的一种特殊索引。下面来看一个案例。在生活节奏逐渐加快的今天我们已经习惯了使用在线订餐的APP。在挑选外卖商家时除了餐食的口味、评价、优惠力度还有一个几乎所有人都要考虑的因素就是距离。因此在订餐APP上呈现的商家检索通常都会有地理位置的限制例如仅查询5千米内的商家。那么通过MongoDB如何实现“查询附近商家”这种功能呢首先假设商家的数据模型如下db.restaurant.insert({restaurantId:0,restaurantName:兰州牛肉面,location:{type:Point,coordinates:[-122.158574,37.449157]}})location字段是一个内嵌型文档用于表明商家的地理位置其中的type表示这是地图上的一个点coordinates则是经纬度。接着创建一个2dsphere索引代码如下db.restaurant.createIndex({location:2dsphere}){numIndexesBefore:1,numIndexesAfter:2,createdCollectionAutomatically:false,ok:1}最后执行查询实现检索附近5千米内的商家代码如下db.restaurants.find({location:{$near:{$geometry:{type:Point,coordinates:[-122.158574,37.449157]},$maxDistance:5000}}})这里使用了$near查询操作符用于实现附近商家的检索返回数据结果会按距离排序。其中$geometry操作符用于指定一个GeoJSON格式的地理空间对象typePoint表示地理坐标点coordinates则是用户当前所在的经纬度位置$maxDistance限定了最大距离单位是米。综上所述MongoDB实现地理空间检索需考虑的因素如下地理空间对象的存储如location地理位置。创建地理空间索引。使用合适的地理位置操作符实现检索。除此之外MongoDB还可以实现一些更为强大的地理空间计算比如区域的交集等。而地理空间对象也不局限于位置点location point具体类型由GeoJSON对象定义。注意MongoDB的地理空间检索基于WGS84坐标系在与一些地图平台集成时需要注意转换如GCJ-02火星坐标系、BD-09百度中国坐标系等。MongoDB 4.0版本之后near可以用于分片集合sharded collection而在此版本之前可以使用geoNear聚合操作来代替。5、唯一性约束在现实场景中唯一性是很常见的一种索引约束需求重复的数据记录会带来许多处理上的麻烦比如订单的编号、用户的登录名等。通过建立唯一性索引可以保证集合中文档的指定字段拥有唯一值。在创建索引时通过指定uniquetrue选项可以将其声明为唯一性索引代码如下db.book.createIndex({title:1},{unique:true})此后如果尝试写入两个拥有相同标题的book文档则将会得到如下错误提示对于指定字段已经存在重复记录的集合如果尝试创建唯一性约束的索引则会提示如下错误5.1、复合索引的唯一性除了单字段索引还可以为复合索引使用唯一性约束。如果只是希望分类下的书籍标题保持唯一性那么可以建立复合式的唯一性索引代码如下db.book.createIndex({type:1,title:1},{unique:true})5.2、嵌套文档的唯一性唯一性约束同样可以用于嵌套文档的某个字段这和普通索引没有什么区别比如db.book.createIndex({author.name:1},{unique:true})但如果希望将整个嵌套文档作为唯一性的保证那么在使用时可能会造成困扰比如db.book.createIndex({author:1},{unique:true})嵌套文档的唯一性约束是严格按照写入顺序进行比较的如下代码所示尽管写入的文档内容是一样的但由于字段的顺序不一致MongoDB仍然认为这是不同的文档。为了避免产生困扰建议尽量少用这种做法。5.3、数组的唯一性如果对数组索引multikey index使用唯一性约束那么可以保证所有的文档之间不会存在重叠的数组元素代码如下但是数组索引上的唯一性约束并无法保证同一个文档中包含重复的元素如下面的语句是可以写入成功的注意如果数组中的元素是嵌套的文档那么同样会遇到字段次序的问题。5.4、使用约束唯一性索引对于文档中缺失的字段会使用null值代替因此不允许存在多个文档缺失索引字段的情况。对于分片的集合唯一性约束必须匹配分片规则。换句话说为了保证全局的唯一性分片键必须作为唯一性索引的前缀字段。6、TTL索引在一般的应用系统中并非所有的数据都需要永久存储。例如一些系统事件、用户消息等这些数据随着时间的推移其重要程度逐渐降低。更重要的是存储这些大量的历史数据需要花费较高的成本因此项目中通常会对过期且不再使用的数据进行老化处理。通常的做法如下。方案一为每个数据记录一个时间戳应用侧开启一个定时器按时间戳定期删除过期的数据。方案二数据按日期进行分表同一天的数据归档到同一张表同样使用定时器删除过期的表。对于数据老化MongoDB提供了一种更加便捷的做法TTLTime To Live索引。TTL索引需要声明在一个日期类型的字段中假定写入的文档数据如下db.systemlog.insert({createDate:newDate(),type:alarm,message:...})执行下面的语句创建TTL索引db.systemlog.createIndex({createDate:1},{expireAfterSeconds:3600}){numIndexesBefore:1,numIndexesAfter:2,createdCollectionAutomatically:false,ok:1}这里为systemlog集合声明了一个TTL索引其指向createdDate字段其中expireAfterSeconds3600表示数据将在createdDate之后3600秒1小时后过期。对集合创建TTL索引之后MongoDB会在周期性运行的后台线程中对该集合进行检查及数据清理工作。除了数据老化功能TTL索引具有普通索引的功能同样可以用于加速数据的查询。6.1、可变的过期时间TTL索引在创建之后仍然可以对过期时间进行修改。这需要使用collMod命令对索引的定义进行变更代码如下db.runCommand({collMod:systemlog,index:{keyPattern:{createDate:1},expireAfterSeconds:7200}}){expireAfterSeconds_old:3600,expireAfterSeconds_new:7200,ok:1}另外一种情形可能也比较常见如每个文档可能拥有各自的存活时长即老化的策略不同。比如在systemlog集合中不同的事件类型的老化周期是不一样的对于此类场景的解决办法是可以使用一个单独的字段expiredDate用于表示每个文档的过期时间点那么TTL索引创建的规则为db.systemlog.createIndex({expireDate:1},{expireAfterSeconds:0}){numIndexesBefore:2,numIndexesAfter:3,createdCollectionAutomatically:false,ok:1}6.2、使用约束TTL索引的确可以减少开发的工作量而且通过数据库自动清理的方式会更加高效、可靠但是在使用TTL索引时需要注意以下的限制TTL索引只能支持单个字段并且必须是非_id字段。TTL索引不能用于固定集合。TTL索引无法保证及时的数据老化MongoDB会通过后台的TTL Monitor定时器来清理老化数据典型的间隔时间是1分钟。当然如果在数据库负载过高的情况下TTL的行为则会进一步受到影响。TTL索引对于数据的清理仅仅使用了remove命令这种方式并不是很高效。因此TTLMonitor在运行期间对系统CPU、磁盘都会造成一定的压力。相比之下按日期分表的方式操作会更加高效。7、其他索引特性7.1、条件索引条件partial索引允许你只对部分文档建立索引这是一种很特殊的用途。例如仅对业务上最常用于查询的数据集创建索引可以节省一些空间。可根据如下代码创建条件索引db.restaurants.createIndex({cuisine:1,name:1},{partialFilterExpression:{rating:{$gt:5}}})上述代码表示只对评分高于5分的餐馆信息进行索引。7.2、稀疏索引sparsetrue由于MongoDB非结构化的特性一个集合中允许结构不完全相同的两个文档共存。这意味着对某个索引字段来说可能某些文档中并不存在该字段但MongoDB索引会将不存在字段的情况等同于null值处理。稀疏索引则具备这样的特性只对存在字段的文档进行索引包括字段值为null的文档。例如db.test.insert({x:1})db.test.insert({x:2,z:null})这里写入两个文档第1个文档仅包含x字段而第2个文档包含x、z两个字段其中z值为null。对集合进行检索代码如下db.test.find({z:null}){_id:ObjectId(6a5cf8e9c0cb67eab82ec241),x:1}{_id:ObjectId(6a5cf8efc0cb67eab82ec242),x:2,z:null}使用建立的稀疏索引进行查询会发现只有包含z字段值为null的文档被返回了结果如下db.test.createIndex({z:1},{sparse:true}){numIndexesBefore:1,numIndexesAfter:2,createdCollectionAutomatically:false,ok:1}db.test.find({z:null}).hint({z:1}){_id:ObjectId(6a5cf8efc0cb67eab82ec242),x:2,z:null}7.3、文本索引MongoDB支持全文检索功能可通过建立文本索引来实现简易的分词检索。预置数据如下db.stories.insert({title:the monkeys hair,summary:the story about monkey})db.stories.insert({title:two stars,summary:long long hair in the sky})创建文本索引代码如下db.stories.createIndex({title:text,summary:text}){numIndexesBefore:1,numIndexesAfter:2,createdCollectionAutomatically:false,ok:1}这里为stories集合创建了一个文本索引该索引同时包含对title、summary字段的分词检索。使用$text操作符进行文本搜索代码如下db.stories.find({$text:{$search:monkey sky}}){_id:ObjectId(6a5cf9a9c0cb67eab82ec243),title:the monkeys hair,summary:the story about monkey}{_id:ObjectId(6a5cf9afc0cb67eab82ec244),title:two stars,summary:long long hair in the sky}$text操作符会将$search文本输入进行分词再检索如上述代码会检索出含有monkey或sky关键词的文档。MongoDB的文本索引功能存在诸多限制而官方并未提供中文分词的功能这使得该功能的应用场景十分受限。7.4、模糊索引MongoDB的文档模式是动态变化的而模糊索引wildcard index可以建立在一些不可预知的字段上以此实现查询的加速。需要注意的是该功能是MongoDB 4.2版本才推出的新特性在此之前的版本并不支持。通过模糊索引商品属性的检索变得更加容易了例如db.goods.insert({name:wallet,attributes:{color:red,price:130}})db.goods.insert({name:chair,attributes:{height:120,price:185}})其中attributes作为嵌套文档存放了商品的多个属性而不同商品所具有的属性很可能是不一样的。接下来创建一个模糊索引代码如下db.goods.createIndex({attributes.$**:1}){numIndexesBefore:1,numIndexesAfter:2,createdCollectionAutomatically:false,ok:1}attributes.$**表示该索引将匹配以attributes字段作为开始路径的任何一个字段。db.goods.find({attributes.color:red}){_id:ObjectId(6a5cfb35c0cb67eab82ec245),name:wallet,attributes:{color:red,price:130}}db.goods.find({attributes.height:{$gt:100}}){_id:ObjectId(6a5cfb3ac0cb67eab82ec246),name:chair,attributes:{height:120,price:185}}db.goods.find({attributes.price:{$lt:120}})8、使用explain命令验证优化前面说过建立索引的目的是用来加速数据查询的。那么如何判断索引是否合理或者说怎样评估索引所起到的作用呢是否存在这么一个工具可以提前告知我们一些预期的效果呢答案是肯定的。MongoDB提供了explain命令它可以帮助我们评估指定查询模型query model的计划。通常我们需要关心的问题如下查询是否使用了索引。索引是否减少了扫描的记录数量。是否存在低效的内存排序。…接下来继续使用之前创建的book集合在没有任何索引的情况下尝试评估查询计划代码如下返回的信息量有点多但务必记住一点现在的集合没有创建任何索引除了自动创建的_id索引所以查询计划显得有些糟糕winningPlan表示获胜的计划即数据库经过一系列评估后选择的最优计划stageCOLLSCAN则说明这是一个全表扫描。executionStats描述了执行的过程信息其中nReturned1是指返回了一条结果而totalDocsExamined50说明整个过程扫描了50条记录。尽管集合中的数据量并不大但至少可以看出在没有索引的情况下查询会多么低效。为了返回1个文档需要耗费50次的扫描假设集合有1000万个文档那么就需要扫描5亿次为了优化这个查询我们为标题建立一个升序索引代码如下继续评估查询计划代码如下这次的结果明显有些不同相比第一次查询计划其优化如下不再使用全表扫描COLLSCAN方式取而代之的是索引扫描IXSCAN。totalDocsExamined1意味着扫描的文档数量只有1个。同时由于使用了索引扫描因此可以看到totalKeysExamined1。executionStats.executionTimeMillis这个属性描述了执行过程所消耗的时间一般需要配合一定的数据规模才能看出差异。本例所提供的数据量太小因此表现出来的执行时间并没有什么不同。但通过explain结果中的查询计划、命中比率返回数/扫描数却能明显地看到索引对于查询效率带来的提升。