1.概述

索引可以理解为一个利于遍历搜索,独立于存储数据的一种数据结构,通常通过数据中的某一列来进行映射。索引的最终目的就是为了提高查询效率,是一种以空间换时间的解决思路。
在mysql中,索引的数据结构为B+树,而在mongo中,索引的数据结构为B-树

2.B-树

B-树又称为B树。与B+树不同,B树每个节点中都存储数据,B树的数据结构如下图
image.png
B树具有以下特点

  1. 一个节点可以存储N个数据,那么节点具有N+1个子树
  2. 每层数据都是从左至右逐步递增
  3. 该节点的左子树,数据都小于该节点,右子树都大于该节点,中间子树介于节点值之间
  4. B树中每个节点即存储索引也存储数据

那么为什么Mongo选择B树作为索引,而不是B+树?
B+树因为只有叶子节点存储数据,因此当需要查询的数据处于较深的层级时,需要多次的磁盘IO。但是因为B+树的叶子节点之间通过单向指针链接成了一个链表,因此B+树对于范围查询的支持更好,并且相对来说更加节省存储空间。
B树相比较B+树来说,对于范围查询效率不如B+树,但是对于等值或者说非跨节点的范围查询,因为索引和数据存在一起,因此磁盘IO的次数要少很多,效率要优于B+树。在极端情况下,时间复杂度=O1
再说回数据库层面,mongo本身是一个非关系型数据库,通常来说如果使用嵌入式数据模型存储,那么相对来说单次查询的比率要多于范围查询,因此选择B树作为索引的数据结构。而Mysql作为一个关系型数据库,范围查询的比率相对来说要多于单次查询,因此选择B+树作为索引的数据结构

3.索引

MongoDB 中索引的类型大致包含单键索引、复合索引、多键值索引、地理索引、全文索引、 散列索引等,下面简单介绍各类索引的用法。

查看索引

  1. 查看一个集合中的全部索引:db.${collectionName}.getIndexes()
  2. 查看一个集合中的索引大小:db.${collectionName}.totalIndexSize()。如果索引使用了前缀压缩,那么得到的是压缩后的大小

    删除索引

    db.${collectionName}.dropIndex(${IndexName})

    单键索引

    概念

    与mysql类似,就是在单一的数据列上建立索引,在mongo中可以理解为在单一键值上创建索引。在默认情况下,所有集合在 _id 字段上都有一个索引

    注意点

    在创建单建索引时可以指定升序或降序。但是对于单字段索引和排序操作,索引键的排序顺序(即升序或降序)无关紧要,因此在使用sort()进行排序时,对于单列索引,无论正序还是倒序都会使用索引

    语句

    db.collection.createlndex ( { key: 1 } ) //1 为升序,-1 为降序

    验证

    如图:sid单建索引创建成功
    image.png

    复合索引

    概念

    与mysql一致,mongo同样支持在多个键值上创建索引,并且mongo中的复合索引同样遵循最左原则。并且复合索引在使用过程中,不同于单列索引,索引的顺序也会影响到具体索引的使用。

    注意点

    复合索引最多只能包含31个字段
    索引的顺序主要的影响出现在使用sort()方法时,复合索引的排序使用遵循以下的示例顺序
    假设当前在键值 k1,k2上创建了符合索引,其中k1在前,k2在后.。两个键值都升序排列。那么索引的数据结构应该是这样的。只有k1有序时,k2才有序
    image.png
    因此在使用sort()时,具有以下观点

  3. k1在前,k2在前,且都为升序的情况下,索引生效

  4. k1在前,k2在前,且都为降序的情况下,索引生效
  5. k1在前,k2在前,顺序相反的情况下,索引不生效
  6. k2在前,k1在后,无论什么顺序,索引不生效

    语句

    db.collection.createlndex ( { key1: 1,key2:1,…… } ) //1 为升序,-1 为降序

    验证

    image.png

    多键值索引

    概念

    在mongo中,因为文档的数据持久化形式为BSON,因此存在值数据类型为数组的键,在这些键上建立的索引,成为多键值索引

    注意点

    需要注意的是,如果一个复合索引中包含多个数组键,那么索引会创建失败,如下图:
    image.png
    且经过测试,多键值索引只对当前层级数组有效,对递归层级无效

    语句

    db.collection.createlndex ( { key: 1 } ) //1 为升序,-1 为降序

    验证

    image.png

    地理位置索引(2d)

    概念

    mongo中,地址位置索引可以直接用于位置距离计算和查询。查询结果默认将会由近到远排序,且查询结果也包含目标点对象、距离目标点的距离等信息。
    通常用于获取一个地址指定距离内的目标地址,或者计算点对点之间的距离
    使用2d index 能够将数据作为2维平面上的点存储起来, 在MongoDB 2.2以前 推荐使用2d index索引。

    注意点

    建立2d索引的key格式必须为:[经度,纬度]

    语句

    create.${collectionName}.createIndexes(${fieldName}:”2d”)

    验证

  7. 存储测试数据

    1. db.dc2.insert({name:"北京",loc:[39.26,115.25]})
    2. db.dc2.insert({name:"大同",loc:[112.34,39.03]})
    3. db.dc2.insert({name:"太原",loc:[111.3,37.27]})
    4. db.dc2.insert({name:"湖南",loc:[24.38,108.47]})
  8. 建立2d地理位置索引

