为了账号安全,请及时绑定邮箱和手机立即绑定

mongo-05-聚合操作和索引

标签:
MongoDB

1. 聚合操作

db.<collection>.aggregate(<pipeline>,<options>)

<pipeline> 文档定义了操作中使用的聚合管道阶段和聚合操作符
<options> 文档声明了一些聚合操作的参数

介绍几种常见的表达式:

  1. 字段路径表达式
    $<field> : 使用 $ 来指示字段路径,例如:KaTeX parse error: Expected 'EOF', got '表' at position 6: name 表̲示用户名字段 `.` : 使用 $ 和 . 来指示内嵌文档字段路径

  2. 系统变量表达式
    $$<variable> : 使用 KaTeX parse error: Expected 'EOF', got '来' at position 2: 来̲指示系统变量 `CURRENT: 指示管道中当前操作的文档$KaTeX parse error: Expected 'EOF', got '和' at position 18: …RRENT.<field>` 和̲ `` 是等效的

  3. 常量表达式
    $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>...})

  1. 根据"索引 name"删除索引
  2. 根据"索引 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 : 不合理的SKIP
  • COUNTSCAN : 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
}
> 
点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消