1. 聚合操作
db.<collection>.aggregate(<pipeline>,<options>)
<pipeline>
文档定义了操作中使用的聚合管道阶段和聚合操作符<options>
文档声明了一些聚合操作的参数
介绍几种常见的表达式:
-
字段路径表达式
$<field>
: 使用 $ 来指示字段路径,例如:KaTeX parse error: Expected 'EOF', got '表' at position 6: name 表̲示用户名字段 `.` : 使用 $ 和 . 来指示内嵌文档字段路径 -
系统变量表达式
$$<variable>
: 使用 KaTeX parse error: Expected 'EOF', got '来' at position 2: 来̲指示系统变量 `CURRENT: 指示管道中当前操作的文档
$KaTeX parse error: Expected 'EOF', got '和' at position 18: …RRENT.<field>` 和̲ `` 是等效的 -
常量表达式
$literal: <value>
: 指示常量<value>
$literal: "$name"
: 指示常量字符串 “$name”,这里 $ 被当作常量处理,而不是字段路径表达式
聚合管道阶段
- $project : 对输入的文档进行投影
- $match : 对输入的文档进行过滤筛选
- $limit : 筛选出管道内前N篇文档
- $skip : 跳过管道内前N篇文档
- $unwind : 展开输入文档中的数组字段
- $sort : 对输入的文档进行排序
- $lookup : 对输入的文档进行查询操作
- $group : 对输入的文档进行分组
- $out : 将管道中的文档输出
1.1 $project : 对输入的文档进行投影
$project是一个非常常用的聚合阶段,可以用来灵活地控制输出文档的格式,也可以用来剔除不相关的字段,以优化聚合管道操作的性能。
现在 user 集合的数据:
> db.user.find()
{ "_id" : "1", "money" : 1500, "name" : "刘一一", "gender" : "女" }
{ "_id" : "2", "money" : 1000, "name" : "陈二", "gender" : "女" }
{ "_id" : "3", "name" : "张三", "sal" : 1000, "gender" : "女" }
{ "_id" : "4", "money" : 800, "name" : "李四", "gender" : "女" }
{ "_id" : "5", "money" : 1000, "name" : "王五", "sal" : 500, "gender" : "女" }
{ "_id" : "6", "name" : "赵六", "money" : 1000 }
{ "_id" : "7", "money" : 1000, "name" : "孙七", "gender" : "女" }
{ "_id" : ObjectId("5fff6a78025e5d9ea86e18cc"), "money" : 1000, "name" : "周八", "gender" : "女" }
{ "_id" : { "age" : 18, "gender" : "男" }, "money" : 1000, "name" : "吴九", "gender" : "女" }
{ "_id" : ObjectId("5fff95783f6d5ed2fcce4133"), "money" : 2000, "name" : "ZHANG", "addr" : [ "三亚", "北京" ], "gender" : "女" }
{ "_id" : ObjectId("60045f5719414c7b5be4c122"), "name" : "富二代", "money" : 1500 }
{ "_id" : ObjectId("6004611c19414c7b5be4c13f"), "name" : "打工人", "money" : 800 }
>
只投影name 和 money 字段
> db.user.aggregate([{$project: {_id:0, name: 1, money: 1}}])
{ "money" : 1500, "name" : "刘一一" }
{ "money" : 1000, "name" : "陈二" }
{ "name" : "张三" }
{ "money" : 800, "name" : "李四" }
{ "money" : 1000, "name" : "王五" }
{ "name" : "赵六", "money" : 1000 }
{ "money" : 1000, "name" : "孙七" }
{ "money" : 1000, "name" : "周八" }
{ "money" : 1000, "name" : "吴九" }
{ "money" : 2000, "name" : "ZHANG" }
{ "name" : "富二代", "money" : 1500 }
{ "name" : "打工人", "money" : 800 }
>
在返回结果中对投影的字段设置别名,将 _id 设置别名 “主键”
> db.user.aggregate([{$project: {_id:0, name: 1, money: 1, 主键: "$_id"}}])
{ "money" : 1500, "name" : "刘一一", "主键" : "1" }
{ "money" : 1000, "name" : "陈二", "主键" : "2" }
{ "name" : "张三", "主键" : "3" }
{ "money" : 800, "name" : "李四", "主键" : "4" }
{ "money" : 1000, "name" : "王五", "主键" : "5" }
{ "name" : "赵六", "money" : 1000, "主键" : "6" }
{ "money" : 1000, "name" : "孙七", "主键" : "7" }
{ "money" : 1000, "name" : "周八", "主键" : ObjectId("5fff6a78025e5d9ea86e18cc") }
{ "money" : 1000, "name" : "吴九", "主键" : { "age" : 18, "gender" : "男" } }
{ "money" : 2000, "name" : "ZHANG", "主键" : ObjectId("5fff95783f6d5ed2fcce4133") }
{ "name" : "富二代", "money" : 1500, "主键" : ObjectId("60045f5719414c7b5be4c122") }
{ "name" : "打工人", "money" : 800, "主键" : ObjectId("6004611c19414c7b5be4c13f") }
>
1.2 $match : 对输入的文档进行过滤筛选
$match 中使用的文档筛选语法,和读取文档时的筛选语法相同。
筛选 name=“张三”或者 money=1500 的文档
> db.user.aggregate([{$match: {$or:[{name: "张三"},{money: 1500}]}}])
{ "_id" : "1", "money" : 1500, "name" : "刘一一", "gender" : "女" }
{ "_id" : "3", "name" : "张三", "sal" : 1000, "gender" : "女" }
{ "_id" : ObjectId("60045f5719414c7b5be4c122"), "name" : "富二代", "money" : 1500 }
>
$match 和 $project 结合使用
> db.user.aggregate([{$match: {$or:[{name: "张三"},{money: 1500}]}},{$project: {_id:0, name: 1, money: 1}}])
{ "money" : 1500, "name" : "刘一一" }
{ "name" : "张三" }
{ "name" : "富二代", "money" : 1500 }
>
注意:
$match 也是一个很常用的聚合阶段,应该尽量在聚合管道的开始阶段应用 $match,这样可以减少后续阶段中需要处理的文档数量,优化聚合操作的性能。
1.3 $limit : 筛选出管道内前N篇文档
筛选第一条文档
> db.user.aggregate([{$limit: 1}])
{ "_id" : "1", "money" : 1500, "name" : "刘一一", "gender" : "女" }
>
1.4 $skip : 跳过管道内前N篇文档
跳过第一条文档
> db.user.aggregate([{$skip: 1}])
{ "_id" : "2", "money" : 1000, "name" : "陈二", "gender" : "女" }
{ "_id" : "3", "name" : "张三", "sal" : 1000, "gender" : "女" }
{ "_id" : "4", "money" : 800, "name" : "李四", "gender" : "女" }
{ "_id" : "5", "money" : 1000, "name" : "王五", "sal" : 500, "gender" : "女" }
{ "_id" : "6", "name" : "赵六", "money" : 1000 }
{ "_id" : "7", "money" : 1000, "name" : "孙七", "gender" : "女" }
{ "_id" : ObjectId("5fff6a78025e5d9ea86e18cc"), "money" : 1000, "name" : "周八", "gender" : "女" }
{ "_id" : { "age" : 18, "gender" : "男" }, "money" : 1000, "name" : "吴九", "gender" : "女" }
{ "_id" : ObjectId("5fff95783f6d5ed2fcce4133"), "money" : 2000, "name" : "ZHANG", "addr" : [ "三亚", "北京" ], "gender" : "女" }
{ "_id" : ObjectId("60045f5719414c7b5be4c122"), "name" : "富二代", "money" : 1500 }
{ "_id" : ObjectId("6004611c19414c7b5be4c13f"), "name" : "打工人", "money" : 800 }
>
1.5 $unwind : 展开输入文档中的数组字段
{$unwind: {path: "$<field>", <includeArrayIndex>: <newFiled>, <preserveNullAndEmptyArrays>:<boolean>}}
path: "$<field>"
: 指定展开的数组字段<includeArrayIndex>: <newFiled>
: newField 指定新字段,用于显示元素在数组中的下标,从0开始。<preserveNullAndEmptyArrays>:<boolean>
: 默认是false,如果为 true , 展开数组时保留空数组或不存在数组的文档
将文档中的 addr 数组展开
> db.user.aggregate([{$match: {name: "ZHANG"}}])
{ "_id" : ObjectId("5fff95783f6d5ed2fcce4133"), "money" : 2000, "name" : "ZHANG", "addr" : [ "三亚", "北京" ], "gender" : "女" }
## 1.addr 数组展开
> db.user.aggregate([{$match: {name: "ZHANG"}},{$unwind: {path: "$addr"}}])
{ "_id" : ObjectId("5fff95783f6d5ed2fcce4133"), "money" : 2000, "name" : "ZHANG", "addr" : "三亚", "gender" : "女" }
{ "_id" : ObjectId("5fff95783f6d5ed2fcce4133"), "money" : 2000, "name" : "ZHANG", "addr" : "北京", "gender" : "女" }
## 1.addr 数组展开,并包含新字段,用于显示元素下标
> db.user.aggregate([{$match: {name: "ZHANG"}},{$unwind: {path: "$addr",includeArrayIndex: "index"}}])
{ "_id" : ObjectId("5fff95783f6d5ed2fcce4133"), "money" : 2000, "name" : "ZHANG", "addr" : "三亚", "gender" : "女", "index" : NumberLong(0) }
{ "_id" : ObjectId("5fff95783f6d5ed2fcce4133"), "money" : 2000, "name" : "ZHANG", "addr" : "北京", "gender" : "女", "index" : NumberLong(1) }
>
展开数组时保留空数组或不存在数组的文档
## 1.默认是false,不保留空数组或不存在数组的文档
> db.user.aggregate([{$unwind: {path: "$addr"}}])
{ "_id" : ObjectId("5fff95783f6d5ed2fcce4133"), "money" : 2000, "name" : "ZHANG", "addr" : "三亚", "gender" : "女" }
{ "_id" : ObjectId("5fff95783f6d5ed2fcce4133"), "money" : 2000, "name" : "ZHANG", "addr" : "北京", "gender" : "女" }
## 1.preserveNullAndEmptyArrays: true 展开数组时保留空数组或不存在数组的文档
> db.user.aggregate([{$unwind: {path: "$addr", preserveNullAndEmptyArrays: true}}])
{ "_id" : "1", "money" : 1500, "name" : "刘一一", "gender" : "女" }
{ "_id" : "2", "money" : 1000, "name" : "陈二", "gender" : "女" }
{ "_id" : "3", "name" : "张三", "sal" : 1000, "gender" : "女" }
{ "_id" : "4", "money" : 800, "name" : "李四", "gender" : "女" }
{ "_id" : "5", "money" : 1000, "name" : "王五", "sal" : 500, "gender" : "女" }
{ "_id" : "6", "name" : "赵六", "money" : 1000 }
{ "_id" : "7", "money" : 1000, "name" : "孙七", "gender" : "女" }
{ "_id" : ObjectId("5fff6a78025e5d9ea86e18cc"), "money" : 1000, "name" : "周八", "gender" : "女" }
{ "_id" : { "age" : 18, "gender" : "男" }, "money" : 1000, "name" : "吴九", "gender" : "女" }
{ "_id" : ObjectId("5fff95783f6d5ed2fcce4133"), "money" : 2000, "name" : "ZHANG", "addr" : "三亚", "gender" : "女" }
{ "_id" : ObjectId("5fff95783f6d5ed2fcce4133"), "money" : 2000, "name" : "ZHANG", "addr" : "北京", "gender" : "女" }
{ "_id" : ObjectId("60045f5719414c7b5be4c122"), "name" : "富二代", "money" : 1500 }
{ "_id" : ObjectId("6004611c19414c7b5be4c13f"), "name" : "打工人", "money" : 800 }
>
1.6 $sort : 对输入的文档进行排序
$sort 中使用的文档排序语法,和游标的 sort() 函数排序语法相同
根据money逆向排序和_id正向排序
> db.user.aggregate([{$sort: {money: -1, _id: 1}}])
{ "_id" : ObjectId("5fff95783f6d5ed2fcce4133"), "money" : 2000, "name" : "ZHANG", "addr" : [ "三亚", "北京" ], "gender" : "女" }
{ "_id" : "1", "money" : 1500, "name" : "刘一一", "gender" : "女" }
{ "_id" : ObjectId("60045f5719414c7b5be4c122"), "name" : "富二代", "money" : 1500 }
{ "_id" : "2", "money" : 1000, "name" : "陈二", "gender" : "女" }
{ "_id" : "5", "money" : 1000, "name" : "王五", "sal" : 500, "gender" : "女" }
{ "_id" : "6", "name" : "赵六", "money" : 1000 }
{ "_id" : "7", "money" : 1000, "name" : "孙七", "gender" : "女" }
{ "_id" : { "age" : 18, "gender" : "男" }, "money" : 1000, "name" : "吴九", "gender" : "女" }
{ "_id" : ObjectId("5fff6a78025e5d9ea86e18cc"), "money" : 1000, "name" : "周八", "gender" : "女" }
{ "_id" : "4", "money" : 800, "name" : "李四", "gender" : "女" }
{ "_id" : ObjectId("6004611c19414c7b5be4c13f"), "name" : "打工人", "money" : 800 }
{ "_id" : "3", "name" : "张三", "sal" : 1000, "gender" : "女" }
>
1.7 $group : 对输入的文档进行分组
{$group: {_id: <expression>, <field1>: {<accumulator1>: <expression1>}...}}
_id
: 定义分组规则<field1>: {<accumulator1>: <expression1>}
: 可以使用聚合操作符来定义新字段
根据money进行分组,查询每组的money总额,平均数,每个分组的数量,最大_id,最小_id
> db.user.aggregate([{$group: {_id: "$money", sum: {$sum: "$money"}, avg: {$avg: "$money"}, count: {$sum: 1}, max_id: {$max: "$_id"}, min_id: {$min: "$_id"}}}])
{ "_id" : 800, "sum" : 1600, "avg" : 800, "count" : 2, "max_id" : ObjectId("6004611c19414c7b5be4c13f"), "min_id" : "4" }
{ "_id" : 1000, "sum" : 6000, "avg" : 1000, "count" : 6, "max_id" : ObjectId("5fff6a78025e5d9ea86e18cc"), "min_id" : "2" }
{ "_id" : 1500, "sum" : 3000, "avg" : 1500, "count" : 2, "max_id" : ObjectId("60045f5719414c7b5be4c122"), "min_id" : "1" }
{ "_id" : 2000, "sum" : 2000, "avg" : 2000, "count" : 1, "max_id" : ObjectId("5fff95783f6d5ed2fcce4133"), "min_id" : ObjectId("5fff95783f6d5ed2fcce4133") }
{ "_id" : null, "sum" : 0, "avg" : null, "count" : 1, "max_id" : "3", "min_id" : "3" }
>
1.8 $out : 将管道中的文档输出
将聚合管道中的文档写入一个新的集合 outDoc
## 1.根据money进行分组,查询每组的money总额,平均数,每个分组的数量,最大_id,最小_id
> db.user.aggregate([{$group: {_id: "$money", sum: {$sum: "$money"}, avg: {$avg: "$money"}, count: {$sum: 1}, max_id: {$max: "$_id"}, min_id: {$min: "$_id"}}}])
{ "_id" : 800, "sum" : 1600, "avg" : 800, "count" : 2, "max_id" : ObjectId("6004611c19414c7b5be4c13f"), "min_id" : "4" }
{ "_id" : 1000, "sum" : 6000, "avg" : 1000, "count" : 6, "max_id" : ObjectId("5fff6a78025e5d9ea86e18cc"), "min_id" : "2" }
{ "_id" : 1500, "sum" : 3000, "avg" : 1500, "count" : 2, "max_id" : ObjectId("60045f5719414c7b5be4c122"), "min_id" : "1" }
{ "_id" : 2000, "sum" : 2000, "avg" : 2000, "count" : 1, "max_id" : ObjectId("5fff95783f6d5ed2fcce4133"), "min_id" : ObjectId("5fff95783f6d5ed2fcce4133") }
{ "_id" : null, "sum" : 0, "avg" : null, "count" : 1, "max_id" : "3", "min_id" : "3" }
## 1.将聚合管道中的文档写入一个新的集合 outDoc
> db.user.aggregate([{$group: {_id: "$money", sum: {$sum: "$money"}, avg: {$avg: "$money"}, count: {$sum: 1}, max_id: {$max: "$_id"}, min_id: {$min: "$_id"}}},{$out: "outDoc"}])
## 1.查看新文档 outDoc
> db.outDoc.find()
{ "_id" : 800, "sum" : 1600, "avg" : 800, "count" : 2, "max_id" : ObjectId("6004611c19414c7b5be4c13f"), "min_id" : "4" }
{ "_id" : 1000, "sum" : 6000, "avg" : 1000, "count" : 6, "max_id" : ObjectId("5fff6a78025e5d9ea86e18cc"), "min_id" : "2" }
{ "_id" : 1500, "sum" : 3000, "avg" : 1500, "count" : 2, "max_id" : ObjectId("60045f5719414c7b5be4c122"), "min_id" : "1" }
{ "_id" : 2000, "sum" : 2000, "avg" : 2000, "count" : 1, "max_id" : ObjectId("5fff95783f6d5ed2fcce4133"), "min_id" : ObjectId("5fff95783f6d5ed2fcce4133") }
{ "_id" : null, "sum" : 0, "avg" : null, "count" : 1, "max_id" : "3", "min_id" : "3" }
>
将聚合管道中的文档写入一个已存在的集合,则会覆盖已有的集合内容;
如果聚合管道操作遇到错误,管道阶段不会创建新集合或者覆盖已存在的集合内容。
1.9. 聚合阶段顺序
聚合阶段执行从前到后的顺序:
$match > $sort > $skip > $limit > $project
2. 索引
索引能加快文档查询和排序的速度。
索引操作db.collection.getIndexes() db.collection.createIndex() db.collection.dropIndex()
索引的类型
- 单键索引
- 复合键索引
- 多键索引
查询分析: explain() 查看索引的效果
索引的特性
- 唯一性
- 稀疏性
- 生存时间
2.1 查看索引
查看当前 user 集合的索引,可以看到只有主键 _id 的索引,主键索引是默认生成的
> db.user.getIndexes()
[ { "v" : 2, "key" : { "_id" : 1 }, "name" : "_id_" } ]
>
2.2 创建索引
db.<collection>.createIndex(<keys>,<options>)
<keys>文档: {<field1>:<1 | -1>...}
指定了创建索引的字段,1 表示升序, -1表示降序。<options>文档:
定义了创建索引时可以使用的一些参数,也可以设定索引的特性,如 唯一性。
给 money 字段创建一个单键索引
> db.user.createIndex({money: 1})
{
"createdCollectionAutomatically" : false,
"numIndexesBefore" : 1,
"numIndexesAfter" : 2,
"ok" : 1
}
> db.user.getIndexes()
[
{
"v" : 2,
"key" : {
"_id" : 1
},
"name" : "_id_"
},
{
"v" : 2,
"key" : {
"money" : 1
},
"name" : "money_1"
}
]
>
为 name 和 money 字段,创建一个复合键索引
> db.user.createIndex({name: 1, money: -1})
{
"createdCollectionAutomatically" : false,
"numIndexesBefore" : 2,
"numIndexesAfter" : 3,
"ok" : 1
}
> db.user.getIndexes()
[
{
"v" : 2,
"key" : {
"_id" : 1
},
"name" : "_id_"
},
{
"v" : 2,
"key" : {
"money" : 1
},
"name" : "money_1"
},
{
"v" : 2,
"key" : {
"name" : 1,
"money" : -1
},
"name" : "name_1_money_-1"
}
]
>
创建一个多键索引,数组 addr 中的每个元素,都会在多键索引中创建一个键
> db.user.createIndex({addr: 1})
{
"createdCollectionAutomatically" : false,
"numIndexesBefore" : 3,
"numIndexesAfter" : 4,
"ok" : 1
}
>
> db.user.getIndexes()
[
{
"v" : 2,
"key" : {
"_id" : 1
},
"name" : "_id_"
},
{
"v" : 2,
"key" : {
"money" : 1
},
"name" : "money_1"
},
{
"v" : 2,
"key" : {
"name" : 1,
"money" : -1
},
"name" : "name_1_money_-1"
},
{
"v" : 2,
"key" : {
"addr" : 1
},
"name" : "addr_1"
}
]
>
2.3 删除索引
db.<collection>.dropIndex("indexName") 或者 db.<collection>.dropIndex({<field1>:<1 | -1>...})
- 根据"索引 name"删除索引
- 根据"索引 key"删除索引
如果需要更改某些字段上已经创建的索引,必须先删除原有的索引,再重新创建新索引,否则,新索引不会包含原有的文档
根据"索引 name"删除索引:
> db.user.getIndexes()
[
{
"v" : 2,
"key" : {
"_id" : 1
},
"name" : "_id_"
},
{
"v" : 2,
"key" : {
"money" : 1
},
"name" : "money_1"
},
{
"v" : 2,
"key" : {
"name" : 1,
"money" : -1
},
"name" : "name_1_money_-1"
},
{
"v" : 2,
"key" : {
"addr" : 1
},
"name" : "addr_1"
}
]
# 删除索引 "addr_1"
> db.user.dropIndex("addr_1")
{ "nIndexesWas" : 4, "ok" : 1 }
# 删除索引 "money_1"
> db.user.dropIndex("money_1")
{ "nIndexesWas" : 3, "ok" : 1 }
> db.user.getIndexes()
[
{
"v" : 2,
"key" : {
"_id" : 1
},
"name" : "_id_"
},
{
"v" : 2,
"key" : {
"name" : 1,
"money" : -1
},
"name" : "name_1_money_-1"
}
]
>
根据"索引 key"删除索引:
> db.user.getIndexes()
[
{
"v" : 2,
"key" : {
"_id" : 1
},
"name" : "_id_"
},
{
"v" : 2,
"key" : {
"name" : 1,
"money" : -1
},
"name" : "name_1_money_-1"
}
]
# 根据"索引 key"删除索引
> db.user.dropIndex({name:1,money:-1})
{ "nIndexesWas" : 2, "ok" : 1 }
> db.user.getIndexes()
[ { "v" : 2, "key" : { "_id" : 1 }, "name" : "_id_" } ]
2.4 创建索引 options
db.<collection>.createIndex(<keys>,<options>)
<options>文档:
定义了创建索引时可以使用的一些参数,也可以设定索引的特性,如 唯一性、稀疏性。
复合键索引也可以具有唯一性,在这种情况下,不同的文档之间,其所包含的复合键字段值的组合不可以重复。
当前 user 集合只有主键索引
> db.user.getIndexes()
[ { "v" : 2, "key" : { "_id" : 1 }, "name" : "_id_" } ]
>
2.4.1 索引的唯一性
创建一个具有唯一性的索引,给 name 字段创建唯一索引
> db.user.createIndex({name: 1},{unique: true})
{
"createdCollectionAutomatically" : false,
"numIndexesBefore" : 1,
"numIndexesAfter" : 2,
"ok" : 1
}
> db.user.getIndexes()
[
{
"v" : 2,
"key" : {
"_id" : 1
},
"name" : "_id_"
},
{
"v" : 2,
"unique" : true,
"key" : {
"name" : 1
},
"name" : "name_1"
}
]
>
user 集合中文档
> db.user.find()
{ "_id" : "1", "money" : 1500, "name" : "刘一一", "gender" : "女" }
{ "_id" : "2", "money" : 1000, "name" : "陈二", "gender" : "女" }
{ "_id" : "3", "name" : "张三", "sal" : 1000, "gender" : "女" }
{ "_id" : "4", "money" : 800, "name" : "李四", "gender" : "女" }
{ "_id" : "5", "money" : 1000, "name" : "王五", "sal" : 500, "gender" : "女" }
{ "_id" : "6", "name" : "赵六", "money" : 1000 }
{ "_id" : "7", "money" : 1000, "name" : "孙七", "gender" : "女" }
{ "_id" : ObjectId("5fff6a78025e5d9ea86e18cc"), "money" : 1000, "name" : "周八", "gender" : "女" }
{ "_id" : { "age" : 18, "gender" : "男" }, "money" : 1000, "name" : "吴九", "gender" : "女" }
{ "_id" : ObjectId("5fff95783f6d5ed2fcce4133"), "money" : 2000, "name" : "ZHANG", "addr" : [ "三亚", "北京" ], "gender" : "女" }
{ "_id" : ObjectId("60045f5719414c7b5be4c122"), "name" : "富二代", "money" : 1500 }
{ "_id" : ObjectId("6004611c19414c7b5be4c13f"), "name" : "打工人", "money" : 800 }
>
已经存在 name=张三 的文档,因为 name 字段具有唯一索引,如果再创建一个 name=张三 的文档会报错
> db.user.insert({name: "张三", money: 1800})
WriteResult({
"nInserted" : 0,
"writeError" : {
"code" : 11000,
"errmsg" : "E11000 duplicate key error collection: test.user index: name_1 dup key: { name: \"张三\" }"
}
})
>
如果新增的文档不包含唯一索引字段,只有第一个缺失该字段的文档可以被写入数据库,索引中该文档的键值被默认为null。
# 第一个文档,name字段不存在,则默认为null
> db.user.insert({money: 1100})
WriteResult({ "nInserted" : 1 })
> db.user.find({money: 1100})
{ "_id" : ObjectId("6004c82e21bca04a14f52bee"), "money" : 1100 }
# 第二个文档,name字段不存在,创建时报错
> db.user.insert({money: 1200})
WriteResult({
"nInserted" : 0,
"writeError" : {
"code" : 11000,
"errmsg" : "E11000 duplicate key error collection: test.user index: name_1 dup key: { name: null }"
}
})
2.4.2 索引的稀疏性
只将包含索引键字段的文档加入到索引中,索引键字段值为null也会被加入索引,但是如果字段不存在,则不加入。
如果同一个索引既具有唯一性,又具有稀疏性,就可以保存多篇缺失索引键值的文档了。
重新给 name 字段创建唯一稀疏索引,因为已经存在 name 的唯一索引,因此要先删除,再重新创建
> db.user.createIndex({name: 1},{unique:true,sparse:true})
{
"createdCollectionAutomatically" : false,
"numIndexesBefore" : 1,
"numIndexesAfter" : 2,
"ok" : 1
}
> db.user.getIndexes()
[
{
"v" : 2,
"key" : {
"_id" : 1
},
"name" : "_id_"
},
{
"v" : 2,
"unique" : true,
"key" : {
"name" : 1
},
"name" : "name_1",
"sparse" : true
}
]
现在已经存在一个name不存在的文档
> db.user.find({name: {$exists: false}})
{ "_id" : ObjectId("6004c82e21bca04a14f52bee"), "money" : 1100 }
>
再插入一个name不存在的文档,插入成功
> db.user.insert({money: 1200})
WriteResult({ "nInserted" : 1 })
> db.user.find({name: {$exists: false}})
{ "_id" : ObjectId("6004c82e21bca04a14f52bee"), "money" : 1100 }
{ "_id" : ObjectId("6004ce5521bca04a14f52bf0"), "money" : 1200 }
>
2.5 explain() 查询分析
db.<collection>.explain(<verbose>).<method(...)>
-
<verbose>
: {String},可选参数。指定冗长模式的解释输出,方式指定后会影响explain()的行为及输出信息。可选值有:“queryPlanner”、“executionStats”、“allPlansExecution”,默认为"queryPlanner" -
queryPlanner :查询计划,查询优化选择的计划细节和被拒绝的计划,默认会显示。
-
executionStats : 执行状态,会返回最佳执行计划的一些统计信息
-
allPlansExecution : 用来获取所有执行计划
可以使用 explain() 进行分析的命令包括 aggregate(),count(),distinct(),find(),group(),remove(),update()
2.5.1 explain 返回信息
queryPlanner(查询计划):查询优化选择的计划细节和被拒绝的计划。其可能包括以下值:
namespace :查询的集合
indexFilterSet:一个Boolean值,表示MongoDB在查询中是否使用索引过滤
winningPlan :最佳执行计划
shards : 包括每个访问片的queryPlanner和serverInfo的文档数组
stage : 查询方式
filter : 过滤条件
direction : 查询顺序
inputStage : 用来描述子stage,并且为其父stage提供文档和索引关键字
keyPattern : 所扫描的index key
indexName : 索引名
isMultiKey : 是否是Multikey。如果索引建立在array上,将是true
inputStages : 表示子过程的文档数组
rejectedPlans : 被查询优化备选并被拒绝的计划数组
executionStats,(执行状态):被选中执行计划和被拒绝执行计划的详细说明:
executionSuccess : 是否执行成功
nReturned : 返回的个数
executionTimeMillis : 计划选择和查询执行所需的总时间(毫秒数)
totalKeysExamined : 扫描的索引总数
totalDocsExamined : 扫描的文档总数
executionStages : 显示执行成功细节的查询阶段树
executionTimeMillisEstimate : 检索文档获取数据的时间
works : 指定查询执行阶段执行的“工作单元”的数量
advanced : 优先返回的结果数
needTime : 未将中间结果推进到其父级的工作周期数
needYield : 存储层要求查询系统产生的锁的次数
isEOF : 指定执行阶段是否到达 steam 结尾,1 或者 true 代表已到达结尾
docsExamined : 文档检查数
executionStats.allPlansExecution : 包含在计划选择阶段期间捕获的部分执行信息,包括选择计划和拒绝计划
serverInfo(服务器信息):MongoDB实例的相关信息:
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
: 不合理的SKIPCOUNTSCAN
: count不使用Index进行count时的stage返回SUBPLA
: 未使用到索引的$or查询的stage返回
使用没有创建索引的字段进行搜索
> db.user.explain("executionStats").find({money: 1000})
{
"queryPlanner" : {
"plannerVersion" : 1,
# 查询的集合
"namespace" : "test.user",
# MongoDB在查询中是否使用索引过滤
"indexFilterSet" : false,
# 查询条件
"parsedQuery" : {
"money" : {
"$eq" : 1000
}
},
"queryHash" : "44936D04",
"planCacheKey" : "44936D04",
# 最佳执行计划
"winningPlan" : {
# 查询方式:COLLSCAN 全表扫描
"stage" : "COLLSCAN",
#过滤条件
"filter" : {
"money" : {
"$eq" : 1000
}
},
# 查询顺序
"direction" : "forward"
},
"rejectedPlans" : [ ]
},
# 执行状态
"executionStats" : {
# 执行成功
"executionSuccess" : true,
# 返回的个数
"nReturned" : 6,
# 计划选择和查询执行所需的总时间(毫秒数)
"executionTimeMillis" : 0,
# 扫描的索引总数
"totalKeysExamined" : 0,
# 扫描的文档总数
"totalDocsExamined" : 15,
"executionStages" : {
# 查询方式:COLLSCAN 全表扫描
"stage" : "COLLSCAN",
# 过滤条件
"filter" : {
"money" : {
"$eq" : 1000
}
},
# 返回的个数
"nReturned" : 6,
# 检索文档获取数据的时间
"executionTimeMillisEstimate" : 0,
"works" : 17,
"advanced" : 6,
"needTime" : 10,
"needYield" : 0,
"saveState" : 0,
"restoreState" : 0,
"isEOF" : 1,
"direction" : "forward",
# 文档检查数
"docsExamined" : 15
}
},
# Mongo服务器信息
"serverInfo" : {
"host" : "docker01",
"port" : 27017,
"version" : "4.4.3",
"gitVersion" : "913d6b62acfbb344dde1b116f4161360acd8fd13"
},
"ok" : 1
}
>
使用已经创建索引的字段进行搜索
> db.user.explain("executionStats").find({name: "张三"})
{
"queryPlanner" : {
"plannerVersion" : 1,
# 查询集合
"namespace" : "test.user",
# 在查询中是否使用索引过滤
"indexFilterSet" : false,
# 查询条件
"parsedQuery" : {
"name" : {
"$eq" : "张三"
}
},
"queryHash" : "01AEE5EC",
"planCacheKey" : "4C5AEA2C",
# 最佳执行计划
"winningPlan" : {
# 查询方式:FETCH 根据索引检索指定文档
"stage" : "FETCH",
# 用来描述子stage,并且为其父stage提供文档和索引关键字
"inputStage" : {
# 查询方式:IXSCAN 索引扫描
"stage" : "IXSCAN",
# 所扫描的index key
"keyPattern" : {
"name" : 1
},
# 索引名称
"indexName" : "name_1",
# 不是多键索引
"isMultiKey" : false,
"multiKeyPaths" : {
"name" : [ ]
},
# 唯一索引
"isUnique" : true,
# 稀疏索引
"isSparse" : true,
"isPartial" : false,
"indexVersion" : 2,
# 查询顺序
"direction" : "forward",
"indexBounds" : {
"name" : [
"[\"张三\", \"张三\"]"
]
}
}
},
"rejectedPlans" : [ ]
},
"executionStats" : {
# 执行成功
"executionSuccess" : true,
# 返回结果数
"nReturned" : 1,
# 执行时间
"executionTimeMillis" : 0,
# 扫描索引数
"totalKeysExamined" : 1,
# 扫描文档数
"totalDocsExamined" : 1,
"executionStages" : {
# 查询方式:FETCH 根据索引检索指定文档
"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,
# 索引 key
"keyPattern" : {
"name" : 1
},
# 索引名称
"indexName" : "name_1",
"isMultiKey" : false,
"multiKeyPaths" : {
"name" : [ ]
},
"isUnique" : true,
"isSparse" : true,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "forward",
"indexBounds" : {
"name" : [
"[\"张三\", \"张三\"]"
]
},
"keysExamined" : 1,
"seeks" : 1,
"dupsTested" : 0,
"dupsDropped" : 0
}
}
},
"serverInfo" : {
"host" : "docker01",
"port" : 27017,
"version" : "4.4.3",
"gitVersion" : "913d6b62acfbb344dde1b116f4161360acd8fd13"
},
"ok" : 1
}
>
仅返回创建了索引的字段
> db.user.explain().find({name: "张三"},{_id:0,name:1})
{
"queryPlanner" : {
"plannerVersion" : 1,
# 查询集合
"namespace" : "test.user",
# 在查询中是否使用索引过滤
"indexFilterSet" : false,
# 查询条件
"parsedQuery" : {
"name" : {
"$eq" : "张三"
}
},
"queryHash" : "3066FB64",
"planCacheKey" : "A5386A93",
# 最佳执行计划
"winningPlan" : {
# 查询方式:PROJECTION_COVERED 索引覆盖查询
"stage" : "PROJECTION_COVERED",
"transformBy" : {
"_id" : 0,
"name" : 1
},
# 用来描述子stage,并且为其父stage提供文档和索引关键字
"inputStage" : {
# 查询方式:IXSCAN 索引扫描
"stage" : "IXSCAN",
# 所扫描的index key
"keyPattern" : {
"name" : 1
},
# 索引name
"indexName" : "name_1",
# 不是多键索引
"isMultiKey" : false,
"multiKeyPaths" : {
"name" : [ ]
},
# 唯一索引
"isUnique" : true,
# 稀疏索引
"isSparse" : true,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "forward",
"indexBounds" : {
"name" : [
"[\"张三\", \"张三\"]"
]
}
}
},
"rejectedPlans" : [ ]
},
"serverInfo" : {
"host" : "docker01",
"port" : 27017,
"version" : "4.4.3",
"gitVersion" : "913d6b62acfbb344dde1b116f4161360acd8fd13"
},
"ok" : 1
}
>
使用未创建索引的字段进行排序
> db.user.explain().find().sort({money:1})
{
"queryPlanner" : {
"plannerVersion" : 1,
# 查询集合
"namespace" : "test.user",
# 在查询中是否使用索引过滤
"indexFilterSet" : false,
# 查询条件
"parsedQuery" : {
},
"queryHash" : "1B95C3FD",
"planCacheKey" : "1B95C3FD",
# 最佳执行计划
"winningPlan" : {
# 查询方式:SORT 在内存中进行了排序
"stage" : "SORT",
"sortPattern" : {
"money" : 1
},
"memLimit" : 104857600,
"type" : "simple",
# 用来描述子stage,并且为其父stage提供文档和索引关键字
"inputStage" : {
# 查询方式:COLLSCAN 全表扫描
"stage" : "COLLSCAN",
"direction" : "forward"
}
},
"rejectedPlans" : [ ]
},
"serverInfo" : {
"host" : "docker01",
"port" : 27017,
"version" : "4.4.3",
"gitVersion" : "913d6b62acfbb344dde1b116f4161360acd8fd13"
},
"ok" : 1
}
>
使用已经创建索引的字段进行排序
# 给 money 创建一个正序的索引
> db.user.createIndex({money:1})
{
"createdCollectionAutomatically" : false,
"numIndexesBefore" : 2,
"numIndexesAfter" : 3,
"ok" : 1
}
# 对使用已经创建索引的字段进行排序进行分析
> db.user.explain().find().sort({money:1})
{
"queryPlanner" : {
"plannerVersion" : 1,
# 查询集合
"namespace" : "test.user",
# 在查询中是否使用索引过滤
"indexFilterSet" : false,
# 查询条件
"parsedQuery" : {
},
"queryHash" : "1B95C3FD",
"planCacheKey" : "1B95C3FD",
# 最佳执行计划
"winningPlan" : {
# 查询方式:FETCH 根据索引检索指定文档
"stage" : "FETCH",
# 用来描述子stage,并且为其父stage提供文档和索引关键字
"inputStage" : {
# 查询方式:IXSCAN 索引扫描
"stage" : "IXSCAN",
# 所扫描的index key
"keyPattern" : {
"money" : 1
},
# 索引名称
"indexName" : "money_1",
"isMultiKey" : false,
"multiKeyPaths" : {
"money" : [ ]
},
# 非唯一索引
"isUnique" : false,
# 非稀疏索引
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "forward",
"indexBounds" : {
"money" : [
"[MinKey, MaxKey]"
]
}
}
},
"rejectedPlans" : [ ]
},
"serverInfo" : {
"host" : "docker01",
"port" : 27017,
"version" : "4.4.3",
"gitVersion" : "913d6b62acfbb344dde1b116f4161360acd8fd13"
},
"ok" : 1
}
>