db.dc2.createIndex({loc:”2d”})

  1. 给定经纬度范围查询

db.dc2.find({loc:{$near:[115.24,29.26]}})
如图,检索出了北京以及湖南两个坐标点
image.png
同时我们可以使用$maxDistance限制最大距离进行查询。$maxDistance单位为度半径
db.dc2.find({loc:{$near:[39.26,115,24],$maxDistance:1}}) :::info 当索引形式为2d时,$maxDistance的单位为1度,1度约等于111km
当索引形式为2dsphere,$maxDistance的单位为1m ::: 如图我们指定了查询最远距离为1弧度,最终只得到了一个坐标点
image.png

地理位置索引(2dSphere)

概念

mongo除了支持2d平面的地理索引,同样支持立体球面的地理索引。
2dsphere index 支持球体的查询和计算,同时它支持数据存储为GeoJSON 和传统坐标。3.0版本以上推荐使用此索引

注意点

建立2dsphere索引的键值对存储格式存在两种:[经度,纬度]或GeoJson格式
GeoJson格式说明点击这里
一般来说我们需要存储用户坐标点的信息,坐标点的信息的格式为

  1. {
  2. "type":"Point",
  3. "coordinates":[
  4. 100,
  5. 0
  6. ]
  7. }

其中,GeoJson使用的是wgs84的坐标规范。
推荐一个好玩的项目:点击这里(可以将wgs84坐标转换为百度、高德等坐标规范)

语句

create.${collectionName}.createIndexes(${fieldName}:”2dsphere”)

常规查询

使用$nearSphere进行查询
查询条件示例

  1. {
  2. "location":{
  3. "$nearSphere":{
  4. "$geometry":{
  5. "type":"Point",
  6. "coordinates":[
  7. 118.783799,
  8. 31.979234
  9. ]
  10. }
  11. }
  12. }
  13. }

同样,我们也可以指定最远距离进行查询

  1. {
  2. "location":{
  3. "$nearSphere":{
  4. "$geometry":{
  5. "type":"Point",
  6. "coordinates":[
  7. 118.783799,
  8. 31.979234
  9. ]
  10. },
  11. "$maxDistance":3000
  12. }
  13. }
  14. }

聚合查询

按照离我最近排序,除了使用 $nearSphere 查询外,我们还可以使用 aggregate 来实现。
使用 aggregate 有两个好处。

  1. 我们在进行排序的后,可以返回两点之间的距离。
  2. 我们可以进行更为复杂的排序,例如我们可以先根据某个字段进行排序,然后该字段相同的情况下再根据距离排序。

使用 aggregate 查询时,我们还可以返回两点之间的距离,其中 distanceField 可以对距离字段进行重命名。
查询条件示例:具体$geoNear可选参数参考这里

  1. {
  2. "$geoNear":{
  3. "near":{
  4. "type":"Point",
  5. "coordinates":[
  6. 118.783799,
  7. 31.979234
  8. ]
  9. },
  10. "distanceField":"distance",
  11. "spherical":true,
  12. "maxDistance":15000,
  13. "query":{
  14. "address":{
  15. "$regex":"安"
  16. }
  17. }
  18. }
  19. }

