mongodb 索引、聚合操作

时间:2022-03-03 21:10:46

MongoDB索引

MongoDB的索引几乎与传统的关系型数据库索引一模一样。

创建索引的方法:

> db.trans.ensureIndex({card1: 1})
{
"createdCollectionAutomatically" : false,
"numIndexesBefore" : 1,
"numIndexesAfter" : 2,
"ok" : 1
}

对于同一个集合,同样的索引只需要创建一次,反复创建是徒劳的。
对某个键创建的索引会加速对该键的查询,然而,对于其他查询是没有帮助的,即便是查询中包含了被索引的键。例如: 以上索引对以下查询不会有任何优化

> db.trans.find({card1: "489592", "card2" : "8055"})

一定要创建查询中用到的所有键的索引。

> db.trans.ensureIndex({card1: 1, card2: 1})
{
"createdCollectionAutomatically" : false,
"numIndexesBefore" : 2,
"numIndexesAfter" : 3,
"ok" : 1
}

一组值为1或-1 的键表示索引创建的方向。
如果索引包含N个键,则对于前几个键的查询都会有帮助。比如有个索引{“a”: 1, “b”: 1, “c”: 1, …,”z”:1},实际上是有了{“a”: 1}、{“a”: 1, “b”:1}、{“a”: 1, “b”: 1, “c”: 1}等的索引。但是使用{“b”: 1}、{“a”:1 , “c”: 1}等索引的查询不会被优化,只有使用索引前部的查询才能使用该索引。

MongoDB的查询优化器会重排查询项的顺序,以便引用索引: 比如查询{“x”: “foo”, “y”: “bar”}的时候,已经有了{“y”: 1, “x”: 1}的索引,MongodB会自己找到并利用它。

创建索引的缺点就是每次插入、更新和删除都会产生额外的开销。这是因为数据库不但需要执行这些操作,还要将这些操作在集合的索引中标记。每个集合默认的最大索引个数为64个。

一定不要索引每个键,这会导致插入非常慢,还会占用很多空间,并且很可能对查询速度提升不大。

创建索引时要考虑如下问题

  1. 会做什么样的查询?其中那些需要索引
  2. 每个键的索引方向
  3. 如何应对扩展?有没有种不同的键的排列可以使常用数据更多的保留在内存中。

    索引内嵌文档中的键

> db.trans.ensureIndex({"comments.author": 1})
{
"createdCollectionAutomatically" : false,
"numIndexesBefore" : 3,
"numIndexesAfter" : 4,
"ok" : 1
}

为排序创建索引
如果对没有索引的键调用sort,MongoDB需要将所有数据提取到内存来排序,可能会是内存不够,如果对要有索引的键调用sort就可以按顺序加入到内存,就能排序大规模的数据。

索引名称
集合中每个索引都有一个字符串类型的名字,来标识索引,服务器通过这个名字操作索引。默认情况下,索引名称是keyname1_dir1_keyname2_dir2_…_keynameN_dirN这种形式,其中keynameX代表索引的键,dirX代表索引的方向。
自定义索引名字:

> db.trans.ensureIndex({date: 1}, {name: "aa"})
{
"createdCollectionAutomatically" : false,
"numIndexesBefore" : 4,
"numIndexesAfter" : 5,
"ok" : 1
}

唯一索引
唯一索引可以确保集合的每一个文档的指定键都有唯一值。

> db.trans.ensureIndex({date: 1, card1: 1}, {uniq: true})
{
"createdCollectionAutomatically" : false,
"numIndexesBefore" : 5,
"numIndexesAfter" : 6,
"ok" : 1
}

默认情况下,insert并不检查文档是否插入过了,所以,为了避免插入的文档中包含与唯一键重复的值,可能要用安全插入才能满足要求, 这样,在插入这样的文档时会看到存在重复键错误的提示。

消除重复

> db.trans.ensureIndex({card2: 1}, {"uniq": true, "dropDups": true})
{
"createdCollectionAutomatically" : false,
"numIndexesBefore" : 6,
"numIndexesAfter" : 7,
"ok" : 1
}

符合唯一索引,单个键的值可以相同,所有键组合起来不同就好。

使用explain和hint

explian会获得查询方面诸多有用的信息。只要对游标调用该方法,就可以得到查询细节。explain会返回一个文档,而不是游标本身。

