MongoDB 高阶技术之聚合操作及索引
MongoDB高阶技术
1. 聚合操作
聚合操作处理数据并返回结果集(如 求和、去重等)。聚合操作可以操作多个文档,然后对数据进行处理,返回结果集。
聚合操作主要分为三个方法:
- 处理每个文档并向每个输入文档发射一个或多个对象的map阶 段,以及reduce组合map操作的输出阶段。
- 作用在一个或几个集合上;
- 对集合中的数据进行的一系列运算;
- 将这些数据转化为期待的数据形式;
- $group 分组,_id 为null 意思是 只有一个组,count:{$sum:1} 根据分组结果进行求和
- pop:{$sum:”$favCount”} 根据收藏数进行求和
- avg:{$avg:”$favCount”} 根据收藏数求平均值
- 可以将数组拆分成单个文档。
- $match,先过滤掉 小于或等于0的文档
- $unwind,由于需要统计每个标签的收藏数,但是标签是个数组,所以需要通过unwind 进行拆分
- $group,通过分组查询,根据tag进行分组,$sum 根据favCount进行累加
- $sort排序
- 根据美国的州和城市来做一系列聚合操作实验,所以我们要先导入 州和城市的数据。
- $group按state进行分组查询后,$sum根据pop字段进行累加
- $match 匹配total 总人口超过1000w的州
- $group按state、city 进行分组查询后,$sum根据pop字段进行累加
- $group 按上一阶段的结果的state进行分组,$avg 分组后 求平均值
- $sort 根据stateAvg 进行降序
- 按照索引包含字段数,可以分为 单键索引和复合索引(组合索引)
-
按照索引字段的类型,可以分为主键索引和非主键索引
- 主键索引又叫 聚簇索引,索引节点 包含了完整的数据记录
- 非主键索引又叫 稀疏索引,索引节点 只包含了 指向 聚簇索引的地址指针,而不包含数据记录
- 按照索引的特性不同,又可以分为唯一索引、稀疏索引、文本索引、地理空间索引等
-
key 为要创建的索引, 1 - 升序, 2 - 降序
-
options 可选参数
Parameter Description background Boolean 建索引过程会阻塞其它数据库操作,background可 指定以异步方式创建索引 unique Boolean 建立的索引是否唯一。指定为true创建唯一索引。默认值为false. string 索引的名称。如果未指定,MongoDB的通过连接索引的字段名和排序顺序生成一个索引名称。 sparse Boolean 对文档中不存在的字段数据不启用索引;这个参数需要特别注意,如果设置为true的话,在索引字段中不会查询出不包含对应字段的文档.。默认值为 false. expireAfterSeconds integer 指定一个以秒为单位的数值,完成 TTL设定,设定集合的生存时间。 index version 索引的版本号。默认的索引版本取决于mongod创建索引时运行的版本。 weights document 索引权重值,数值在 1 到 99,999 之间,表示该索引相对于其他索引字段的得分权重。 default_language string 对于文本索引,该参数决定了停用词及词干和词器的规则的列表。 默认为英语 language_override string 对于文本索引,该参数指定了包含在文档中的字段 名,语言覆盖默认的language,默认值为 language.1
2
3
4
5
6
7
8
9
10
11# 以后台的形式创建索引
> db.books.createIndex({title:1},{background:true})
{
"createdCollectionAutomatically" : false,
"numIndexesBefore" : 1,
"numIndexesAfter" : 2,
"ok" : 1
}
# 创建唯一索引,注意:如果存在非唯一值,会创建失败
> db.books.createIndex({type:1},{unique:true})查看索引
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35# 查看所有的索引
> db.books.getIndexes()
[
{
"v" : 2,
"key" : {
"_id" : 1
},
"name" : "_id_"
},
{
"v" : 2,
"key" : {
"title" : 1
},
# 索引名称
"name" : "title_1",
"background" : true
}
]
# 查看索引key值
> db.books.getIndexKeys()
[ { "_id" : 1 }, { "title" : 1 } ]
# 查看索引占用空间,后面的参数 是可选参数,0 - 显示所有索引占用空间 , 1 - 显示所有索引占用空间
> db.books.totalIndexSize(0)
45056
# 显示所有索引占用空间
> db.books.totalIndexSize(1)
_id_ 24576
title_1 20480
45056查看索引是否生效
- 我们可以通过explain() 执行计划去查看是否走了索引。
-
$near 查询操作符,根据 经纬度查询附近数据,返回数据结果会按距离排序。
-
$geometry操作符指定地理空间对象
- type=Point表示地理坐标点(经纬度)
- coordinates 用户所在经纬度地址
-
$maxDistance 最大距离,单位是米,如 10000米内
全文索引(Text Indexes)
MongoDB支持简易的分词检索,且官方仅支持英文检索, 不支持中文分词。如果项目中需要用到中文分词,建议还是使用elasticsearch
注意: 一个集合只能创建一个 全文索引 ,创建多个会出错。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22db.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" }
]
)
# 插入成功
BulkWriteResult({
"writeErrors" : [ ],
"writeConcernErrors" : [ ],
"nInserted" : 5,
"nUpserted" : 0,
"nMatched" : 0,
"nModified" : 0,
"nRemoved" : 0,
"upserted" : [ ]
})创建复合索引 - 全文索引
1
2
3
4
5
6
7
8# 创建复合 全文检索索引,key 是字段名,value 是固定格式text,代表是全文索引
> db.stores.createIndex({name:"text",description:"text"})
{
"createdCollectionAutomatically" : false,
"numIndexesBefore" : 1,
"numIndexesAfter" : 2,
"ok" : 1
}1、key 是要创建索引的字段名,value 是固定格式text,代表是全文索引
2、创建了全文索引后,会根据字段名进行分词 进行倒排索引排序,后续使用$text查询时 会直接从索引树里面查找
通过$text操作符 查询数据中包含 “java”、”coffee”、”shop”的文档
1
2
3
4
5
6
7# $text 全文检索操作符,查询的关键字用空格隔开,之间的关系是 or,查询包含 java or coffe or shop的文档
> 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" }
{ "_id" : 5, "name" : "Java Shopping", "description" : "Indonesian goods" } - $text 全文检索操作符,关键字用空格隔开,之间的关系是 or,查询包含 java 或 coffe 或 shop的文档
- 由于创建是复合索引,关键字会在 name和description 里面进行查询
- 通配符索引 属于稀疏索引,无法索引空字段 ,所以只能查询有字段的文档,不存在字段的文档查询不出来。
- 通配符索引是为 文档或数组 内容本身生成的索引 ,而并非 文档/数组 字段本身。所以无法 精确匹配到文档/数组本身。
- 等式表达式(例如:field: value或使用$eq操作符)
- $exists: true
- $gt, $gte, $lt , $lte
- $type
- 顶层的$and
- 插入重复name,age 满足了 部分索引创建时的匹配条件,所以会触发 唯一约束 ,无法正常插入
- 插入重复name,age 不满足 匹配条件,则无法触发唯一约束,意外的插入了数据,这不是我们想要的结果
- TTL索引只支持 单键索引(单个字段),并且必须是非 _id字段。
- TTL索引过期后不会马上删除,会有60秒的时间差。
- TTL删除时 用的是remove命令,如果数据量大的话会对CPU、磁盘产生一定的压力。
-
为每个集合创建一个合适的索引
- 对于数据量比较大的集合时,就 要合理的创建索引,可以有效的提高查询效率。 如果不创建索引的话,mongoDB会把所有的文档从硬盘读取到内存,这对服务器的压力是很大的。
-
尽量用复合索引来完成查询,而不是用交叉索引
- 交叉索引:每个字段都创建单键索引,然后查询时 这些字段都用上去查询就叫交叉索引查询。
- 尽量 通过2-5个复合索引来解决 90%的查询命令,减少单键索引的使用 ,因为单键索引 利用率不高
-
使用复合索引时,要满足 最左前缀匹配原则 ,否则无法走索引
-
使用复合索引时,查询时 等值匹配放在前,范围查询放后面
- 先利用等值匹配筛选结果后完成排序,才通过范围查询,效率比较高
-
尽可能使用 覆盖索引
- 覆盖索引并非真正的索引,而是 你需要什么数据,你就返回什么数据,这样 尽可能的不回表在去取数据,等同于MySQL的覆盖索引
-
创建索引时 尽可能的后台运行
- 在大数据量的情况下去创建索引时会导致系统阻塞,建议使用后台方式创建索引
-
避免设计过长的 数组索引
- 数组索引的内容较多,存储的时候需要更多的空间,影响索引树的查询效率
-
更新频繁的字段慎用索引
- 如果字段更新频繁的话,索引树需要频繁的更新信息
-
对于超长字符串的字段慎用索引
- 不建议的原因同第七点一样
-
是否走索引
- winningPlan.inputStage.stage = IXSCAN 代表走索引,COLLSCAN 代表走全表扫描
-
语句执行时间、索引扫描次数
- executionStats.executionTimeMillis 语句执行时间
- executionStats.totalKeysExamined 索引扫描次数
- executionStats.totalDocsExamined 文档扫描次数
- verbose 为可选参数,默认为queryPlanner。
-
COLLSCAN 全表扫描
- 如果在大数据量的集合下,尽量使用索引扫描,不要走 全集合扫描。
注意:聚合管道想要查看执行计划explain,需要在 聚合函数之前先调用explain。其他的命令则是在最末尾加上explain()。
如:
db.books.explain().aggregate([ xxx ])
常规语法:
1
db.collection.find().explain(<verbose>)
queryPlanner
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58# explain不写参数的话,默认就是queryPlanner
> db.books.find({title:"book-2"}).explain()
# 执行计划返回结果
{
"queryPlanner" : {
# 执行计划版本号
"plannerVersion" : 1,
# 查询的集合名称
"namespace" : "appDb.books",
# 是否使用索引,这里保留个疑问,因为下面明明使用了索引,但是这里却是为false,初步怀疑该字段的真实性和有效性
"indexFilterSet" : false,
"parsedQuery" : {
# 查询条件
"title" : {
"$eq" : "book-2"
}
},
"queryHash" : "6E0D6672",
"planCacheKey" : "B1CDA929",
# 最佳执行计划
"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-2\", \"book-2\"]"
]
}
}
},
"rejectedPlans" : [ ]
},
"serverInfo" : {
"host" : "cvm52851",
"port" : 27017,
"version" : "4.4.26",
"gitVersion" : "acdc463fe60bdc85aeced25297041b4051a0fc33"
},
"ok" : 1
}executionStats
executionStats 模式的返回信息中包含了 queryPlanner 模式的所有字段,并且还包含了更为详尽的最佳执行计划执行情况。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101> db.books.find({title:"book-2"}).explain("executionStats")
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "appDb.books",
"indexFilterSet" : false,
"parsedQuery" : {
"title" : {
"$eq" : "book-2"
}
},
"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-2\", \"book-2\"]"
]
}
}
},
"rejectedPlans" : [ ]
},
"executionStats" : {
"executionSuccess" : true,
"nReturned" : 1,
"executionTimeMillis" : 2,
"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-2\", \"book-2\"]"
]
},
"keysExamined" : 1,
"seeks" : 1,
"dupsTested" : 0,
"dupsDropped" : 0
}
}
},
"serverInfo" : {
"host" : "cvm52851",
"port" : 27017,
"version" : "4.4.26",
"gitVersion" : "acdc463fe60bdc85aeced25297041b4051a0fc33"
},
"ok" : 1
}allPlansExecution
allPlansExecution返回的信息包含 executionStats 模式的内容,且包含allPlansExecution,这是最详细的执行计划。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19> db.books.find({title:"book-2"}).explain("allPlansExecution")
# 返回结果,这里我故意只显示allPlansExecution部分,其他的我没写进来,因为信息实在太多了
"allPlansExecution" : [
{
"nReturned" : <int>,
"executionTimeMillisEstimate" : <int>,
"totalKeysExamined" : <int>,
"totalDocsExamined" :<int>,
"executionStages" : {
"stage" : <STAGEA>,
"nReturned" : <int>,
"executionTimeMillisEstimate" : <int>,
...
}
}
},
...
]stage状态
COLLSCAN IXSCAN FETCH 根据索引检索指定文档 SHARD_MERGE 将各个分片返回数据进行合并 在内存中进行了排序 LIMIT 使用limit限制返回数 使用skip进行跳过 IDHACK 对_id进行查询 SHARDING_FILTER 通过mongos对分片数据进行查询 COUNTSCAN count不使用Index进行count时的stage返回 COUNT_SCAN count使用了Index进行count时的stage返回 SUBPLA 未使用到索引的$or查询的stage返回 使用全文索引进行查询时候的stage返回 PROJECTION 限定返回字段时候stage的返回执行计划的 stage状态尽量不要出现以下几种情况 :
2)索引操作
创建索引
1
db.collection.createIndex(keys, options)
当创建索引后,执行explain的话,发现成功走了索引扫描。
删除索引
1
2
3
4
5
6
7
8
9
10
11
12# 删除集合的 指定索引,value 是索引名称
> db.books.dropIndex("title_1")
{ "nIndexesWas" : 2, "ok" : 1 }
# 删除集合中的所有索引
> db.books.dropIndexes()
{
"nIndexesWas" : 1,
"msg" : "non-_id indexes dropped for collection",
"ok" : 1
}注意:删除指定索引时,指定的value 是索引对应的名称(name)
3)索引类型
单键索引
1
2
3
4
5
6
7
8# 以后台的形式创建索引
> db.books.createIndex({title:1},{background:true})
{
"createdCollectionAutomatically" : false,
"numIndexesBefore" : 1,
"numIndexesAfter" : 2,
"ok" : 1
}使用任务计划查询看看是否使用了索引
复合索引(联合索引)
复合索引是多个字段组合而成的索引,可以参考MySQL的复合索引。查询的时候要遵循 最左匹配原则,因为创建索时 是按最左的字段排序的,如果 不遵循最左原则的话,会无法走索引。
1
2# 创建复合索引,type和favCount 都以升序排序
> db.books.createIndex({type:1,favCount:1})最左原则查询
1
2
3# 符合最左匹配原则查询
> db.books.find({type:"sociality",favCount:{$gt:20}}).explain()创建联合索引顺序:type,favCount,而查询的时候 直接跳过了 type,自然无法走索引
多键索引(Multikey Index)
1
2
3
4
5
6
7
8
9
10# 批量插入多条测试数据
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 ] }
])
{ "acknowledged" : true, "insertedIds" : [ 5, 6, 7, 8, 9 ] }创建多键索引
1
2
3
4
5
6
7
8# 创建多键索引
> db.inventory.createIndex({ratings:1})
{
"createdCollectionAutomatically" : false,
"numIndexesBefore" : 1,
"numIndexesAfter" : 2,
"ok" : 1
}注意:多键索引并非复合索引,不要混淆概念。 符合索引是联合多个字段创建的,而多键索引 是在数组的属性上创建索引。
创建 复合索引(含多键索引)
1
2
3
4
5
6
7
8# 创建复合索引的话,复合索引只能允许存在一个 多键索引
> db.inventory.createIndex({item:1,ratings:1})
{
"createdCollectionAutomatically" : false,
"numIndexesBefore" : 2,
"numIndexesAfter" : 3,
"ok" : 1
}注意:复合索引只能允许存在一个 多键索引。
嵌入文档的索引数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31# 批量插入多条嵌套数组的数据
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 }
]
}
])在嵌套数组上 创建 多键索引
1
2
3
4
5
6
7
8# 在嵌套数组上 创建 多键索引
> db.inventory.createIndex( { "stock.size": 1, "stock.quantity": 1 } )
{
"createdCollectionAutomatically" : false,
"numIndexesBefore" : 3,
"numIndexesAfter" : 4,
"ok" : 1
}1
2# 查询 inventory集合,条件:stock.size = S 并且 quantity 大于25
> db.inventory.find({"stock.size":"S","stock.quantity":{$gt:25}})地理空间索引(Geospatial Index)
在互联网中,地理空间检索是非常常用的,而MongoDB地理空间索引(2dsphereindex)就是专门用于实现位置检索的一种特殊索引。
如何实现查询 附近商家
1
2
3
4
5
6
7
8
9# 插入商家和定位信息
db.restaurant.insert({
restaurantId: 0,
restaurantName:"兰州牛肉面",
location : {
type: "Point",
coordinates: [ -73.97, 40.77 ]
}
})创建一个地理空间索引 - 2d
1
2
3
4
5
6
7
8# 创建 地里空间索引,字段 location ,类型2dsphere(地理空间索引)
> db.restaurant.createIndex({location : "2dsphere"})
{
"createdCollectionAutomatically" : false,
"numIndexesBefore" : 1,
"numIndexesAfter" : 2,
"ok" : 1
}根据经纬度 查询附近10000米的商家信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14db.restaurant.find({
location:{
$near:{
$geometry:{
type:"Point",
coordinates:[-73.88, 40.78]
},
$maxDistance:10000
}
}
})
# 查询结果
{ "_id" : ObjectId("659caea6e268f34c178d2071"), "restaurantId" : 0, "restaurantName" : "兰州牛肉面", "location" : { "type" : "Point", "coordinates" : [ -73.97, 40.77 ] } }通过执行计划可以看出,成功走了索引
Hash索引(Hashed Indexes)
哈希索引只支持精确匹配,不支持范围查询。如果确定该索引只用于 等值查询,可以创建哈希索引来提升查询效率。
1
> db.users.createIndex({username : 'hashed'})
通配符索引(Wildcard Indexes)
由于MongoDB文档可以动态改变,存在一些不可预知的字段结构,为了提高这部分字段的查询速度,可以为其创建 通配符索引。
插入准备数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38db.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"
}
])
# 插入结果
BulkWriteResult({
"writeErrors" : [ ],
"writeConcernErrors" : [ ],
"nInserted" : 3,
"nUpserted" : 0,
"nMatched" : 0,
"nModified" : 0,
"nRemoved" : 0,
"upserted" : [ ]
})创建通配符索引
1
2
3
4
5
6
7
8# 规格参数,规格参数是动态可变的,所以我们为其创建通配符 product_attributes.$** 代表这个字段后面的字段都会被匹配到
> db.products.createIndex( { "product_attributes.$**" : 1 } )
{
"createdCollectionAutomatically" : false,
"numIndexesBefore" : 1,
"numIndexesAfter" : 2,
"ok" : 1
}product_attributes.$**:代表这个字段后面的字段都会被匹配到
通过匹配符索引 查询 length大于50的数据
1
2
3
4
5# 查询length 大于50的数据,由于是product_attributes字段下的,会被通配符索引匹配到
> db.products.find({"product_attributes.size.length":{$gt:50} })
# 查询结果
{ "_id" : ObjectId("659ccf6152ef6967235d5313"), "product_name" : "Spy Coat", "product_attributes" : { "material" : [ "Tweed", "Wool", "Leather" ], "size" : { "length" : 72, "units" : "inches" } } }通过匹配符索引 查询 material = Tweed 的数据
1
2
3
4> db.products.find({"product_attributes.material": "Tweed" })
# 查询结果
{ "_id" : ObjectId("659ccf6152ef6967235d5313"), "product_name" : "Spy Coat", "product_attributes" : { "material" : [ "Tweed", "Wool", "Leather" ], "size" : { "length" : 72, "units" : "inches" } } }通过匹配符索引 查询 size.units = inches 的数据
1
2
3
4> db.products.find({"product_attributes.size.units": "inches" })
# 查询结果
{ "_id" : ObjectId("659ccf6152ef6967235d5313"), "product_name" : "Spy Coat", "product_attributes" : { "material" : [ "Tweed", "Wool", "Leather" ], "size" : { "length" : 72, "units" : "inches" } } }4)索引属性
唯一索引(Unique Indexes)
在生产环境中,重复的脏数据必然带来一定的麻烦,为此对于一些要求 唯一的字段,如 订单号,用户名等 可以给他设置成 唯一索引,确保该字段在集合中是唯一的。
插入准备数据
1
2
3
4
5
6
7
8> db.user.insertMany([{name:"张三"},{name:"李四"}])
{
"acknowledged" : true,
"insertedIds" : [
ObjectId("659ceff23426724fbba207a6"),
ObjectId("659ceff23426724fbba207a7")
]
}创建唯一索引
1
2
3
4
5
6
7
8# 创建唯一索引的属性是 unique:true
> db.user.createIndex({name:1},{unique:true})
{
"createdCollectionAutomatically" : false,
"numIndexesBefore" : 1,
"numIndexesAfter" : 2,
"ok" : 1
}再次插入 重复的数据
创建复合唯一索引
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18# 准备数据
> db.user.insertMany([{name:"王五",age:23,interest:["basketball","computer"]}])
{
"acknowledged" : true,
"insertedIds" : [
ObjectId("659cf3223426724fbba207ab")
]
}
# 创建唯一索引的属性是 unique:true
> db.user.createIndex({name:1,age:20},{unique:true})
{
"createdCollectionAutomatically" : false,
"numIndexesBefore" : 2,
"numIndexesAfter" : 3,
"ok" : 1
}部分索引(Partial Indexes)
创建索引:创建部分索引时,会根据 匹配条件进行匹配,只有 满足条件的数据才会建立索引。
使用索引查询:使用部分索引查询, 只有满足 创建索引时的匹配条件的情况下才会走 部分索引 。如果强行要求他走部分索引的话,则会出现 丢失数据的情况,因为索引树中 没有 不满足条件的数据。
好处:节省索引树空间和硬盘存储空间,具有更低的创建索引和维护索引成本。
创建部分索引
1
2
3
4
5
6
7
8
9
10
11
12> db.user.createIndex(
{ name:1 },
{ partialFilterExpression: { age: { $gt: 5 } } }
)
# 创建结果
{
"createdCollectionAutomatically" : false,
"numIndexesBefore" : 1,
"numIndexesAfter" : 2,
"ok" : 1
}partialFilterExpression选项接受指定过滤条件:
查询的时候使用部分索引
1
2
3
4> db.user.find({name:"王五",age:23})
# 查询结果
{ "_id" : ObjectId("659cf3223426724fbba207ab"), "name" : "王五", "age" : 23, "interest" : [ "basketball", "computer" ] }在查询的时候,如果想使用部分索引的话, 必须满足 创建索引时匹配条件 ,age 大于5 。
稀疏索引(Sparse Indexes)
稀疏索引,只会对存在的字段创建索引 。查询数据时,走稀疏索引只会查找具有索引的字段。
使用稀疏索引进行查询时,可能会丢失部分数据,因为只对存在的字段创建索引。
如果 使用稀疏索引会导致查询和排序 结果集不完整,mongoDB默认不会使用该索引 。除非使用hint()强行指定走索引,但是这样会丢失部分数据。
查询现有数据
1
2
3
4
5
6> db.user.find()
{ "_id" : ObjectId("659d0eb0ba5c32556ca0c2f1"), "name" : "张三", "age" : 2 }
{ "_id" : ObjectId("659d0eb5ba5c32556ca0c2f2"), "name" : "李四", "age" : 4 }
{ "_id" : ObjectId("659d0ebaba5c32556ca0c2f3"), "name" : "王五", "age" : 26 }
{ "_id" : ObjectId("659d0ed1ba5c32556ca0c2f5"), "name" : "王五", "age" : 5 }
{ "_id" : ObjectId("659d124cba5c32556ca0c2f6"), "name" : "张三", "age" : 6 }创建稀疏索引
1
2
3
4
5
6
7
8# 为age创建稀疏索引
> db.user.createIndex( { age: 1 } , { sparse: true } )
{
"createdCollectionAutomatically" : false,
"numIndexesBefore" : 1,
"numIndexesAfter" : 2,
"ok" : 1
}当前的集合中,所有的文档age都是存在数据的,此时去查询必然会走稀疏索引。
案例二:不走稀疏索引
如果在集合中新增几条文档,文档里面不包含age的话,那这几条文档不会走稀疏索引。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22# 新增数据,没有age(稀疏索引字段)
> db.user.insert({name:"赵六"})
WriteResult({ "nInserted" : 1 })
# 查询user所有文档,不会走稀疏索引,如果强行走稀疏索引,必然会丢失部分数据
> db.user.find()
{ "_id" : ObjectId("659d0eb0ba5c32556ca0c2f1"), "name" : "张三", "age" : 2 }
{ "_id" : ObjectId("659d0eb5ba5c32556ca0c2f2"), "name" : "李四", "age" : 4 }
{ "_id" : ObjectId("659d0ebaba5c32556ca0c2f3"), "name" : "王五", "age" : 26 }
{ "_id" : ObjectId("659d0ed1ba5c32556ca0c2f5"), "name" : "王五", "age" : 5 }
{ "_id" : ObjectId("659d124cba5c32556ca0c2f6"), "name" : "张三", "age" : 6 }
{ "_id" : ObjectId("659d2f1c4baf2e0ec00f658c"), "name" : "赵六" }
# 对集合进行排序
> db.user.find().sort({age: -1})
{ "_id" : ObjectId("659d0ebaba5c32556ca0c2f3"), "name" : "王五", "age" : 26 }
{ "_id" : ObjectId("659d124cba5c32556ca0c2f6"), "name" : "张三", "age" : 6 }
{ "_id" : ObjectId("659d0ed1ba5c32556ca0c2f5"), "name" : "王五", "age" : 5 }
{ "_id" : ObjectId("659d0eb5ba5c32556ca0c2f2"), "name" : "李四", "age" : 4 }
{ "_id" : ObjectId("659d0eb0ba5c32556ca0c2f1"), "name" : "张三", "age" : 2 }
{ "_id" : ObjectId("659d2f1c4baf2e0ec00f658c"), "name" : "赵六" }排序结果:
强行指定使用索引
强行指定走索引的话,必然会丢失部分数据,如下图所示,丢失了一条 “赵六” 的数据。因为 “赵六” 的文档没有 age字段。
1
2
3
4
5
6
7> db.user.find().sort({age: -1}).hint({age:1})
{ "_id" : ObjectId("659d0ebaba5c32556ca0c2f3"), "name" : "王五", "age" : 26 }
{ "_id" : ObjectId("659d124cba5c32556ca0c2f6"), "name" : "张三", "age" : 6 }
{ "_id" : ObjectId("659d0ed1ba5c32556ca0c2f5"), "name" : "王五", "age" : 5 }
{ "_id" : ObjectId("659d0eb5ba5c32556ca0c2f2"), "name" : "李四", "age" : 4 }
{ "_id" : ObjectId("659d0eb0ba5c32556ca0c2f1"), "name" : "张三", "age" : 2 }隐藏索引(Hidden Indexes)
隐藏索引并不是一个真正意义上的索引,而是索引的一个属性,类似于 停用该索引。
隐藏索引后,查询数据时就不会走该索引。该功能更多的是临时隐藏索引,停止使用该索引,以后需要启用的时候在 取消隐藏。
创建隐藏索引
1
2
3
4
5
6
7
8# 创建隐藏索引,属性:hidden:true
> db.log.createIndex({ logMessage : 1 },{ hidden: true });
{
"createdCollectionAutomatically" : false,
"numIndexesBefore" : 1,
"numIndexesAfter" : 2,
"ok" : 1
}3. explain执行计划
MongoDB提供了explain命令, 可以让我们直观的看到 查询命令的执行计划,根据实际情况来做出相应优化。
执行计划最主要看的是几个属性:
1)单一聚合
MongoDB提供三种聚合函数,使用简单但是缺少灵活性和功能性。
db.collection.estimatedDocumentCount() 忽略查询条件,返回集合中所有文档的总数量 db.collection.count() 可以根据条件在集合中进行求和 db.collection.distinct() 可以根据条件在集合中进行去重,并通过数组的形式返回数据单一聚合实验
1 |
#检索books集合中所有文档的计数 |
2)聚合管道
MongoDB 聚合框架(Aggregation Framework)是一个计算框架,它可以:
聚合框架更类似于 SQL中的GROUP BY、LEFT JOIN 、AS 别名等功能。
管道(Pipeline)和阶段(Stage)
整个聚合运算过程称为管道(Pipeline),它是由多个阶段(Stage)组成的,每个阶段都可以对文档进行运算,并将文档输送给下一个阶段。
1 |
var tags = ["nosql","mongodb","document","developer","popular"]; |
导入后的数据:
$project
1、投影操作, 将原始字段投影成指定名称, 如将集合中的 title 投影成 name,可以理解为 SQL中的 as 别名
2、可以返回 指定的字段(或者说 排除不想要的字段),类似于SQL的 select xxx
1 |
> db.books.aggregate([{$project:{name:"$title"}}]) |
指定想要返回的字段(或者说 排除不想返回的字段):
1 |
# value为0的代表隐藏,为1 代表显示 。除了_id外,其他字段如果不写的话默认都是隐藏。 |
$match
match 类似于SQL的where, 建议把match放到管道最前端 ,过滤后的结果在进行处理,可以有效的提高处理效率。
1 |
# 只查询type = technology的文档 |
$count
1 |
> db.books.aggregate([{$match:{type:"technology"}},{$count:"type_count"}]) |
根据查询条件 type = technology 进行求和,并以 type_count字段返回求和结果
管道内,第一阶段 $match 会先匹配数据,并传给下一阶段,第二阶段 $count 会根据结果集 进行求和
$group
根据指定的表达式条件进行分组,并将分组后的结果传给下一阶段。
accumulator操作符
类比 sql $first 返回每组第一个文档,如果有排序,按照排序,如果没有按照默认的存储的顺序的第一个文档。 limit 0,1 $last 返回每组最后一个文档,如果有排序,按照排序,如果没有按照默认的存储的顺序的最后个文档。 根据分组,获取集合中所有文档对应值得最大值。 根据分组,获取集合中所有文档对应值得最小值。 $push 将指定的表达式的值添加到一个数组中。 $addToSet 将表达式的值添加到一个集合中(无重复值,无序)。 java 的set $stdDevPop 返回输入值的总体标准偏差(population standard deviation) $stdDevSamp 返回输入值的样本标准偏差(the sample standard deviation)$group允许最大内存100M,如果超过则会报错,如果要处理超大数据的话,可以设置 allowDiskUse = true,$group操作产生的数据会写入到硬盘中。
book的数量,收藏总数和平均值
1 |
> db.books.aggregate([ |
统计每个作者的book收藏总数
1 |
# 先根据author.name 作者名称进行分组,分组之后的结果 根据favCount 通过$sum函数进行求和 |
统计每个作者的每本book的收藏数
1 |
# 先根据author.name和title 进行分组(每个作者每本书),然后 进行求和,求和的结果集名称 pop |
每个作者的book的type合集
1 |
# 根据author.name进行分组,下一阶段 使用$addToSet 把type类型 添加到一个set集合(无序 不重复) |
$group 根据作者名称进行分组,把分组的结果 传递给下一阶段
$addToSet 将type类型添加到set集合
$unwind
将作者 xx001的book的tag数组拆分成单个文档
1 |
> db.books.aggregate([ |
tag 标签参数是个数组,里面存了多个标签,可以通过unwind 拆分成单个文档,其他参数则按照原有的来。
查询出每个作者的book的tag集合
1 |
> db.books.aggregate([ |
1、将tag 从数组拆分成单个文档
2、根据拆分后的结果,根据作者名称进行分组,分组后 将tag存入到set集合中
$limit
限制传递到管道中下一阶段的文档数,如 limit 20 那么传递给下一阶段的只有20条文档。
1 |
> db.books.aggregate([ |
注意:当$sort 和$limit同时出现时,要区分清楚 sort在limit前还是limit后,不同的位置导致结果不同(先排序后分页,还是先分页后排序)。
$skip
跳过指定数量的文档,将剩余文档传递给管道的下一阶段。注意,这里的skip是文档数量,而不是分页数量。
1 |
> db.books.aggregate([ |
$sort
1 |
> db.books.aggregate([ |
对字段设置排序的话,-1 代表降序,1 代表升序
$lookup(关联表查询)
通过$lookup可以实现 多表关联查询。经过$lookup后生成新的数组。
1 |
db.collection.aggregate([{ |
该方法等同于MySQL的关联表查询,只需要注意 from(被join的表),以及join 条件即可。
1 |
# 插入用户集合 |
查询用户表 并关联 订单表
1 |
db.customer.aggregate([ |
查询用户表,关联order表,关联字段 customerCode,as 重命名关联结果。
查询订单表并关联 用户表
1 |
db.order.aggregate([ |
聚合操作示例(一)
1 |
> db.books.aggregate([ |
以type 进行group分组,然后 sum 进行累加,参数为1 。 所以会把 type同一个类型的数量统计出来。
根据标签进行拆分,统计每个标签的收藏数(favCount)并进行降序排序
1 |
> db.books.aggregate([ |
区间统计book文档收藏数[0,10),[10,60),[60,80),[80,100),[100,+∞)
1 |
db.books.aggregate([{ |
$bucket 是关键字,意思是 启动区间统计
boundaries 统计的区间, 0-10,10-60,60-80,80-100等等
聚合操作示例(二)
导入邮政编码数据集 : https://media.mongodb.org/zips.json
导入工具 mongoimport 下载地址( https://www.mongodb.com/try/download/database-tools)
1 |
下载工具 |
-h 主机地址
-u 用户名
-p 密码
-d 数据库名称
-authenticationDatabase 认证数据库
-c 导入的数据库中的集合名称
-file 导入的文件名
返回人口超过1000万的州
1 |
db.zips.aggregate([ |
返回各州平均城市人口
1 |
db.zips.aggregate([ |
按州返回最大和最小的城市
1 |
db.zips.aggregate( [ { $group: { |
聚合操作示例二 - 返回人口超过1000万的州
1 |
|
AggregationResults
聚合操作示例二 - 返回各州平均城市人口
1 |
|
2. MongoDB索引
MongoDB索引是一种用来 快速查询数据的数据结构 。底层使用的是B+Tree来作为索引的数据结构。
MongoDB 支持各种丰富的索引类型 ,包括单键索引、复合索引,唯一索引等一 些常用的结构。在一些特殊应用场景,MongoDB还支持地理 空间索引、文本检索索引、TTL索引等不同的特性。
不使用索引的话,会扫描全部文档进行匹配,而使用 索引的话通过索引找到对应的文档 ,大大提高了查询效率。
MongoDB索引和MySQL的索引非常相似,基本一致,学习成本很低。