文章目录
索引详解
MongoDB采用B+Tree 做索引,索引创建colletions上。
索引数据结构
B-Tree说法来源于官方文档,然后就导致了分歧:有人说MongoDB索引数据结构使用的是B-Tree,有的人又说是B+Tree。
MongoDB官方文档:https://docs.mongodb.com/manual/indexes/
MongoDB indexes use a B-tree data structure.
WiredTiger官方文档:https://source.wiredtiger.com/3.0.0/tune_page_size_and_comp.html
这里就说了,mongodb的索引其实是使用的B+树
WiredTiger maintains a table’s data in memory using a data structure called a B-Tree ( B+ Tree to be specific), referring to the nodes of a B-Tree as pages. Internal pages carry only keys. The leaf pages store both keys and values.
WiredTiger数据文件在磁盘的存储结构
mongodb的数据在磁盘中存储也是B+树的格式
B+ Tree中的leaf page包含一个页头(page header)、块头(block header)和真正的数据(key/value),其中页头定义了页的类型、页中实际载荷数据的大小、页中记录条数等信息;块头定义了此页的checksum、块在磁盘上的寻址位置等信息。
WiredTiger有一个块设备管理的模块,用来为page分配block。如果要定位某一行数据(key/value)的位置,可以先通过block的位置找到此page(相对于文件起始位置的偏移量),再通过page找到行数据的相对位置,最后可以得到行数据相对于文件起始位置的偏移量offsets。
索引操作
创建索引
db.collection.createIndex( <key and index type specification>, <options> )
-
Key 值为你要创建的索引字段,1 按升序创建索引, -1 按降序创建索引
-
options可选参数列表如下:
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.
3.0.0 版本前创建索引方法为 db.collection.ensureIndex()
# 创建索引后台执行 并且是唯一索引 , 我这里未指定索引名,所以就默认生成了title_1_favCount_1作为名字
test> db.books.createIndex({title: 1, favCount: 1},{background: true, unique: true})
title_1_favCount_1
查看索引
#查看索引信息
test> db.books.getIndexes()
[
{ v: 2, key: { _id: 1 }, name: '_id_' },
{
v: 2,
key: { title: 1, favCount: 1 },
name: 'title_1_favCount_1',
background: true,
unique: true
}
]
test> db.books.getIndexSpecs()
[
{ v: 2, key: { _id: 1 }, name: '_id_' },
{
v: 2,
key: { title: 1, favCount: 1 },
name: 'title_1_favCount_1',
background: true,
unique: true
}
]
# 查看索引键
test> db.books.getIndexKeys()
[ { _id: 1 }, { title: 1, favCount: 1 } ]
删除索引
#删除集合指定索引
db.集合名.dropIndex("索引名称")
#删除集合所有索引 不能删除主键索引
db.集合名.dropIndexes()
# 案例
# 先删除,再查询发现就只有一个主键索引了
test> db.books.dropIndex("title_1_favCount_1")
{ nIndexesWas: 2, ok: 1 }
test> db.books.getIndexes()
[ { v: 2, key: { _id: 1 }, name: '_id_' } ]
索引的类型
单键索引
单键索引(Single Field Indexes)
在某一个特定的字段上建立索引 mongoDB在ID上建立了唯一的单键索引,所以经常会使用id来进行查询; 在索引字段上进行精确匹配、排序以及范围查找都会使用此索引
# 创建一个单键索引
db.books.createIndex({title:1})
# 对内嵌文档字段创建索引:
db.books.createIndex({"author.name":1})
复合索引
**复合索引(Compound Index)**复合索引是多个字段组合而成的索引
复合索引中字段的顺序、字段的升降序对查询性能有直接的影响
# 创建一个复合索引
test> db.books.createIndex({type:1,favCount:1})
type_1_favCount_1
#查看执行计划
test> db.books.find({type: "travel", favCount: 14}).explain()
多键(数组)索引
在数组的属性上建立索引。针对这个数组的任意值的查询都会定位到这个文档,既多个索引入口或者键值引用同一个文档
准备inventory集合:
db.inventory.insertMany([
{ _id: 5, type: "food", item: "aaa", ratings: [ 5, 8, 9 ] },
{ _id: 6, type: "food", item: "bbb", ratings: [ 5, 9 ] },
{ _id: 7, type: "food", item: "ccc", ratings: [ 9, 5, 8 ] },
{ _id: 8, type: "food", item: "ddd", ratings: [ 9, 5 ] },
{ _id: 9, type: "food", item: "eee", ratings: [ 5, 9, 5 ] }
])
创建多键索引
test> db.inventory.createIndex({ratings: 1})
ratings_1
多键索引很容易与复合索引产生混淆,复合索引是多个字段的组合,而多键索引则仅仅是在一个字段上出现了多键(multi key)。而实质上,多键索引也可以出现在复合字段上
# 创建复合多键索引
test> db.inventory.createIndex({item:1, ratings: 1})
item_1_ratings_1
注意: MongoDB并不支持一个复合索引中同时出现多个数组字段
嵌入文档的索引数组
db.inventory.insertMany([
{
_id: 1,
item: "abc",
stock: [
{ size: "S", color: "red", quantity: 25 },
{ size: "S", color: "blue", quantity: 10 },
{ size: "M", color: "blue", quantity: 50 }
]
},
{
_id: 2,
item: "def",
stock: [
{ size: "S", color: "blue", quantity: 20 },
{ size: "M", color: "blue", quantity: 5 },
{ size: "M", color: "black", quantity: 10 },
{ size: "L", color: "red", quantity: 2 }
]
},
{
_id: 3,
item: "ijk",
stock: [
{ size: "M", color: "blue", quantity: 15 },
{ size: "L", color: "blue", quantity: 100 },
{ size: "L", color: "red", quantity: 25 }
]
}
])
在包含嵌套对象的数组字段上创建多键索引
test> db.inventory.createIndex({"stock.size": 1,"stock.quantity": 1})
stock.size_1_stock.quantity_1
# 再执行一次查询,看看执行计划
test> db.inventory.find({"stock.size": "M", "stock.quantity": {$gt: 15}}).explain()
Hash索引
在索引字段上进行精确匹配,但不支持范围查询,不支持多键hash; Hash索引上的入口是均匀分布的,在分片集合中非常有用;
db.集合名.createIndex({字段名: "hashed"})
# 如下所示
test> db.inventory.createIndex({"item": "hashed"})
item_hashed
地理空间索引
在移动互联网时代,基于地理位置的检索(LBS)功能几乎是所有应用系统的标配。MongoDB为地理空间检索提供了非常方便的功能。地理空间索引(2dsphereindex)就是专门用于实现位置检索的一种特殊索引。
案例:MongoDB如何实现“查询附近商家"?
假设商家的数据模型如下:
db.restaurant.insert({
restaurantId: 0,
restaurantName:"兰州牛肉面",
location : {
type: "Point",
coordinates: [ -73.97, 40.77 ]
}
})
创建一个2dsphere索引
db.集合名.createIndex({字段名: "2dsphere"})
# 案例如下
test> db.restaurant.createIndex({"location": "2dsphere"})
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限定了最大距离,单位是米。
全文索引
MongoDB支持全文检索功能,可通过建立文本索引来实现简易的分词检索。
db.集合名.createIndex( { 字段名: "text" } )
t e x t 操作符可以在有 t e x t i n d e x 的集合上执行文本检索。 text操作符可以在有text index的集合上执行文本检索。 text操作符可以在有textindex的集合上执行文本检索。text将会使用空格和标点符号作为分隔符对检索字符串进行分词, 并且对检索字符串中所有的分词结果进行一个逻辑上的 OR 操作。
全文索引能解决快速文本查找的需求,比如有一个博客文章集合,需要根据博客的内容来快速查找,则可以针对博客内容建立文本索引。
案例
数据准备
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" }
]
)
创建name和description的全文索引
test> db.stores.createIndex({name: "text", description: "text"})
name_text_description_text
测试
通过$text操作符来查寻数据中所有包含“coffee”,”shop”,“java”列表中任何词语的商店
test> db.stores.find({$text: {$search: "coffee java shop"}})
[
{ _id: 3, name: 'Coffee Shop', description: 'Just coffee' },
{ _id: 1, name: 'Java Hut', description: 'Coffee and cakes' },
{ _id: 5, name: 'Java Shopping', description: 'Indonesian goods' }
]
MongoDB的文本索引功能存在诸多限制,而官方并未提供中文分词的功能,这使得该功能的应用场景十分受限。
通配符索引
通配符索引(Wildcard Indexes)
MongoDB的文档模式是动态变化的,而通配符索引可以建立在一些不可预知的字段上,以此实现查询的加速。
MongoDB 4.2 引入了通配符索引来支持对未知或任意字段的查询。
案例
准备商品数据,不同商品属性不一样
db.products.insert([
{
"product_name" : "Spy Coat",
"product_attributes" : {
"material" : [ "Tweed", "Wool", "Leather" ],
"size" : {
"length" : 72,
"units" : "inches"
}
}
},
{
"product_name" : "Spy Pen",
"product_attributes" : {
"colors" : [ "Blue", "Black" ],
"secret_feature" : {
"name" : "laser",
"power" : "1000",
"units" : "watts",
}
}
},
{
"product_name" : "Spy Book"
}
])
创建通配符索引
test> db.products.createIndex({"product_attributes.$**": 1})
product_attributes.$**_1
测试
通配符索引可以支持任意单字段查询 product_attributes或其嵌入字段:
test> db.products.find({"product_attributes.size.length": {$gt: 60}})
[
{
_id: ObjectId("66a639fac7bc0da7a56aa3f9"),
product_name: 'Spy Coat',
product_attributes: {
material: [ 'Tweed', 'Wool', 'Leather' ],
size: { length: 72, units: 'inches' }
}
}
]
test> db.products.find({"product_attributes.material": "Wool"})
[
{
_id: ObjectId("66a639fac7bc0da7a56aa3f9"),
product_name: 'Spy Coat',
product_attributes: {
material: [ 'Tweed', 'Wool', 'Leather' ],
size: { length: 72, units: 'inches' }
}
}
]
test> db.products.find({"product_attributes.secret_feature.name" : "laser"})
[
{
_id: ObjectId("66a639fac7bc0da7a56aa3fa"),
product_name: 'Spy Pen',
product_attributes: {
colors: [ 'Blue', 'Black' ],
secret_feature: { name: 'laser', power: '1000', units: 'watts' }
}
}
]
注意事项:
- 通配符索引不兼容的索引类型或属性
-
通配符索引是稀疏的,不索引空字段。因此,通配符索引不能支持查询字段不存在的文档。
# 通配符索引不能支持以下查询 db.products.find( {"product_attributes" : { $exists : false } } ) db.products.aggregate([ { $match : { "product_attributes" : { $exists : false } } } ])
-
通配符索引为文档或数组的内容生成条目,而不是文档/数组本身。因此通配符索引不能支持精确的文档/数组相等匹配。通配符索引可以支持查询字段等于空文档{}的情况。
#通配符索引不能支持以下查询: db.products.find({ "product_attributes.colors" : [ "Blue", "Black" ] } ) db.products.aggregate([{ $match : { "product_attributes.colors" : [ "Blue", "Black" ] } }])
索引属性
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. |
唯一索引
# 创建唯一索引
db.collection.createIndex({字段名: 1}, {unique: true})
# 复合索引支持唯一性约束
db.collection.createIndex({字段1: 1, 字段2: 1}, {unique: true})
# 多键索引支持唯一性约束
db.collection.createIndex({字段名: 1, 数组类型字段名: 1}, {unique: true})
- 唯一性索引对于文档中缺失的字段,会使用null值代替,因此不允许存在多个文档缺失索引字段的情况。
- 对于分片的集合,唯一性约束必须匹配分片规则。换句话说,为了保证全局的唯一性,分片键必须作为唯一性索引的前缀字段。
部分索引
部分索引仅对满足指定过滤器表达式的文档进行索引。
通过在一个集合中为文档的一个子集建立索引,部分索引具有更低的存储需求和更低的索引创建和维护的性能成本。
3.2新版功能。部分索引提供了稀疏索引功能的超集,应该优先于稀疏索引。
test> db.books.createIndex(
{title: 1, type: 1},
{partialFilterExpression: {favCount: {$gt: 5}}}
)
title_1_type_1
partialFilterExpression选项接受指定过滤条件的文档:
- 等式表达式(例如:field: value或使用$eq操作符)
- $exists: true
- $gt, $gte, $lt, $lte
- $type
- 顶层的$and
# 符合条件,使用索引
db.books.find({title: "book-18", favCount: {$gt: 8}})
# 不符合条件,不能使用索引
db.books.find({title: "book-18"})
db.books.find({title: "book-18", type: 'technology'})
唯一约束结合部分索引使用导致唯一约束失效,能插入重复数据的问题
注意:如果同时指定了partialFilterExpression和唯一约束,那么唯一约束只适用于满足筛选器表达式的文档。如果文档不满足筛选条件,那么带有惟一约束的部分索引不会阻止插入不满足惟一约束的文档。
案例1
和上面books集合的测试案例没什么区别
restaurants集合数据
db.restaurants.insert({
"_id" : ObjectId("5641f6a7522545bc535b5dc9"),
"address" : {
"building" : "1007",
"coord" : [
-73.856077,
40.848447
],
"street" : "Morris Park Ave",
"zipcode" : "10462"
},
"borough" : "Bronx",
"cuisine" : "Bakery",
"rating" : { "date" : ISODate("2014-03-03T00:00:00Z"),
"grade" : "A",
"score" : 2
},
"name" : "Morris Park Bake Shop",
"restaurant_id" : "30075445"
})
创建索引
db.restaurants.createIndex(
{ borough: 1, cuisiine: 1 },
{ partialFilterExpression: { 'rating.grade': { $eq: 'A' } } }
)
测试:
# 能用索引
db.restaurants.find( { borough: "Bronx", 'rating.grade': "A" } )
# 不能用索引
db.restaurants.find( { borough: "Bronx", cuisine: "Bakery" } )
案例2
users集合数据准备
db.users.insertMany( [
{ username: "david", age: 29 },
{ username: "amanda", age: 35 },
{ username: "rajiv", age: 57 }
] )
创建索引,指定username字段和部分过滤器表达式age: {$gte: 21}的唯一约束。
db.users.createIndex(
{username: 1},
{unique: true, partialFilterExpression: {age: {$gte: 21}}}
)
# 索引防止了以下文档的插入,因为文档已经存在,且指定的用户名和年龄字段大于21:
db.users.insertMany( [
{ username: "david", age: 27 },
{ username: "amanda", age: 25 },
{ username: "rajiv", age: 32 }
] )
# 执行结果,报错信息也提到了 test数据库的users集合下的 username_1 这个索引
Uncaught:
MongoBulkWriteError: E11000 duplicate key error collection: test.users index: username_1 dup key: { username: "david" }
Result: BulkWriteResult {
insertedCount: 0,
matchedCount: 0,
modifiedCount: 0,
deletedCount: 0,
upsertedCount: 0,
upsertedIds: {},
insertedIds: {
'0': ObjectId("66a64554c7bc0da7a56aa3ff"),
'1': ObjectId("66a64554c7bc0da7a56aa400"),
'2': ObjectId("66a64554c7bc0da7a56aa401")
}
}
Write Errors: [
WriteError {
err: {
index: 0,
code: 11000,
errmsg: 'E11000 duplicate key error collection: test.users index: username_1 dup key: { username: "david" }',
errInfo: undefined,
op: {
username: 'david',
age: 27,
_id: ObjectId("66a64554c7bc0da7a56aa3ff")
}
}
}
]
# 但是,以下具有重复用户名的文档是允许的,因为唯一约束只适用于年龄大于或等于21岁的文档。
db.users.insertMany( [
{ username: "david", age: 20 },
{ username: "amanda" },
{ username: "rajiv", age: null }
] )
稀疏索引
稀疏索引(Sparse Indexes)
索引的稀疏属性确保索引只包含具有索引字段的文档的条目,索引将跳过没有索引字段的文档。
特性: 只对存在字段的文档进行索引(包括字段值为null的文档)
# 创建稀疏索引格式
db.集合名.createIndex( { 字段名: 1 }, { sparse: true } )
如果稀疏索引会导致查询和排序操作的结果集不完整,MongoDB将不会使用该索引,除非hint()明确指定索引。
同时具有稀疏性和唯一性的索引可以防止集合中存在字段值重复的文档,但允许不包含此索引字段的文档插入。
案例:
数据准备
db.scores.insertMany([
{"userid" : "newbie"},
{"userid" : "abby", "score" : 82},
{"userid" : "nina", "score" : 90}
])
创建索引
test> db.scores.createIndex({score: 1}, {sparse: true})
score_1
# 测试
# 下面这种会使用索引
db.scores.find({score: {$lt: 85}})
# 不会使用索引,因为如果使用我们创建的稀疏索引会导致查询的结果集不完整
db.scores.find().sort({score: -1})
# 我们也可以通过hint()来指定使用索引, 我们创建的索引就是score: 1
db.scores.find().sort({score: -1}).hint({score: 1})
案例
# 删除上面创建的稀疏索引
test> db.scores.dropIndex('score_1')
{ nIndexesWas: 2, ok: 1 }
# 创建稀疏+唯一索引
test> db.scores.createIndex({score: 1}, {unique: true, sparse: true})
score_1
测试
这个索引将允许插入具有唯一的分数字段值或不包含分数字段的文档。因此,给定scores集合中的现有文档,索引允许以下插入操作:
db.scores.insertMany( [
{ "userid": "AAAAAAA", "score": 50 },
{ "userid": "BBBBBBB", "score": 64 },
{ "userid": "CCCCCCC" },
{ "userid": "CCCCCCC" }
] )
索引不允许添加下列文件,因为已经存在评分为82和90的文件:
test> db.scores.insertMany( [
{ "userid": "AAAAAAA", "score": 82 },
{ "userid": "BBBBBBB", "score": 90 }
] )
Uncaught:
MongoBulkWriteError: E11000 duplicate key error collection: test.scores index: score_1 dup key: { score: 82 }
Result: BulkWriteResult {
insertedCount: 0,
matchedCount: 0,
modifiedCount: 0,
deletedCount: 0,
upsertedCount: 0,
upsertedIds: {},
insertedIds: {
'0': ObjectId("66a64bebc7bc0da7a56aa40c"),
'1': ObjectId("66a64bebc7bc0da7a56aa40d")
}
}
Write Errors: [
WriteError {
err: {
index: 0,
code: 11000,
errmsg: 'E11000 duplicate key error collection: test.scores index: score_1 dup key: { score: 82 }',
errInfo: undefined,
op: {
userid: 'AAAAAAA',
score: 82,
_id: ObjectId("66a64bebc7bc0da7a56aa40c")
}
}
}
]
TTL索引
在一般的应用系统中,并非所有的数据都需要永久存储。例如一些系统事件、用户消息等,这些数据随着时间的推移,其重要程度逐渐降低。更重要的是,存储这些大量的历史数据需要花费较高的成本,因此项目中通常会对过期且不再使用的数据进行老化处理。
通常的做法如下:
-
为每个数据记录一个时间戳,应用侧开启一个定时器,按时间戳定期删除过期的数据。
-
数据按日期进行分表,同一天的数据归档到同一张表,同样使用定时器删除过期的表。
对于数据老化,MongoDB提供了一种更加便捷的做法:TTL(Time To Live)索引。
TTL索引需要声明在一个日期类型的字段中,TTL 索引是特殊的单字段索引,MongoDB 可以使用它在一定时间或特定时钟时间后自动从集合中删除文档。
# 创建 TTL 索引,TTL 值为3600秒
db.eventlog.createIndex( { "lastModifiedDate": 1 }, { expireAfterSeconds: 3600 } )
TTL 索引不保证过期数据会在过期后立即被删除。删除过期文档的后台任务每 60 秒运行一次。
案例
数据准备
db.log_events.insertOne( {
"createdAt": new Date(),
"logEvent": 2,
"logMessage": "Success!"
} )
创建TTL索引
# 方便测试,过期时间设置为20s, 20s之后,上方插入的一条文档就被删除了
test> db.log_events.createIndex({createdAt: 1}, {expireAfterSeconds: 20})
createdAt_1
可变的过期时间
TTL索引在创建之后,仍然可以对过期时间进行修改。这需要使用collMod命令对索引的定义进行变更
# db.runCommand({collMod: "集合名", index: {keyPattern: {索引}, expireAfterSeconds: 600}})
test> db.runCommand({collMod: "log_events", index: {keyPattern: {createdAt: 1}, expireAfterSeconds: 600}})
使用约束
TTL索引的确可以减少开发的工作量,而且通过数据库自动清理的方式会更加高效、可靠,但是在使用TTL索引时需要注意以下的限制:
- TTL索引只能支持单个字段,并且必须是非_id字段。
- TTL索引不能用于固定集合。
- TTL索引无法保证及时的数据老化,MongoDB会通过后台的TTLMonitor定时器来清理老化数据,默认的间隔时间是1分钟。当然如果在数据库负载过高的情况下,TTL的行为则会进一步受到影响。
- TTL索引对于数据的清理仅仅使用了remove命令,这种方式并不是很高效。因此TTL Monitor在运行期间对系统CPU、磁盘都会造成一定的压力。相比之下,按日期分表的方式操作会更加高效。
隐藏索引
隐藏索引(Hidden Indexes)
隐藏索引对查询规划器不可见,不能用于支持查询。
通过对规划器隐藏索引,用户可以在不实际删除索引的情况下评估删除索引的潜在影响。如果影响是负面的,用户可以取消隐藏索引,而不必重新创建已删除的索引。4.4新版功能。
# 创建隐藏索引
db.集合名.createIndex({ 字段名: 1 },{ hidden: true });
# 隐藏现有索引
db.集合名.hideIndex( { 字段名: 1} );
db.集合名.hideIndex( "索引名称" )
# 取消隐藏索引
db.集合名.unhideIndex( { 字段名: 1} );
db.集合名.unhideIndex( "索引名称" );
案例
准备测试数据
db.scores.insertMany([
{"userid" : "newbie"},
{"userid" : "abby", "score" : 82},
{"userid" : "nina", "score" : 90}
])
创建隐藏索引
db.scores.createIndex(
{ userid: 1 },
{ hidden: true }
)
测试
# 下面这条查询未使用索引
db.scores.find({userid:"abby"}).explain()
# 取消隐藏索引
db.scores.unhideIndex( { userid: 1} )
# 此时就使用索引
db.scores.find({userid:"abby"}).explain()
索引使用建议
为每一个查询建立合适的索引
这个是针对于数据量较大比如说超过几十上百万(文档数目)数量级的集合。如果没有索引MongoDB需要把所有的Document从盘上读到内存,这会对MongoDB服务器造成较大的压力并影响到其他请求的执行。
创建合适的复合索引,不要依赖于交叉索引
如果你的查询会使用到多个字段,MongoDB有两个索引技术可以使用:交叉索引和复合索引。
交叉索引就是针对每个字段单独建立一个单字段索引,然后在查询执行时候使用相应的单字段索引进行索引交叉而得到查询结果。交叉索引目前触发率较低,所以如果你有一个多字段查询的时候,建议使用复合索引能够保证索引正常的使用。
#查找所有年龄小于30岁的深圳市马拉松运动员
db.athelets.find({sport: "marathon", location: "sz", age: {$lt: 30}}})
#创建复合索引
db.athelets.createIndex({sport:1, location:1, age:1})
复合索引字段顺序:匹配条件在前,范围条件在后(Equality First, Range After)
前面的例子,在创建复合索引时如果条件有匹配和范围之分,那么匹配条件(sport: “marathon”) 应该在复合索引的前面。范围条件(age: <30)字段应该放在复合索引的后面。
尽可能使用覆盖索引(Covered Index)
建议只返回需要的字段,同时,利用覆盖索引来提升性能。
建索引要在后台运行
在对一个集合创建索引时,该集合所在的数据库将不接受其他读写操作。对大数据量的集合建索引,建议使用后台运行选项 {background: true}
避免设计过长的数组索引
数组索引是多值的,在存储时需要使用更多的空间。如果索引的数组长度特别长,或者数组的增长不受控制,则可能导致索引空间急剧膨胀。
explain执行计划
通常我们需要关心的问题:
- 查询是否使用了索引
- 索引是否减少了扫描的记录数量
- 是否存在低效的内存排序
MongoDB提供了explain命令,它可以帮助我们评估指定查询模型(querymodel)的执行计划,根据实际情况进行调整,然后提高查询效率。
explain()方法的形式如下:
db.collection.find().explain(<verbose>)
verbose
可选参数,表示执行计划的输出模式,默认queryPlanner
模式名字 | 描述 |
---|---|
queryPlanner | 执行计划的详细信息,包括查询计划、集合信息、查询条件、最佳执行计划、查询方式和 MongoDB 服务信息等 |
exectionStats | 最佳执行计划的执行情况和被拒绝的计划等信息 |
allPlansExecution | 选择并执行最佳执行计划,并返回最佳执行计划和其他执行计划的执行情况 |
queryPlanner
# 此时我未对books数据库创建索引
db.books.find({title:"book-1"}).explain("queryPlanner")
字段名称 | 描述 |
---|---|
explainVersion | 执行计划的版本 |
namespace | 查询的集合 |
indexFilterSet | 是否使用索引 |
parsedQuery | 查询条件 |
winningPlan | 最佳执行计划 |
stage | 查询方式 |
filter | 过滤条件 |
direction | 查询顺序 |
rejectedPlans | 拒绝的执行计划 |
serverInfo | mongodb服务器信息 |
executionStats
executionStats 模式的返回信息中包含了 queryPlanner 模式的所有字段,并且还包含了最佳执行计划的执行情况
#创建索引
db.books.createIndex({title:1})
db.books.find({title:"book-1"}).explain("executionStats")
test> db.books.find({title:"book-1"}).explain("executionStats")
{
explainVersion: '1',
queryPlanner: {
namespace: 'test.books',
indexFilterSet: false,
parsedQuery: { title: { '$eq': 'book-1' } },
queryHash: '244E9C29',
planCacheKey: 'D716F3F7',
maxIndexedOrSolutionsReached: false,
maxIndexedAndSolutionsReached: false,
maxScansToExplodeReached: false,
winningPlan: {
stage: 'FETCH',
inputStage: {
stage: 'IXSCAN',
keyPattern: { title: 1 },
indexName: 'title_1',
isMultiKey: false,
multiKeyPaths: { title: [] },
isUnique: false,
isSparse: false,
isPartial: false,
indexVersion: 2,
direction: 'forward',
indexBounds: { title: [ '["book-1", "book-1"]' ] }
}
},
rejectedPlans: []
},
executionStats: {
executionSuccess: true,
nReturned: 1,
executionTimeMillis: 0,
totalKeysExamined: 1,
totalDocsExamined: 1,
executionStages: {
stage: 'FETCH',
nReturned: 1,
executionTimeMillisEstimate: 0,
works: 2,
advanced: 1,
needTime: 0,
needYield: 0,
saveState: 0,
restoreState: 0,
isEOF: 1,
docsExamined: 1,
alreadyHasObj: 0,
inputStage: {
stage: 'IXSCAN',
nReturned: 1,
executionTimeMillisEstimate: 0,
works: 2,
advanced: 1,
needTime: 0,
needYield: 0,
saveState: 0,
restoreState: 0,
isEOF: 1,
keyPattern: { title: 1 },
indexName: 'title_1',
isMultiKey: false,
multiKeyPaths: { title: [] },
isUnique: false,
isSparse: false,
isPartial: false,
indexVersion: 2,
direction: 'forward',
indexBounds: { title: [ '["book-1", "book-1"]' ] },
keysExamined: 1,
seeks: 1,
dupsTested: 0,
dupsDropped: 0
}
}
},
command: { find: 'books', filter: { title: 'book-1' }, '$db': 'test' },
serverInfo: {
host: 'localhost.localdomain',
port: 27017,
version: '6.0.5',
gitVersion: 'c9a99c120371d4d4c52cbb15dac34a36ce8d3b1d'
},
serverParameters: {
internalQueryFacetBufferSizeBytes: 104857600,
internalQueryFacetMaxOutputDocSizeBytes: 104857600,
internalLookupStageIntermediateDocumentMaxSizeBytes: 104857600,
internalDocumentSourceGroupMaxMemoryBytes: 104857600,
internalQueryMaxBlockingSortMemoryUsageBytes: 104857600,
internalQueryProhibitBlockingMergeOnMongoS: 0,
internalQueryMaxAddToSetBytes: 104857600,
internalDocumentSourceSetWindowFieldsMaxMemoryBytes: 104857600
},
ok: 1
}
字段名称 | 描述 |
---|---|
winningPlan.inputStage | 用来描述子stage,并且为其父stage提供文档和索引关键字 |
winningPlan.inputStage.stage | 子查询方式 |
winningPlan.inputStage.keyPattern | 所扫描的index内容 |
winningPlan.inputStage.indexName | 索引名 |
winningPlan.inputStage.isMultiKey | 是否是Multikey。如果索引建立在array上,将是true |
executionStats.executionSuccess | 是否执行成功 |
executionStats.nReturned | 返回的个数 |
executionStats.executionTimeMillis | 这条语句执行时间 |
executionStats.executionStages.executionTimeMillisEstimate | 检索文档获取数据的时间 |
executionStats.executionStages.inputStage.executionTimeMillisEstimate | 扫描获取数据的时间 |
executionStats.totalKeysExamined | 索引扫描次数 |
executionStats.totalDocsExamined | 文档扫描次数 |
executionStats.executionStages.isEOF | 是否到达 steam 结尾,1 或者 true 代表已到达结尾 |
executionStats.executionStages.works | 工作单元数,一个查询会分解成小的工作单元 |
executionStats.executionStages.advanced | 优先返回的结果数 |
executionStats.executionStages.docsExamined | 文档检查数 |
allPlansExecution
allPlansExecution返回的信息包含 executionStats 模式的内容,且包含allPlansExecution:[]块
"allPlansExecution" : [
{
"nReturned" : <int>,
"executionTimeMillisEstimate" : <int>,
"totalKeysExamined" : <int>,
"totalDocsExamined" :<int>,
"executionStages" : {
"stage" : <STAGEA>,
"nReturned" : <int>,
"executionTimeMillisEstimate" : <int>,
...
}
}
},
...
]
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)