> db.trans.find({"card1" : "621773"}).explain()
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "mongo_test.trans",
"indexFilterSet" : false,
"parsedQuery" : {
"card1" : {
"$eq" : "621773"
}
},
"winningPlan" : {
"stage" : "FETCH",
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"card1" : 1
},
"indexName" : "card1_1",
"isMultiKey" : false,
"direction" : "forward",
"indexBounds" : {
"card1" : [
"[\"621773\", \"621773\"]"
]
}
}
},
"rejectedPlans" : [
{
"stage" : "FETCH",
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"card1" : 1,
"card2" : 1
},
"indexName" : "card1_1_card2_1",
"isMultiKey" : false,
"direction" : "forward",
"indexBounds" : {
"card1" : [
"[\"621773\", \"621773\"]"
],
"card2" : [
"[MinKey, MaxKey]"
]
}
}
}
]
},
"serverInfo" : {
"host" : "upsmart",
"port" : 27017,
"version" : "3.0.3",
"gitVersion" : "b40106b36eecd1b4407eb1ad1af6bc60593c6105"
},
"ok" : 1
}

使用hint强制使用某个索引

> db.trans.find({"card1" : "621773"}).hint({card1: 1}).explain()
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "mongo_test.trans",
"indexFilterSet" : false,
"parsedQuery" : {
"card1" : {
"$eq" : "621773"
}
},
"winningPlan" : {
"stage" : "FETCH",
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"card1" : 1
},
"indexName" : "card1_1",
"isMultiKey" : false,
"direction" : "forward",
"indexBounds" : {
"card1" : [
"[\"621773\", \"621773\"]"
]
}
}
},
"rejectedPlans" : [ ]
},
"serverInfo" : {
"host" : "upsmart",
"port" : 27017,
"version" : "3.0.3",
"gitVersion" : "b40106b36eecd1b4407eb1ad1af6bc60593c6105"
},
"ok" : 1
}

索引管理

索引的元信息存储在每个数据库的system.index集合中。这是一个保留集合,不能对其插入或删除文档。操作只能通过ensureIndex或者dropIndexes进行。

建立索引可以在后台进行,同时正常处理请求,要是不包括background这个选项,数据库会阻塞建立索引期间的所有请求

> db.employees.ensureIndex({firstername: 1},{background: true})
{
"createdCollectionAutomatically" : false,
"numIndexesBefore" : 1,
"numIndexesAfter" : 2,
"ok" : 1
}

删除索引:

> db.runCommand({dropIndexes: "employees", index: "firstername_1"})
{ "nIndexesWas" : 2, "ok" : 1 }

地理空间索引

有一种查询变得越来越流行: 找到离当前位置最近的N个场所。MongoDB为坐标平面查询提供了专门的索引,称作地理空间索引。
地理空间索引同样可以由ensureIndex来创建,只不过参数不是1或者-1,而是”2d”;

> db.map.ensureIndex({"gps": "2d"})
{
"createdCollectionAutomatically" : false,
"numIndexesBefore" : 1,
"numIndexesAfter" : 2,
"ok" : 1
}

“gps”键的值必须是某种形式的一对值:一个包含两个元素的数组或是包含两个键的内嵌文档。

地理空间查询以两种方式进行,即普通查询或者使用数据库命令。

> db.map.find({gps: {$near: [20,10]}})
{ "_id" : ObjectId("57a47024ac1c6b4c5f65869f"), "gps" : [ 2, 39 ] }
{ "_id" : ObjectId("57a464faac1c6b4c5f65869e"), "gps" : [ 0, 100 ] }
> db.runCommand({geoNear: "map", near: [10,30] , limit:1})
{
"results" : [
{
"dis" : 12.041594578792296,
"obj" : {
"_id" : ObjectId("57a47024ac1c6b4c5f65869f"),
"gps" : [
2,
39
]
}
}
],
"stats" : {
"nscanned" : 1,
"objectsLoaded" : 1,
"avgDistance" : 12.041594578792296,
"maxDistance" : 12.041594578792296,
"time" : 0
},
"ok" : 1
}

MongoDB不但能找到靠近的一个点的文档,还能找到指定形状内的文档。做法就是将原来的” near"" within”。

MongoDB Enterprise > db.map.find({gps: {$within: {$box: [[2, 4], [20, 50]]}}})
{ "_id" : ObjectId("57a5b0d254330f9fbc585fa5"), "gps" : [ 3, 40 ] }

box center”来找到圆形内部的所有点,只不过参数变成圆心和半径。

MongoDB Enterprise > db.map.find({gps: {$within: {$center: [[0,0],50]}}})
{ "_id" : ObjectId("57a5b0d254330f9fbc585fa5"), "gps" : [ 3, 40 ] }

聚合