验证

  1. 存储测试数据

    1. db.test.insert({"address": "南京 禄口国际机场", "location": {"type": "Point", "coordinates": [118.783799, 31.979234]}})
    2. db.test.insert({"address": "南京 浦口公园", "location": {"type": "Point", "coordinates": [118.639523, 32.070078]}})
    3. db.test.insert({"address": "南京 火车站", "location": {"type": "Point", "coordinates": [118.803032, 32.09248]}})
    4. db.test.insert({"address": "南京 新街口", "location": {"type": "Point", "coordinates": [118.790611, 32.047616]}})
    5. db.test.insert({"address": "南京 张府园", "location": {"type": "Point", "coordinates": [118.790427, 32.03722]}})
    6. db.test.insert({"address": "南京 三山街", "location": {"type": "Point", "coordinates": [118.788135, 32.029064]}})
    7. db.test.insert({"address": "南京 中华门", "location": {"type": "Point", "coordinates": [118.781161, 32.013023]}})
    8. db.test.insert({"address": "南京 安德门", "location": {"type": "Point", "coordinates": [118.768964, 31.99646]}})
  2. 创建2dsphere索引

    db.test.createIndex({“location”:”2dsphere”})

  3. near方法测试

    1. {
    2. "$geoNear":{
    3. "near":{
    4. "type":"Point",
    5. "coordinates":[
    6. 118.783799,
    7. 31.979234
    8. ]
    9. },
    10. "distanceField":"distance",
    11. "spherical":true
    12. }
    13. }

    如图:使用聚合查询可以看到点对点之间的距离信息,单位为米
    image.png

  4. 点距离方法测试

    1. {
    2. "$geoNear":{
    3. "near":{
    4. "type":"Point",
    5. "coordinates":[
    6. 118.783799,
    7. 31.979234
    8. ]
    9. },
    10. "distanceField":"distance",
    11. "spherical":true,
    12. "query":{
    13. "address":"南京 安德门"
    14. }
    15. }
    16. }
    1. {
    2. "$geoNear":{
    3. "near":{
    4. "type":"Point",
    5. "coordinates":[
    6. 118.783799,
    7. 31.979234
    8. ]
    9. },
    10. "distanceField":"distance",
    11. "spherical":true,
    12. "query":{
    13. "location.coordinates":[
    14. 118.790611,
    15. 32.047616
    16. ]
    17. }
    18. }
    19. }

    如图,可以通过$geoNear的query项进行数据的筛选,从而准确获取点对点之间的距离
    image.png

    地理位置索管道符详解

    $geoIntersects

  5. 定义:指出与查询位置相交的文档。

  6. 支持的索引:2dsphere
  7. 参数:$geometry (仅支持 2dsphere 索引,指定GeoJSON格式的几何图形)

    $geoWithin

  8. 定义:指出完全包含在某个区域的文档。

  9. 支持的索引:2dsphere、2d
  10. 参数:
  • $box(仅支持 2d 索引,查询出矩形范围内的所有文档)
  • $center(仅支持 2d 索引,查询出圆形范围内的所有文档)
  • $polygon (仅支持 2d 索引,查询出多边形范围内的所有文档)
  • $centerSphere(支持 2d 索引和 2dsphere 索引,查询出球面圆形范围内的所有文档)
  • $geometry (仅支持 2dsphere 索引,指定GeoJSON格式的几何图形)

    $near

  1. 定义:指出与查询位置从最近到最远的文档。
  2. 支持的索引:2dsphere、2d
  3. 参数:
  • $maxDistance (支持 2dsphere 索引和 2d 索引,指定查询结果的最大距离)
  • $minDistance (仅支持 2dsphere 索引,指定查询结果的最小距离。在4.0后支持 2d 索引)
  • $geometry (仅支持 2dsphere 索引,指定GeoJSON格式的点)
  1. 备注:$minDistance 官方文档说仅支持 2dsphere 索引,但是实践证明 $minDistance 也支持 2d 索引,这里保留争议。

    $nearSphere

  2. 定义:使用球面几何计算近球面的距离,指出与查询位置从最近到最远的文档。

  3. 支持的索引:2dsphere、2d
  4. 参数:
  • $maxDistance (支持 2dsphere 索引和 2d 索引,指定查询结果的最大距离)
  • $minDistance (仅支持 2dsphere 索引,指定查询结果的最小距离。在4.0后支持 2d 索引)
  • $geometry (仅支持 2dsphere 索引,指定GeoJSON格式的点)

    全文索引

    概念

    从 2.4 版本开始,MongoDB 开始支持全文检索功能,全文检索就是对文本中的每个词建立索引,指明该词在文章中出现的次数和位置,当用户查询时,检索程序就根据事先建立的索引进行查找,并将查找的结果反馈给用户,整个过程类似于通过字典中的检索字表查字的过程。
    目前,MongoDB 支持大约 15 种语言的全文索引,例如 danish、dutch、english、finnish、french、german、hungarian、italian、norwegian、portuguese、romanian、russian、spanish、swedish、turkish 等。

    原理

    通过上述支持语言可以看出,目前mongo全文索引本身并不支持中文查询。其本质是因为mongo的分词机制是依赖于空格进行分词,进而通过倒排索引机制实现的。
    倒排索引的原理如下:
    image.png
    因此对于中文来说,只能通过一些分词原则,将理论上的按照单个文字分词修改为按照空格分词
    目前有以下中文分词方法可进行操作
  1. 一元分词法:顾名思义将语句中的每个字都作为一个单词,例如:我喜欢”张钰玲”就会被拆分为”我 喜 欢 张 钰 玲”。这种分词方法优点就是简单,但是缺点也很显而易见,一个汉字对应的文档可能会很多,进而导致当高频汉字对应的文档数量过多时,计算交集的效率过低问题
  2. 二元分词法:很好理解就是每两个字进行一次分词,例如:”我喜欢张钰玲”就会被拆分为”我喜 喜欢 欢张 张钰 钰玲”。二元分词的情况下,每个单次对应的文档数量会相对应的减少,进而导致计算交集的效率提高。因此二元分词法的查询效率是要高于一元分词法的
  3. 结巴中文分词:结巴中文分词是最流行的Python中文分词组件,它有一种搜索引擎模式,在精确模式的基础上,对长词再次切分,提高召回率,适合用于搜索引擎分词。下面是引用自它项目主页的示例:image.png

