索引
什么是索引?
索引是特殊的数据结构,它以一种易于遍历的形式存储集合数据集的一小部分。索引存储一个或一组特定字段的值,按字段的值排序。索引项的排序支持有效的相等匹配和基于范围的查询操作。此外,MongoDB可以通过使用索引中的排序返回排序后的结果。
WiredTiger: B+
比如基于 B+ tree 的索引数据结构图示如下:
单键索引
如基于主键ID 进行的B+ tree 数据结构
复合索引(复合索引只能支持前缀子查询)(A,B,C)
基于name, age, position 建立的复合索引
name
name age
name age postion
索引的特点?
索引支持更快的查询
更快的排序
默认id索引
在创建集合期间,MongoDB 在_id字段上创建唯一索引。该索引可防止客户端插入两个具有相同值的文档。你不能将_id字段上的index删除。
创建索引
db.collection.createIndex(<keys>, <options>)
<keys> 指定了创建索引的字段
构造一组数据
mongos> db.orders.insertMany( [
... { _id: 0, name: "Pepperoni", size: "small", price: 19,
... quantity: 10, date: ISODate( "2021-03-13T08:14:30Z" ) },
... { _id: 1, name: "Pepperoni", size: "medium", price: 20,
... quantity: 20, date : ISODate( "2021-03-13T09:13:24Z" ) },
... { _id: 2, name: "Pepperoni", size: "large", price: 21,
... quantity: 30, date : ISODate( "2021-03-17T09:22:12Z" ) },
... { _id: 3, name: "Cheese", size: "small", price: 12,
... quantity: 15, date : ISODate( "2021-03-13T11:21:39.736Z" ) },
... { _id: 4, name: "Cheese", size: "medium", price: 13,
... quantity:50, date : ISODate( "2022-01-12T21:23:13.331Z" ) },
... { _id: 5, name: "Cheese", size: "large", price: 14,
... quantity: 10, date : ISODate( "2022-01-12T05:08:13Z" ) },
... { _id: 6, name: "Vegan", size: "small", price: 17,
... quantity: 10, date : ISODate( "2021-01-13T05:08:13Z" ) },
... { _id: 7, name: "Vegan", size: "medium", price: 18,
... quantity: 10, date : ISODate( "2021-01-13T05:10:13Z" ) }
... ] )
创建一个单键索引
mongos> db.orders.createIndex({"name":1})
{
"raw" : {
"shard2/node01:28010,node01:28011,node01:28012" : {
"numIndexesBefore" : 2,
"numIndexesAfter" : 3,
"createdCollectionAutomatically" : false,
"commitQuorum" : "votingMembers",
"ok" : 1
},
"shard1/node01:27010,node01:27011,node01:27012" : {
"numIndexesBefore" : 2,
"numIndexesAfter" : 3,
"createdCollectionAutomatically" : false,
"commitQuorum" : "votingMembers",
"ok" : 1
}
},
"ok" : 1,
"$clusterTime" : {
"clusterTime" : Timestamp(1652851733, 8),
"signature" : {
"hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
"keyId" : NumberLong(0)
}
},
"operationTime" : Timestamp(1652851733, 8)
}
索引的默认名称是索引键和索引中每个键的方向(即1或-1)的连接,使用下划线作为分隔符, 也可以通过指定 name 来自定义索引名称;
mongos> db.orders.createIndex({"name":1},{name:"name_1"})
{
"raw" : {
"shard1/node01:27010,node01:27011,node01:27012" : {
"numIndexesBefore" : 3,
"numIndexesAfter" : 3,
"note" : "all indexes already exist",
"ok" : 1
},
"shard2/node01:28010,node01:28011,node01:28012" : {
"numIndexesBefore" : 3,
"numIndexesAfter" : 3,
"note" : "all indexes already exist",
"ok" : 1
}
},
"ok" : 1,
"$clusterTime" : {
"clusterTime" : Timestamp(1652851761, 1),
"signature" : {
"hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
"keyId" : NumberLong(0)
}
},
"operationTime" : Timestamp(1652851761, 1)
}
对于单字段索引和排序操作,索引键的排序顺序(升序或降序)并不重要,因为MongoDB可以从任何方向遍历索引。
查询集合中已经存在的索引
mongos> db.orders.getIndexes()
[
{
"v" : 2,
"key" : {
"_id" : 1
},
"name" : "_id_"
},
{
"v" : 2,
"key" : {
"_id" : "hashed"
},
"name" : "_id_hashed"
},
{
"v" : 2,
"key" : {
"name" : 1
},
"name" : "name_1"
}
]
创建一个复合索引
MongoDB支持在多个字段上创建用户定义索引,即 复合索引。
复合索引中列出的字段的顺序具有重要意义。如果一个复合索引由 {name: 1, age: -1} 组成,索引首先按name 升序排序,然后在每个name值内按 age 降序 排序。
mongos> db.orders.createIndex({name:-1,size:1})
mongos> db.orders.getIndexes()
[
{
"v" : 2,
"key" : {
"_id" : 1
},
"name" : "_id_"
},
{
"v" : 2,
"key" : {
"name" : 1
},
"name" : "name_1"
},
{
"v" : 2,
"key" : {
"name" : -1,
"size" : 1
},
"name" : "name_-1_size_1"
}
]
对于复合索引和排序操作,索引键的排序顺序(升序或降序)可以决定索引是否支持排序操作。
创建多键索引
MongoDB使用多键索引来索引存储在数组中的内容。如果索引包含数组值的字段,MongoDB为数组的每个元素创建单独的索引项。数组字段中的每一个元素,都会在多键索引中创建一个键
db.collection.createIndex( { tags:1});
索引的效果解析
可以使用 explain 进行分析的操作包含 aggregate, count, distinct, find ,group, remove, update
winningPlan: stage 的值含义
COLLSCAN: 整个集合扫描
IXScan: 索引扫描
FETCH: 根据索引指向的文档的地址进行查询
SORT: 需要再内存中排序,效率不高
覆盖查询
当查询条件和查询的<投影>只包含索引字段时,MongoDB直接从索引返回结果,而不扫描任何文档或将文档带入内存。这些覆盖的查询可能非常高效。
mongos> db.orders.explain().find({ name:"Pepperoni"},{_id:0, name:1});
{
"queryPlanner" : {
"mongosPlannerVersion" : 1,
"winningPlan" : {
"stage" : "SHARD_MERGE",
"shards" : [
{
"shardName" : "shard1",
"connectionString" : "shard1/node01:27010,node01:27011,node01:27012",
"serverInfo" : {
"host" : "node01",
"port" : 27010,
"version" : "5.0.8",
"gitVersion" : "c87e1c23421bf79614baf500fda6622bd90f674e"
},
"namespace" : "order.orders",
"indexFilterSet" : false,
"parsedQuery" : {
"name" : {
"$eq" : "Pepperoni"
}
},
"queryHash" : "3066FB64",
"planCacheKey" : "066D32F9",
"maxIndexedOrSolutionsReached" : false,
"maxIndexedAndSolutionsReached" : false,
"maxScansToExplodeReached" : false,
"winningPlan" : {
"stage" : "PROJECTION_SIMPLE",
"transformBy" : {
"_id" : 0,
"name" : 1
},
"inputStage" : {
"stage" : "SHARDING_FILTER",
"inputStage" : {
"stage" : "FETCH",
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"name" : 1
},
"indexName" : "name_1",
"isMultiKey" : false,
"multiKeyPaths" : {
"name" : [ ]
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "forward",
"indexBounds" : {
"name" : [
"[\"Pepperoni\", \"Pepperoni\"]"
]
}
}
}
}
},
"rejectedPlans" : [
{
"stage" : "PROJECTION_SIMPLE",
"transformBy" : {
"_id" : 0,
"name" : 1
},
"inputStage" : {
"stage" : "SHARDING_FILTER",
"inputStage" : {
"stage" : "FETCH",
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"name" : -1,
"size" : 1
},
"indexName" : "name_-1_size_1",
"isMultiKey" : false,
"multiKeyPaths" : {
"name" : [ ],
"size" : [ ]
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "forward",
"indexBounds" : {
"name" : [
"[\"Pepperoni\", \"Pepperoni\"]"
],
"size" : [
"[MinKey, MaxKey]"
]
}
}
}
}
}
]
},
{
"shardName" : "shard2",
"connectionString" : "shard2/node01:28010,node01:28011,node01:28012",
"serverInfo" : {
"host" : "node01",
"port" : 28010,
"version" : "5.0.8",
"gitVersion" : "c87e1c23421bf79614baf500fda6622bd90f674e"
},
"namespace" : "order.orders",
"indexFilterSet" : false,
"parsedQuery" : {
"name" : {
"$eq" : "Pepperoni"
}
},
"queryHash" : "3066FB64",
"planCacheKey" : "066D32F9",
"maxIndexedOrSolutionsReached" : false,
"maxIndexedAndSolutionsReached" : false,
"maxScansToExplodeReached" : false,
"winningPlan" : {
"stage" : "PROJECTION_SIMPLE",
"transformBy" : {
"_id" : 0,
"name" : 1
},
"inputStage" : {
"stage" : "SHARDING_FILTER",
"inputStage" : {
"stage" : "FETCH",
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"name" : 1
},
"indexName" : "name_1",
"isMultiKey" : false,
"multiKeyPaths" : {
"name" : [ ]
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "forward",
"indexBounds" : {
"name" : [
"[\"Pepperoni\", \"Pepperoni\"]"
]
}
}
}
}
},
"rejectedPlans" : [
{
"stage" : "PROJECTION_SIMPLE",
"transformBy" : {
"_id" : 0,
"name" : 1
},
"inputStage" : {
"stage" : "SHARDING_FILTER",
"inputStage" : {
"stage" : "FETCH",
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"name" : -1,
"size" : 1
},
"indexName" : "name_-1_size_1",
"isMultiKey" : false,
"multiKeyPaths" : {
"name" : [ ],
"size" : [ ]
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "forward",
"indexBounds" : {
"name" : [
"[\"Pepperoni\", \"Pepperoni\"]"
],
"size" : [
"[MinKey, MaxKey]"
]
}
}
}
}
}
]
}
]
}
},
"serverInfo" : {
"host" : "node01",
"port" : 4000,
"version" : "5.0.8",
"gitVersion" : "c87e1c23421bf79614baf500fda6622bd90f674e"
},
"serverParameters" : {
"internalQueryFacetBufferSizeBytes" : 104857600,
"internalQueryFacetMaxOutputDocSizeBytes" : 104857600,
"internalLookupStageIntermediateDocumentMaxSizeBytes" : 104857600,
"internalDocumentSourceGroupMaxMemoryBytes" : 104857600,
"internalQueryMaxBlockingSortMemoryUsageBytes" : 104857600,
"internalQueryProhibitBlockingMergeOnMongoS" : 0,
"internalQueryMaxAddToSetBytes" : 104857600,
"internalDocumentSourceSetWindowFieldsMaxMemoryBytes" : 104857600
},
"command" : {
"find" : "orders",
"filter" : {
"name" : "Pepperoni"
},
"projection" : {
"_id" : 0,
"name" : 1
},
"lsid" : {
"id" : UUID("de01c16c-67c9-480f-836d-4f3aa6cc6b75")
},
"$clusterTime" : {
"clusterTime" : Timestamp(1652852181, 1),
"signature" : {
"hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
"keyId" : NumberLong(0)
}
},
"$db" : "order"
},
"ok" : 1,
"$clusterTime" : {
"clusterTime" : Timestamp(1652852230, 2),
"signature" : {
"hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
"keyId" : NumberLong(0)
}
},
"operationTime" : Timestamp(1652852224, 1)
}
这时不需要 fetch, 可以直接从索引中获取数据。
db.orders.explain().find().sort( {name:1 ,size:-1}) ;
使用已创建索引的字段进行排序,能利用索引的顺序,不需要重新排序,效率高
db.orders.explain().find().sort( {name:1 ,size: 1}) ;
使用未创建索引的字段进行排序, 因为和创建索引时的顺序不一致,所以需要重新排序,效率低
如果需要更改某些字段上已经创建的索引,必须首先删除原有索引,再重新创建新索引,否则,新索引不会包含原有文档
db.collection.dropIndex()
使用索引名称删除索引
db.collection.dropIndex("name_1");
使用索引定义删除索引
db.collection.dropIndex({name:1,age:-1});
db.collection.createIndex(<keys>, <options>)
<options> 定义了创建索引时可以使用的一些参数,也可以指定索引的特性
索引的唯一性
索引的unique属性使MongoDB拒绝索引字段的重复值。除了唯一性约束,唯一索引和MongoDB其他索引功能上是一致的
db.collection.createIndex({filed:1},{unique:true});
如果文档中的字段已经出现了重复值,则不可以创建该字段的唯一性索引
如果新增的文档不具备加了唯一索引的字段,则只有第一个缺失该字段的文档可以被添加,索引中该键值被置为null。
复合键索引也可以具有唯一性,这种情况下,不同的文档之间,其所包含的复合键字段值的组合不可以重复。
索引的稀疏性
索引的稀疏属性可确保索引仅包含具有索引字段的文档的条目。索引会跳过没有索引字段的文档。可以将稀疏索引与唯一索引结合使用,以防止插入索引字段值重复的文档,并跳过索引缺少索引字段的文档。
准备数据:
db.sparsedemo.insertMany([{name:"xxx",age:19},{name:"zs",age:20}])
创建 唯一键,稀疏索引
db.sparsedemo.createIndex({name:1},{unique:true ,sparse:true});
如果同一个索引既具有唯一性,又具有稀疏性,就可以保存多篇缺失索引键值的文档了
db.sparsedemo.insertOne({name:"zs2w",age:20});
db.sparsedemo.insertOne({name:"zs2w2",age:20});
mongos> db.sparsedemo.insertOne({name:"zs2w2",age:20});
WriteError({
"index" : 0,
"code" : 11000,
"errmsg" : "E11000 duplicate key error collection: order.sparsedemo index: name_1 dup key: { name: \"zs2w2\" }",
"op" : {
"_id" : ObjectId("628487cd59656175ee81a817"),
"name" : "zs2w2",
"age" : 20
}
}) :
说明:如果只单纯的 唯一键索引,则 缺失索引键的字段,只能有一个
复合键索引也可以具有稀疏性,在这种情况下,只有在缺失复合键所包含的所有字段的情况下,文档才不会被加入到索引中。
索引的生存时间
针对日期字段,或者包含了日期元素的数组字段,可以使用设定了生存时间的索引,来自动删除字段值超过生存时间的文档。
构造数据:
db.ttl.insertMany( [ { name:"zhangsanss", age:19,tags:["00","It","SH"], create_time:new Date()} ] ); db.ttl.createIndex({ create_time: 1},{expireAfterSeconds:30 });
mongos> db.ttl.insertMany( [ { name:"zhangsanss", age:19,tags:["00","It","SH"], create_time:new Date()},{name:"lisi","age":20,tags:["222","苹果"]} ] );
mongos> db.ttl.find()
{ "_id" : ObjectId("6284891159656175ee81a81a"), "name" : "zhangsanss", "age" : 19, "tags" : [ "00", "It", "SH" ], "create_time" : ISODate("2022-05-18T05:50:09.818Z") }
{ "_id" : ObjectId("6284891159656175ee81a81b"), "name" : "lisi", "age" : 20, "tags" : [ "222", "苹果" ] }30秒后,这条记录就会被删除
mongos> db.ttl.find()
{ "_id" : ObjectId("6284891159656175ee81a81b"), "name" : "lisi", "age" : 20, "tags" : [ "222", "苹果" ] }
在create_time字段上面创建了一个生存时间是30s的索引
mongos> db.ttl.getIndexes()
[
{
"v" : 2,
"key" : {
"_id" : 1
},
"name" : "_id_"
},
{
"v" : 2,
"key" : {
"create_time" : 1
},
"name" : "create_time_1",
"expireAfterSeconds" : 30
}
]
复合键索引不具备生存时间的特性
当索引键是包含日期元素的数组字段时,数组中最小的日期将被用来计算文档是否已经过期
数据库使用一个后台线程来监测和删除过期的文档,删除操作可能会有一定的延迟