MongoDB索引简述

索引是一种用来快速查询数据的数据结构。

如果没有索引,MongoDB在读取数据时必须扫描集合中的每个文件并选取那些符合查询条件的记录。 这种扫描全集合的查询效率是非常低的,特别在处理大量的数据时,查询可以要花费几十秒甚至几分钟,这对系统的性能是非常致命的。索引是特殊的数据结构,索引存储在一个易于遍历读取的数据集合中,索引是对数据库表中一列或多列的值进行排序的一种结构;

MongoDB 采用 B+Tree 做索引,索引创建在 colletions 上。

在这里插入图片描述

索引操作

索引创建

db.collection.createIndex(keys, options)
  • Key 值为你要创建的索引字段,1 按升序创建索引, -1 按降序创建索引

  • 可选参数列表如下:

Parameter

Type

Description

background

Boolean

建索引过程会阻塞其它数据库操作,background可指定以后台方式创建索引,即增加 “background” 可选参数。 “background” 默认值为false。

unique

Boolean

建立的索引是否唯一。指定为true创建唯一索引。默认值为false

name

String

索引的名称。如果未指定,MongoDB的通过连接索引的字段名和排序顺序生成一个索引名称。

dropDups

Boolean

3.0+版本已废弃。在建立唯一索引时是否删除重复记录,指定 true 创建唯一索引。默认值为 false.

sparse

Boolean

对文档中不存在的字段数据不启用索引;这个参数需要特别注意,如果设置为true的话,在索引字段中不会查询出不包含对应字段的文档.。默认值为 false.

expireAfterSeconds

Integer

指定一个以秒为单位的数值,完成 TTL设定,设定集合的生存时间。

v

Index Version

索引的版本号。默认的索引版本取决于mongod创建索引时运行的版本。

weights

Document

索引权重值,数值在 1 到 99,999 之间,表示该索引相对于其他索引字段的得分权重。

default_language

String

对于文本索引,该参数决定了停用词及词干和词器的规则的列表。 默认为英语

language_override

String

对于文本索引,该参数指定了包含在文档中的字段名,语言覆盖默认的language,默认值为 language.

# 创建索引, 默认索引名为title_1
db.book.createIndex({title:1})
# 创建唯一索引
db.book.createIndex({title:1},{unique:true})
# 创建复合索引
db.products.createIndex(
    { title: 1, favCount: -1 } ,
    { name: "title_favCount" }
)

查看索引

// 查看索引信息
db.collection.getIndexes()
// 查看索引键
db.collection.getIndexKeys()
// 查看索引占用空间。is_detail为0则只显示所有索引的总大小,为1显示每个索引的大小及总大小
db.collection.totalIndexSize(is_detail)

删除索引

// 删除集合指定索引
db.collection.dropIndex("索引名称")
// 删除集合所有索引
db.collection.dropIndexes()

索引类型

单键索引 (Single Field Indexes)

//在某一个特定的字段上建立了唯一的单键索引
db.book2.createIndex({title:1})

复合索引 (Compound Index)

//复合索引是多个字段组合而成的索引
db.book2.createIndex({type:1,favCount:1})

多键索引 (Multikey Index)

准备 inventory 集合:

db.inventory.insertMany([
    { _id: 1, type: "food", item: "aaa", ratings: [ 5, 8, 9 ] },
    { _id: 2, type: "food", item: "bbb", ratings: [ 5, 9 ] },
    { _id: 3, type: "food", item: "ccc", ratings: [ 9, 5, 8 ] },
    { _id: 4, type: "food", item: "ddd", ratings: [ 9, 5 ] },
    { _id: 5, type: "food", item: "eee", ratings: [ 5, 9, 5 ] }
])

创建多键索引:

//在数组的属性上建立索引。针对该数组的任意值都会定位到该文档,既多个索引入口或者键值
db.inventory.createIndex( { ratings: 1 } )

查询:

> db.inventory.find({ratings:{$in:[8]}})
{ "_id" : 1, "type" : "food", "item" : "aaa", "ratings" : [ 5, 8, 9 ] }
{ "_id" : 3, "type" : "food", "item" : "ccc", "ratings" : [ 9, 5, 8 ] }

创建复合多键索引:

db.inventory.createIndex( { item:1,ratings: 1} )