综上所述,mongo全文索引本身只支持空格形式的分词查询,因此可以通过各种分词算法,将中文语句分割成符合mongo分词模式的查询机制,进行查询。其实对于分词查询,使用ES是更好的选择

注意点

  1. 一个集合中,只能存在一个全文索引
  2. 可以建立组合键全文索引,但是一个集合中,文本索引只能有一个

    语句

  3. 建立索引:db.${collectionName}.createIndex({${fieldName:”text”}})

  4. 查询:db.${collectionName}.find({$text:{$search:”${fieldName}”}}})
  5. 聚合查询:db.test.aggregate({$match:{$text:{$search:”张”}}})

    验证

  6. 存储测试数据

    1. db.test.insert({"text":"我 叫 冯 铁 城"})
    2. db.test.insert({"text":"我 叫 张 钰 玲"})
  7. 建立全文索引

db.test.createIndex({text:”text”})

  1. 查询

image.png
image.png

散列索引

概念

哈希索引使用索引字段值的哈希来维护索引条目。
哈希索引可以用作哈希分片键来对数据进行分片基于哈希的分片将字段的哈希索引用作分片键,以跨分片群集对数据进行分区。
使用哈希分片键对集合进行分片使数据分布更随机
通常来讲,我们可以在频繁进行等值查询的列上建立hash索引。因为hash索引的形式是天然支持等值查询并且效率也很快

注意

  1. MongoDB哈希索引在哈希之前将浮点数截断为64位整数。例如,哈希索引将为具有2.3、2.2和2.9的值的字段存储相同的值。为防止冲突,请勿对不能准确地转换为64位整数的浮点数使用哈希索引。
  2. MongoDB哈希索引不支持大于2^53的浮点值
  3. MongoDB 4.2确保PowerPC上浮点值2^63的哈希值与其他平台一致。
  4. MongoDB不支持对数组列建立hash索引
  5. 不能创建具有哈希索引字段的复合索引,也不能在哈希索引上指定唯一约束(unique:true)

    语句

    create.${collectionName}.createIndex(${fieldName}:”hashed”)

    验证

    插入数据,建立索引
    image.png
    查询数据
    image.png

    4.索引额外属性

    唯一索引

    唯一索引的功能和mysql中一样。可以理解为我们在某个单列索引或者复合索引上建立了唯一性,进而实现了集合中文档的唯一性。
    具体语句:db.${collectionName}.createIndex({${fieldName}:1/-1},{unique:true})
    具体验证过程如图
    image.png

    稀疏索引

    存在以下场景

  6. 集合中存入一个文档,文档中有且只有一个t字段

  7. 在t字段建立单建索引,并设置了索引唯一性
  8. 此时存入一个文档,文档中且只有一个m字段
  9. mongo会默认为该文档创建t字段,并赋值为空
  10. 此时存入一个文档,文档中且只有一个m字段
  11. 那么,如果mongo会默认为该文档创建t字段,并赋值为空的话,就会出现t字段重复,重复的字段值为null