MongoDB除了基本的查询功能,还提供了很多强大的聚合工具,其中简单的可计算集合中的文档个数,复杂的可利用MapReduce做复杂的数据分析。
count
count是最简单的聚合工具,返回集合中的文档个数

> db.map.count()
4
> db.map.find().count()
4

distinct
distinct用来找出给定键的所有不同的值。使用时必须指定集合和键。

MongoDB Enterprise > db.runCommand({distinct: "foo", key: "age"})
{
"waitedMS" : NumberLong(0),
"values" : [
10
],
"stats" : {
"n" : 5,
"nscanned" : 0,
"nscannedObjects" : 5,
"timems" : 0,
"planSummary" : "COLLSCAN"
},
"ok" : 1

group
group先选定分组依据的键,而后Mongodb就会将集合依据选定键值的不同分成若干组,然后,可以通过聚合每一组内的文档,产生一个结果文档。

MongoDB Enterprise > db.runCommand({group: {
... ns: "foo",
... key: "name",
... initial: {age: 0},
... $reduce: function(doc, prev) {
... if(doc.age > prev.age) {
... prev.age = doc.age
... }
... }
... }})
{
"waitedMS" : NumberLong(0),
"retval" : [
{
"age" : 10
}
],
"count" : NumberLong(6),
"keys" : NumberLong(1),
"ok" : 1
}

还可以在group中添加condition过滤一些文档。

> db.runCommand({group: { ns: "day_log", key: {"date": true,"
ieie"
: true}, initial: {"count": 0}, $reduce: function(doc, prev) { prev.count
+= 1; }, condition: {date: {$gt: "2014/11/1"}} }})

使用完成器用于精简从数据库查到用户的数据。

> db.runCommand({group: { ns: "day_log", key: {"date": true},
initial: {"ieies": {}}, $reduce: function(doc, prev) { if(doc.ieie in prev.ieie
s){prev.ieies[doc.ieie]++;} else { prev.ieies[doc.ieie] = 1;} }, condition: {dat
e: {$gt: "2014/11/1"}}, finalize: function(prev) { var max = 0; for ( i in pre
v.ieies) { if (prev.ieies[i] > max) { prev.ieie = i; max = prev.ieies
[i]; } } delete prev.ieies; }}})

有时候分组所依据的条件非常复杂,可以将函数作为键使用

MongoDB Enterprise > db.runCommand({group: { ns: "day_log", $keyf: function(x){
return {date: x.date.substring(0,9)}; }, initial: {"ieies": {}}, $reduce: functi
on(doc, prev) { if(doc.ieie in prev.ieies){prev.ieies[doc.ieie]++;} else { prev.
ieies[doc.ieie] = 1;} }, condition: {date: {$gt: "2014/11/1"}}, finalize: functi
on(prev) { var max = 0; for ( i in prev.ieies) { if (prev.ieies[i] > max) {
prev.ieie = i; max = prev.ieies[i]; } } delete prev.ieies; }}})

注意function返回的必须是一个对象。
MapReduce

使用MapReduce的代价就是速度: group不是很快,Mapreduce更慢,绝不要用在“实时”环境中。要作为后台任务来运行Mapreduce,将创建一个保存结果的集合,可以对这个集合进行实时查询。

> map = function() { emit(this.date, this.ieie); }
function () { emit(this.date, this.ieie); }
> reduce = function(key, emits) { value_map= new Map(); count
= 0; for (i in emits) { if(value_map[i]==null) { value_map.put(i,0); count++
;} } return {"count": count}; }
> mr = db.runCommand({mapreduce: "day_log", "map": map,"reduc
e"
: reduce, "out": "sss"})
{
"result" : "sss",
"timeMillis" : 335,
"counts" : {
"input" : 2303,
"emit" : 2303,
"reduce" : 204,
"output" : 61
},
"ok" : 1
}

有时候不包含out时会报 “‘out’ has to be a string or an object”的错误。
reduce一定要能反复调用,不论是映射环节还是前一个简化环节,所以reduce返回文档必须能做为reduce的第二个参数的一个元素。reduce应该能处理emit文档和reduce结果的各种组合。

mapreduce可选的键:

  • “finalize”: 函数
    将reduce结果发送给这个键,这是处理的最后一步。
  • “query”: 文档
    会在发往map函数前,先用指定条件过滤文档
  • “sort”:文档
    会在发往map前先给文档排序(与limit一同使用非常有用)
  • “limit”:整数
    发往map函数的文档数量上限。
  • “scope”: 文档
    JavaScript代码中要用到的变量。
  • “verbose”: 布尔
    是否产生更加详尽的服务器日志。