地理空间索引 (Geospatial Index)

假设商家的数据模型如下:

db.restaurant.insert({
    restaurantId: 0,
    restaurantName:"兰州牛肉面",
    location : {
        type: "Point",
        coordinates: [ -73.97, 40.77 ]
    }
})

创建一个 2dsphere 索引:

db.restaurant.createIndex({location : "2dsphere"})

查询附近10000米商家信息:

db.restaurant.find( {
    location:{
        $near :{
            $geometry :{
                type : "Point" ,
                coordinates : [ -73.88, 40.78 ]
            } ,
            $maxDistance:10000
        }
    }
} )
$near 查询操作符,用于实现附近商家的检索,返回数据结果会按距离排序。
$geometry 操作符用于指定一个 GeoJSON 格式的地理空间对象。type=Point 表示地理坐标点,coordinates 则是用户当前所在的经纬度位置;
$maxDistance 限定了最大距离,单位是米。

全文索引 (Text Indexes)

数据准备:

db.stores.insert([
    { _id: 1, name: "Java Hut", description: "Coffee and cakes" },
    { _id: 2, name: "Burger Buns", description: "Gourmet hamburgers" },
    { _id: 3, name: "Coffee Shop", description: "Just coffee" },
    { _id: 4, name: "Clothes Clothes Clothes", description: "Discount clothing"},
    { _id: 5, name: "Java Shopping", description: "Indonesian goods" }
])

创建索引:

//通过建立文本索引来实现简易的分词检索。$text 操作符可以执行文本检索。
db.stores.createIndex( { description: "text" } )

查询:

> db.stores.find({$text: {$search: "java coffee shop"}})
{ "_id" : 3, "name" : "Coffee Shop", "description" : "Just coffee" }
{ "_id" : 1, "name" : "Java Hut", "description" : "Coffee and cakes" }

Hash索引 (Hashed Indexes)

不同于传统的 B-Tree 索引,哈希索引使用 hash 函数来创建索引。在索引字段上进行精确匹配,但不支持范围查询,不支持多键 hash; Hash 索引上的入口是均匀分布的,在分片集合中非常有用。

db.users.createIndex({username : 'hashed'})

通配符索引 (Wildcard Indexes)

MongoDB 的文档模式是动态变化的,而通配符索引可以建立在一些不可预知的字段上,以此实现查询的加速。MongoDB4.2 引入了通配符索引来支持对未知或任意字段的查询。

准备商品数据,不同商品属性不一样:

db.products.insert([
    {
        "_id" : 1,
        "product_name" : "Spy Coat",
        "product_attributes" : {
            "material" : [ "Tweed", "Wool", "Leather" ],
            "size" : { "length" : 72, "units" : "inches"}
        }
    },
    {
        "_id" : 2,
        "product_name" : "Spy Pen",
        "product_attributes" : {
            "colors" : [ "Blue", "Black" ],
            "secret_feature" : { "name" : "laser", "power" : "1000", "units" : "watts"}
        }
    },
    {
        "_id" : 3,
        "product_name" : "Spy Book"
    }
])

创建通配符索引:

db.products.createIndex( { "product_attributes.$**" : 1 } )

查询:通配符索引可以支持任意单字段查询 product_attributes 或其嵌入字段

db.products.find( { "product_attributes.size.length" : { $gt : 60 } } )
db.products.find( { "product_attributes.material" : "Leather" } )
db.products.find( { "product_attributes.secret_feature.name" : "laser" } )
- 通配符索引不兼容的索引类型或属性。(Compound、TTL、Text、2d、2dsphere、Hashed、Unique)
- 通配符索引是稀疏的,不索引空字段。因此,通配符索引不能支持查询字段不存在的文档。
- 通配符索引为文档或数组的内容生成条目,而不是文档/数组本身。因此通配符索引不能支持精确的文档/数组相等匹配。通配符索引可以支持查询字段等于空文档 {} 的情况。
  如,不支持 db.products.find({ "product_attributes.colors" : [ "Blue", "Black" ] } )

索引属性

唯一索引 (Unique Indexes)

通过建立唯一性索引,可以保证集合中文档的指定字段拥有唯一值。

// 创建唯一索引
db.book2.createIndex({title:1},{unique:true})
// 复合索引支持唯一性约束
db.book2.createIndex({title:1, type:1},{unique:true})
// 多键索引支持唯一性约束
db.inventory.createIndex({ratings:1},{unique:true})