image.png
此时就需要引入稀疏索引的概念了
索引的稀疏属性可确保索引仅包含具有索引字段的文档的条目,索引会跳过没有索引字段的文档
可以将稀疏索引与唯一索引结合使用,以防止插入索引字段值重复的文档,并跳过索引缺少索引字段的文档。
具体语句:db.${collectionName}.createIndex({${fieldName}:1/-1},{unique:true,sparse:true})
具体验证过程如图:可以看到同时稀疏性不会报错了
image.png

部分索引

在一个集合中,可能只有一部分的文档,适用于建立索引的场景,此时就可以通过部分索引来指定数据子集建立索引
部分索引具有以下特点:

  • 部分索引仅对满足指定过滤器表达式的文档进行索引。通过在一个集合中为文档的一个子集建立索引。
  • 部分索引具有更低的存储需求和更低的索引创建和维护的性能成本。3.2新版功能。
  • 部分索引提供了稀疏索引功能的超集,应该优先于稀疏索引。

具体语句:db.${collectionName}.createIndex({${fieldName}:1/-1},{partialFilterExpression:{具体查询条件})
验证步骤

  1. 首先存入数据

image.png

  1. 然后创建一个单建索引,并设置其部分性(只有t>2的文档,才建立索引)

image.png

  1. 对符合条件的数据进行查询,如图,用到了索引

image.png

  1. 对不符合条件的数据进行查询,如图,没用到索引

image.png

TTL索引

针对日期字段,或者包含了日期元素的数组字段,可以使用设定了生存时间的索引,来自动删除字段值超过生存时间的文档。
具体语句:db.${collectionName}.createIndex({${fieldName}:1/-1},{expireAfterSeconds:秒数})
验证如图:在createDate字段上建立了ttl索引,过期时间为10s。10s后数据被删除
image.png

5.索引在磁盘上的存储形式

如图,在wiredTiger引擎,索引在磁盘上的存储形式为.wt文件
image.png

6.覆盖索引

与mysql一样。mongo中同样存在覆盖索引的概念。
如果查询的投影,直接命中了索引列,那么就无须再去文档中查询对应的字段,这一过程成为覆盖索引。覆盖索引的查询效率明显优于回表查询的
同时如果索引字段是一个数组或对象(子文档),那么也不会触发覆盖索引操作
可以通过explain(“executionStat”)返回结果中的必须扫描文档数,来查看是否覆盖索引,覆盖索引时必须扫描文档数=0

7.索引的注意事项

额外开销

每个索引都会占用一些空间,并且在每次执行插入、更新和删除等操作时也需要对索引进行操作,导致额外的开销。因此,如果很少将某个集合用于读取操作,最好不要在集合中使用索引。

RAM 使用

由于索引存储在 RAM(内存)中,因此应确保索引的总大小不超过 RAM 的限制。如果总大小大于 RAM 的大小,那么 MongoDB 将删除一些索引,这就会导致性能下降。

查询限制

在以下的查询中,不能使用索引:

  • 正则表达式或否定运算符,例如 $nin、$not 等;
  • 算术运算符,例如 $mod 等;
  • $where 子句。
    因此,建议经常使用 explain() 来检查查询时索引的使用情况。

    索引键限制

    从 2.6 版本开始,如果现有索引字段的值超过索引键的限制,那么 MongoDB 将不会创建索引。

    插入超过索引键限制的文档

    如果文档索引字段的值超过了索引键的限制,那么 MongoDB 不会将任何文档插入到集合中。mongorestore 和 mongoimport 实用程序也是如此。

    索引建限制

    在定义索引时有以下几点需要注意:

  • 集合的索引不能超过 64 个;

  • 索引名称的长度不能超过 128 个字符;
  • 复合索引最多可以拥有 31 个字段。