部分索引 (Partial Indexes)

部分索引仅对满足指定过滤器表达式的文档进行索引。

db.restaurants.createIndex(
    { cuisine: 1, name: 1 },
    { partialFilterExpression: { rating: { $gt: 5 } } }
)

查询:

// 符合条件,使用索引
db.restaurants.find( { cuisine: "Italian", rating: { $gte: 8 } } )
// 不符合条件,不能使用索引
db.restaurants.find( { cuisine: "Italian" } )

注意:唯一约束结合部分索引使用导致唯一约束失效的问题。

// 只有满足筛选器表达式的文档,才满足唯一约束。
db.users.createIndex(
    { username: 1 },
    { unique: true, partialFilterExpression: { age: { $gte: 21 } } }
)

稀疏索引 (Sparse Indexes)

索引的稀疏属性确保索引只包含具有索引字段的文档的条目,索引将跳过没有索引字段的文档。

# 只对包含xmpp_id字段的文档进行索引
db.addresses.createIndex( { "xmpp_id": 1 }, { sparse: true } )

注意:稀疏索引会导致查询和排序操作的结果集不完整。

db.scores.insertMany([
    {"userid" : "newbie"},
    {"userid" : "abby", "score" : 82},
    {"userid" : "nina", "score" : 90}
])
db.scores.createIndex( { score: 1 } , { sparse: true } )
// 使用稀疏索引
db.scores.find( { score: { $lt: 90 } } )
// 不使用稀疏索引,以返回完整的结果
db.scores.find().sort( { score: -1 } )
// 想使用稀疏索引,使用hint()显式指定索引, 但结果集不完整
db.scores.find().sort( { score: -1 } ).hint( { score: 1 } )

TTL索引 (TTL Indexes)

TTL索引需要声明在一个日期类型的字段中,TTL 索引是特殊的单字段索引,MongoDB 可以使用它在一定时间或特定时钟时间后自动从集合中删除文档。

db.log_events.insertOne( {
    "createDate": new Date(),
    "logMessage": "Success!"
} )

创建TTL索引:

db.log_events.createIndex( { "createDate": 1 }, { expireAfterSeconds: 20 } )

修改过期时间:

db.runCommand({
    collMod:"log_events",
    index:{keyPattern:{createDate:1},expireAfterSeconds:600}
})

:::tip

  1. TTL 索引只能支持单个字段,并且必须是非 _id 字段。

  2. TTL 索引不能用于固定集合。

  3. TTL 索引无法保证及时的数据清理,MongoDB 会通过后台的 TTLMonitor 定时器来清理老化数据,默认的间隔时间是1分钟。当然如果在数据库负载过高的情况下,TTL 的行为则会进一步受到影响。

  4. TTL 索引对于数据的清理仅仅使用了 remove 命令,这种方式并不是很高效。因此TTL Monitor在运行期间对系统CPU、磁盘都会造成一定的压力。相比之下,按日期分表的方式操作会更加高效。 :::

隐藏索引 (Hidden Indexes)

隐藏索引对查询规划器不可见,不能用于支持查询。通过对规划器隐藏索引,用户可以在不实际删除索引的情况下评估删除索引的潜在影响。如果影响是负面的,用户可以取消隐藏索引,而不必重新创建已删除的索引。4.4新版功能。

// 创建隐藏索引
db.restaurants.createIndex({ borough: 1 },{ hidden: true });
// 隐藏现有索引
db.restaurants.hideIndex( { borough: 1} );
db.restaurants.hideIndex( "索引名称" )
// 取消隐藏索引
db.restaurants.unhideIndex( { borough: 1} );
db.restaurants.unhideIndex( "索引名称" );

explain 执行计划

db.collection.find().explain(<verbose>)

verbose 模式

描述

queryPlanner

默认输出模式。执行计划的详细信息,包括查询计划、集合信息、查询条件、最佳执行计划、查询方式和 MongoDB 服务信息等

exectionStats

最佳执行计划的执行情况和被拒绝的计划等信息

allPlansExecution

选择并执行最佳执行计划,并返回最佳执行计划和其他执行计划的执行情况

stage 状态

状态

描述

COLLSCAN

全表扫描

IXSCAN

索引扫描

FETCH

根据索引检索指定文档

SHARD_MERGE

将各个分片返回数据进行合并

SORT

在内存中进行了排序

LIMIT

使用limit限制返回数

SKIP

使用skip进行跳过

IDHACK

对_id进行查询

SHARDING_FILTER

通过mongos对分片数据进行查询

COUNTSCAN

count不使用Index进行count时的stage返回

COUNT_SCAN

count使用了Index进行count时的stage返回

SUBPLA

未使用到索引的$or查询的stage返回

TEXT

使用全文索引进行查询时候的stage返回

PROJECTION

限定返回字段时候stage的返回

执行计划的返回结果中尽量不要出现以下 stage:
 - COLLSCAN (全表扫描)
 - SORT (使用sort但是无index)
 - 不合理的 SKIP
 - SUBPLA (未用到index的$or)
 - COUNTSCAN (不使用index进行count)

注意事项

既然索引可以加快查询速度,那么是不是只要是查询语句,就创建索引呢?答案是否定的。因为索引虽然加快了查询速度,但索引也是有代价的:索引文件本身要消耗存储空间,同时索引会加重插入、删除和修改记录时的负担,另外,数据库在运行时也要消耗资源维护索引,因此索引并不是越多越好。 那么什么情况不建议创建索引呢? 例如一两千条甚至只有几百条记录的表,没必要建索引,让查询做全集合扫描就好了。 至于多少条记录才算多?以万为单位来做索引;

如何创建合适的索引

为每一个常用查询结构建立合适的索引。 复合索引是创建的索引由多个字段组成, 例如: db.test.createIndex({“username”:1, “age”:-1}) 交叉索引是每个字段单独建立索引,但是在查询的时候组合查找, 例如: db.test.createIndex({“username”:1}) db.test.createIndex({“age”:-1}) db.test.find({“username”:“kaka”, “age”: 30})

交叉索引的查询效率较低,在使用时,当查询使用到多个字段的时候,尽量使用复合索引,而不是交叉索引。

复合索引的字段排列顺序

当我们的组合索引内容包含匹配条件以及范围条件的时候,比如包含用户名(匹配条件)以及年龄(范围条件),那么匹配条件应该放在范围条件之前。

比如需要查询: db.test.find({“username”:“kaka”, “age”: {$gt: 30}}) 那么复合索引应该这样创建: db.test.ensureIndex({“username”:1, “age”:-1})

查询时尽可能仅查询出索引字段

有时候仅需要查询少部分的字段内容,而且这部分内容刚好都建立了索引,那么尽可能只查询出这些索引内容,需要用到的字段显式声明(_id字段需要显式忽略!)。因为这些数据需要把原始数据文档从磁盘读入内存,造成一定的损耗。

比如说我们的表有三个字段: name, age, mobile 索引是这样建立的: db.stu.createIndex({“name”:1,“age”:-1}) 我们仅需要查到某个用户的年龄(age),那可以这样写: db.stu.find({“name”:“kaka”}, {“_id”:0, “age”:1}) 注意到上面的语句,我们除了”age”:1外,还加了”_id”:0,因为默认情况下,_id都是会被一并查询出来的,当不需要_id的时候记得直接忽略,避免不必要的磁盘操作。

对现有的数据大表建立索引的时候,采用后台运行方式

在对数据集合建立索引的过程中,数据库会停止该集合的所有读写操作,因此如果建立索引的数据量大,建立过程慢的情况下,建议采用后台运行的方式,避免影响正常业务流程。 db.stu.ensureIndex({“name”:1,“age”:-1},{“background”:true})

索引限制

额外开销

每个索引占据一定的存储空间,在进行插入,更新和删除操作时也需要对索引进行操作。所以,如果你很少对集合进行读取操作,建议不使用索引。反之:使用索引的属性一定查询次数远远高于增加、删除、修改次数。

内存使用

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

查询限制

索引不能被以下的查询使用: 正则表达式(最左匹配除外)及非操作符,如 $nin, $not, 等。 算术运算符,如 $mod, 等。 所以,检测你的语句是否使用索引是一个好的习惯,可以用explain来查看。

最大范围

集合中索引不能超过64个 索引名的长度不能超过128个字符 一个复合索引最多可以有31个字段