1、mongo操作
1.1、数据库操作
- 创建数据库:
use DATABASE_NAME
,如果数据库不存在,则创建数据库,否则切换到指定数据库。 - 查看当前所处数据库:
db
- 查看所有数据库:
show dbs
,如果数据库中没有数据,那么不会显示没有数据的数据库 - 删除当前所处数据库:
db.dropDatabase()
1.2、集合操作
- 创建集合:
db.createCollection(name, options)
- name: 要创建的集合名称MongoDB
- options: 可选参数, 指定有关内存大小及索引的选项
- options可以是如下参数
字段 | 类型 | 描述 |
---|---|---|
capped | 布尔 | (可选)如果为 true,则创建固定集合。固定集合是指有着固定大小的集合,当达到最大值时,它会自动覆盖最早的文档。当该值为 true 时,必须指定 size 参数。 |
autoIndexId | 布尔 | 3.2 之后不再支持该参数。(可选)如为 true,自动在 _id 字段创建索引。默认为 false。 |
size | 数值 | (可选)为固定集合指定一个最大值,以千字节计(KB)。如果 capped 为 true,也需要指定该字段。 |
max | 数值 | (可选)指定固定集合中包含文档的最大数量。 |
- 查看已有集合:
show collections
或者show tables
- 向集合中插入数据:
db.CollectionName.insert()
,如果集合不存在,那么会自动创建集合 - 删除集合:
db.CollectionName.drop()
1.3、文档操作
文档的数据结构和 JSON 基本一样,所有存储在集合中的数据都是 BSON 格式,BSON 是一种类似 JSON 的二进制形式的存储格式,是 Binary JSON 的简称。
- 插入文档:
db.COLLECTION_NAME.insert(document)
或者db.COLLECTION_NAME.save(document)
- save():如果 _id 主键存在则更新数据,如果不存在就插入数据。该方法3.2版本后已不建议使用,可以使用
db.collection.insertOne()
或db.collection.replaceOne()
来代替。 - insert(): 若插入的数据唯一索引已经存在,如果使用spring的MongoTemplate则会抛 org.springframework.dao.DuplicateKeyException 异常,提示主键重复,不保存当前数据。
- 3.2 版本之后新增了
db.collection.insertOne()
和db.collection.insertMany()
- save():如果 _id 主键存在则更新数据,如果不存在就插入数据。该方法3.2版本后已不建议使用,可以使用
- 向集合插入一个新文档:
db.collection.insertOne()
db.collection.insertOne(
<document>,
{
writeConcern: <document>
}
)
- 向集合插入多条文档:
db.collection.insertMany()
db.collection.insertMany(
[ <document 1> , <document 2>, ... ],
{
writeConcern: <document>,
ordered: <boolean>
}
)
参数说明:
* document:要写入的文档。
* writeConcern:写入策略,默认为 1,即要求确认写操作,0 是不要求。
* ordered:指定是否按顺序写入,默认 true,按顺序写入。
MongoDB支持的WriteConncern选项如下
w: 数据写入到number个节点才向用客户端确认
{w: 0} 对客户端的写入不需要发送任何确认,适用于性能要求高,但不关注正确性的场景:比如还没有插入完成,另一线程就开始读取数据会读不到
{w: 1} 默认的writeConcern,数据写入到Primary就向客户端发送确认
{w: “majority”} 数据写入到副本集大多数成员后向客户端发送确认,适用于对数据安全性要求比较高的场景,该选项会降低写入性能
j: 写入操作的journal持久化后才向客户端确认
默认为”{j: false},如果要求Primary写入持久化了才向客户端确认,则指定该选项为true
wtimeout: 写入超时时间,仅w的值大于1时有效。
当指定{w: }时,数据需要成功写入number个节点才算成功,如果写入过程中有节点故障,可能导致这个条件一直不能满足,从而一直不能向客户端发送确认结果,针对这种情况,客户端可设置wtimeout选项来指定超时时间,当写入过程持续超过该时间仍未结束,则认为写入失败。
-
操作符
- 条件操作符
- (>) 大于 - $gt
- (<) 小于 - $lt
- (>=) 大于等于 - $gte
- (<= ) 小于等于 - $lte
- 条件操作符
-
删除文档:
db.collection.remove()
db.collection.remove(
<query>,
{
justOne: <boolean>,
writeConcern: <document>
}
)
参数说明:
* query :(可选)删除的文档的条件。
* justOne : (可选)如果设为 true 或 1,则只删除一个文档,如果不设置该参数,或使用默认值 false,则删除所有匹配条件的文档。
* writeConcern :(可选)抛出异常的级别。
- 更新文档:
db.collection.update()
db.collection.update(
<query>,
<update>,
{
upsert: <boolean>,
multi: <boolean>,
writeConcern: <document>
}
)
参数说明:
* query : update的查询条件,类似sql update查询内where后面的。
* update : update的对象和一些更新的操作符(如
,
,
,inc…)等,也可以理解为sql update查询内set后面的
* upsert : 可选,这个参数的意思是,如果不存在update的记录,是否插入objNew,true为插入,默认是false,不插入。
* multi : 可选,mongodb 默认是false,只更新找到的第一条记录,如果这个参数为true,就把按条件查出来多条记录全部更新。
* writeConcern :可选,抛出异常的级别。
-
查询文档:
db.collection.find(query, projection)
后边可以再加.pretty()
表示以格式化的方式来显示文档,增加可读性,findOne()
代替find()
表示查询符合条件的一条数据- query :可选,使用查询操作符指定查询条件
- projection :可选,使用投影操作符指定返回的键。查询时返回文档中所有键值, 只需省略该参数即可(默认省略)。
-
查询文档and条件:
db.col.find({key1:value1, key2:value2}).pretty()
-
查询文档or条件:
db.col.find(
{
$or: [
{key1: value1}, {key2:value2}
]
}
).pretty()
- 查询and和or联用:
db.col.find(
{ key1:{$gt:50,$lt:100}, key2:value2,
$or: [
{key3: value3}, {key4:value4}
]
}
).pretty()
表达成sql格式为where key1>50 AND key1<100 AND key2=value2 AND (key3 = value3 OR key4 = value4)
- 模糊查询
查询包含XXX:{name:/xxx/}
查询以XXX开头:{name:/^xxx/}
查询以XXX结尾:{name:/xxx^/}
查询包含XXX忽略大小写:{name:/xxx/i}
1.4、练习
- js shell
1.//切换数据库
use weijiabintest
2.//插入数据
db.weicol.insertMany(
[{ stringid:"001",
name:"张三",
sex:"男",
age:16,
addr:"北京",
},{ stringid:"002",
name:"李四",
sex:"男",
age:21,
addr:"天津",
},{ stringid:"003",
name:"王五",
sex:"男",
age:18,
addr:"上海",
},{ stringid:"004",
name:"小红",
sex:"女",
age:19,
addr:"杭州",
},{ stringid:"005",
name:"小丽",
sex:"女",
age:27,
addr:"广州",
},{ stringid:"006",
name:"小美",
sex:"女",
age:22,
addr:"深圳",
}]
)
3.//查询年龄在[18,21],并且来自上海或者杭州的成员
这里的age数值为21.00也是可以查出来的,即便数据库中的类型为NumberInt
db.weicol.find(
{age:{$gte:18,$lte:21},
$or:[{addr:"上海"},{addr:"杭州"}]}
)
//结果:
{ "_id" : ObjectId("5f027ccb9faf43b63593de56"), "stringid" : "003", "name" : "王五", "sex" : "男", "age" : 18, "addr" : "上海" }
{ "_id" : ObjectId("5f027ccb9faf43b63593de57"), "stringid" : "004", "name" : "小红", "sex" : "女", "age" : 19, "addr" : "杭州" }
4.//根据_id查询
db.weicol.find(
{_id:ObjectId("5f027ccb9faf43b63593de56")}
)
//结果:
{ "_id" : ObjectId("5f027ccb9faf43b63593de56"), "stringid" : "003", "name" : "王五", "sex" : "男", "age" : 18, "addr" : "上海" }
5.//更改王五的年龄为21
db.weicol.update(
{name:"王五"},
{$set:{age:21}}
)
//结果:WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
6.//查询王五的年龄
db.weicol.find(
{name:"王五"},
{age: 1}
)
//结果:{ "_id" : ObjectId("5f027ccb9faf43b63593de56"), "age" : 21 }
7.删除张三的信息
db.weicol.remove(
{name:"张三"}
)
//结果:WriteResult({ "nRemoved" : 1 })
8.模糊查询,/相当于mysql中的%
db.weicol.find({
name: /黑/
})
- MongoTemplate
package com.fanggeek.mongo.test;
import com.fanggeek.mongo.entity.Student;
import com.mongodb.MongoClientOptions;
import com.mongodb.MongoClientURI;
import com.mongodb.client.result.UpdateResult;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.SimpleMongoDbFactory;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import java.util.regex.Pattern;
public class MongoTest {
private SimpleMongoDbFactory mongoDbFactory;
private MongoTemplate mongoTemplate;
@Before
public void init(){
MongoClientOptions.Builder mongoBuilder = new MongoClientOptions.Builder();
MongoClientURI mongoClientURI = new MongoClientURI("mongodb://127.0.0.1:27017/weijiabintest?authSource=admin",mongoBuilder);
mongoDbFactory = new SimpleMongoDbFactory(mongoClientURI);
mongoTemplate = new MongoTemplate(mongoDbFactory);
}
@After
public void finish(){
try {
mongoDbFactory.destroy();
} catch (Exception e) {
e.printStackTrace();
}
}
@Test
public void insert(){
Student student = new Student("009", "张琪", "男", 16, "南京");
System.out.println(mongoTemplate.insert(student,"weicol"));
}
@Test
public void find(){
//查询年龄大于等于18的男生
Query query = new Query();
query.addCriteria(Criteria.where("sex").is("男"));
query.addCriteria(Criteria.where("age").gte(18));
mongoTemplate.find(query, Student.class, "weicol").stream().forEach(System.out::println);
//模糊查询以 ^开始 以$结束 .*相当于Mysql中的%
//查询以小字开头的姓名或者来自天津的学生信息
Pattern pattern1 = Pattern.compile("^小.*$",Pattern.CASE_INSENSITIVE);
Criteria criteria = new Criteria();
//phone以222结尾的 或者 name以222结尾的
criteria.orOperator(Criteria.where("name").regex(pattern1),
Criteria.where("addr").is("天津"));
Query query1 = new Query(criteria);
mongoTemplate.find(query1, Student.class, "weicol").stream().forEach(System.out::println);;
//查询以小字开头的姓名并且来自广州的学生信息
Criteria criteria2 = new Criteria();
criteria2.andOperator(Criteria.where("name").regex(pattern1),
Criteria.where("addr").regex("广州"));
Query query2 = new Query(criteria2);
mongoTemplate.find(query2, Student.class, "weicol").stream().forEach(System.out::println);;
}
@Test
public void update(){
//将stringid为008的学生的地址改为addr
Query query = new Query();
query.addCriteria(Criteria.where("stringid").is("008")); //_id区分引号 "1"和1
Update update = Update.update("addr", "杭州");
// UpdateResult result = mongoTemplate.updateMulti(query, update, "weicol"); //查询到的全部更新
// UpdateResult result = mongoTemplate.updateFirst(query, update, "weicol"); //查询更新第一条
UpdateResult result = mongoTemplate.upsert(query, update, "weicol"); //有则更新,没有则新增
}
@Test
public void delete(){
Query query = new Query(Criteria.where("name").is("小黑"));
mongoTemplate.remove(query, "weicol");
}
}
1.5、聚合查询
- 聚合表达式
表达式 | 描述 | 实例 |
---|---|---|
$sum | 计算总和。 | db.mycol.aggregate([{KaTeX parse error: Expected '}', got 'EOF' at end of input: …roup : {_id : "by_user", num_tutorial : { s u m : " sum : " sum:"likes"}}}]) |
$avg | 计算平均值 | db.mycol.aggregate([{KaTeX parse error: Expected '}', got 'EOF' at end of input: …roup : {_id : "by_user", num_tutorial : { a v g : " avg : " avg:"likes"}}}]) |
$min | 获取集合中所有文档对应值得最小值。 | db.mycol.aggregate([{KaTeX parse error: Expected '}', got 'EOF' at end of input: …roup : {_id : "by_user", num_tutorial : { m i n : " min : " min:"likes"}}}]) |
$max | 获取集合中所有文档对应值得最大值。 | db.mycol.aggregate([{KaTeX parse error: Expected '}', got 'EOF' at end of input: …roup : {_id : "by_user", num_tutorial : { m a x : " max : " max:"likes"}}}]) |
$push | 在结果文档中插入值到一个数组中,不去除重复的值。 | db.mycol.aggregate([{KaTeX parse error: Expected '}', got 'EOF' at end of input: …roup : {_id : "by_user", url : { p u s h : " push: " push:"url"}}}]) |
$addToSet | 在结果文档中插入值到一个数组中,去除重复的值。 | db.mycol.aggregate([{KaTeX parse error: Expected '}', got 'EOF' at end of input: …roup : {_id : "by_user", url : { a d d T o S e t : " addToSet : " addToSet:"url"}}}]) |
$first | 根据资源文档的排序获取第一个文档数据。 | db.mycol.aggregate([{KaTeX parse error: Expected '}', got 'EOF' at end of input: …roup : {_id : "by_user", first_url : { f i r s t : " first : " first:"url"}}}]) |
$last | 根据资源文档的排序获取最后一个文档数据 | db.mycol.aggregate([{KaTeX parse error: Expected '}', got 'EOF' at end of input: …roup : {_id : "by_user", last_url : { l a s t : " last : " last:"url"}}}]) |
管道在Unix和Linux中一般用于将当前命令的输出结果作为下一个命令的参数。
MongoDB的聚合管道将MongoDB文档在一个管道处理完毕后将结果传递给下一个管道处理。管道操作是可以重复的。
表达式:处理输入文档并输出。表达式是无状态的,只能用于计算当前聚合管道的文档,不能处理其它的文档。
-
聚合框架中常用的操作
$project:修改输入文档的结构。可以用来重命名、增加或删除域,也可以用于创建计算结果以及嵌套文档。
m a t c h : 用 于 过 滤 数 据 , 只 输 出 符 合 条 件 的 文 档 。 match:用于过滤数据,只输出符合条件的文档。 match:用于过滤数据,只输出符合条件的文档。match使用MongoDB的标准查询操作。
$limit:用来限制MongoDB聚合管道返回的文档数。
$skip:在聚合管道中跳过指定数量的文档,并返回余下的文档。
$unwind:将文档中的某一个数组类型字段拆分成多条,每条包含数组中的一个值。
$group:将集合中的文档分组,可用于统计结果。
$sort:将输入文档排序后输出。
$geoNear:输出接近某一地理位置的有序文档。 -
聚合查询的应用
聚合查询的应用 1) 从dev环境fanggeek数据库,复制team_house_follow_ups集合的结构和数据 2) 查询某个房源(houseId=5eaa7118ee8038000165a9f6)的跟进聚合数据,要求:查询出每个人每分钟内的跟进信息列表(包括人agentId,每分钟时间戳,跟进内容列表),查询结果按照跟进时间倒序排序
解决思路:首先由于是每分钟内,所以应该把时间改为精确到分钟的格式以便筛选出每个人同一分钟的数据,然后每个人每分钟,所以应该将agentId与精确到分钟的时间作为唯一值,跟进信息列表应该将每人每分钟的content放到一个list里边,每条数据中的跟进时间为当前人员当前分钟最早跟进的时间,然后按照时间进行倒序排序。
db.team_house_follow_ups.aggregate(
[{$match:{houseId:"5eaa7118ee8038000165a9f6"}},
{$project:{_id:0,agentId:1,content:1,createTime:1,time:{$dateToString: {
format: "%Y%m%d%H%M",
date: "$createTime"
}
}}},
{$group:{_id:{time:"$time",agentId:"$agentId"},num_tutorial:{$sum:1},content_list:{$push:"$content"},createTime:{$first:"$createTime"}}},
{$sort:{createTime:-1}}]
)
1.6、索引及explain
1.索引的_id是如何生成的,有什么作用?
之前我们使用MySQL等关系型数据库时,主键都是设置成自增的。但在分布式环境下,这种方法就不可行了,会产生冲突。为此,MongoDB采用了一个称之为ObjectId的类型来做主键。ObjectId是一个12字节的BSON类型字符串,每个字节两位16进制数字,是一个24位的字符串。按照字节顺序,依次代表:
- 4字节:UNIX时间戳
- 3字节:表示运行MongoDB的机器标识码
- 2字节:表示生成此_id的进程id(pid)
- 3字节:由一个随机数开始的计数器生成的值
前九字节保证了同一秒钟不同机器的不同进程产生的ObjectId时唯一的,最后三字节是自增计数器,确保相同进程同一秒钟产生的ObjectId是唯一的。
ObjectId获取时间
从ObjectId的构造上来看,内部就嵌入了时间类型。我们肯定可以从中获取时间信息:即插入此文档时的时间。MongoDB对ObjectId对象提供了getTimestamp()方法来获取ObjectId的时间。
> a = new ObjectId()
ObjectId("5f03edbdd51fe110834f0ad9")
> a.getTimestamp()
ISODate("2020-07-07T03:36:29Z")
MongoDB默认在ObjectId上建立索引,是按照插入时间排序的,我们可以使用此索引进行查询和排序。
2.如何查看一次查询是否命中了索引?命中的是什么索引?如果索引命中情况不理想,如何指定使用某个索引进行查询?
命中索引情况可以使用explain进行查看,可以从下边winningPlan.inputStage.stage为IXSCAN可以看出索引已经命中,并且从keyPattern可以得知命中的索引名称
> db.weicol.find({name:"小红"}).explain("executionStats"))
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "weijiabintest.weicol",
"indexFilterSet" : false,
"parsedQuery" : {
"name" : {
"$eq" : "小红"
}
},
"winningPlan" : {
"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" : [
"[\"小红\", \"小红\"]"
]
}
}
},
"rejectedPlans" : [ ]
},
"executionStats" : {
"executionSuccess" : true,
"nReturned" : 1,
"executionTimeMillis" : 10,
"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" : {
"name" : 1
},
"indexName" : "name_1",
"isMultiKey" : false,
"multiKeyPaths" : {
"name" : [ ]
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "forward",
"indexBounds" : {
"name" : [
"[\"小红\", \"小红\"]"
]
},
"keysExamined" : 1,
"seeks" : 1,
"dupsTested" : 0,
"dupsDropped" : 0
}
}
},
"serverInfo" : {
"host" : "DESKTOP-2H4GAIN",
"port" : 27017,
"version" : "4.2.2",
"gitVersion" : "a0bbbff6ada159e19298d37946ac8dc4b497eadf"
},
"ok" : 1
}
强制使用指定索引:hint()方法,前提要是指定的索引已经被创建
db.users.find({“username”:“user1000”, “age”:30}).hint({“username”:1})
假设在users上有个{“a”: 1, “b”: 1}的索引,名称是"a_1_b_1",则如下两种方式等价:
db.users.find({“a”: 4, “b”: 5, “c”: 6}).hint({“a”: 1, “b”: 1})
db.users.find({“a”: 4, “b”: 5, “c”: 6}).hint(“a_1_b_1”)
也可以强迫查询不使用索引,做表扫描:
db.users.find().hint({"$natural":1})
3.聚合查询如何使用explain
db.collection.expalin().aggregate()
4.explain的关键字段
explain接收三个不同的参数,通过指定不同的参数,可以查看更详细的查询计划
- queryPlanner:queryPlanner是默认参数,添加queryPlanner参数的查询结果就是我们下边看到的查询结果。
- executionStats:executionStats会返回最佳执行计划的一些统计信息。
- allPlansExecution:allPlansExecution用来获取所有执行计划,结果参数基本与上文相同,这个参数和explain(true)效果相同。
queryPlanner:是默认参数
示例:
> db.weicol.find({name:"小红"}).explain()
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "weijiabintest.weicol",
"indexFilterSet" : false,
"parsedQuery" : {
"name" : {
"$eq" : "小红"
}
},
"queryHash" : "01AEE5EC",
"planCacheKey" : "01AEE5EC",
"winningPlan" : {
"stage" : "COLLSCAN",
"filter" : {
"name" : {
"$eq" : "小红"
}
},
"direction" : "forward"
},
"rejectedPlans" : [ ]
},
"serverInfo" : {
"host" : "DESKTOP-2H4GAIN",
"port" : 27017,
"version" : "4.2.2",
"gitVersion" : "a0bbbff6ada159e19298d37946ac8dc4b497eadf"
},
"ok" : 1
}
查询结果参数含义:
-
plannerVersion : 查询计划版本
-
namespace :要查询的集合(该值返回的是该query所查询的表)
-
indexFilterSet : 是否使用索引(针对该query是否有indexfilter)
-
parsedQuery :查询条件,此处为name=“小红”
-
winningPlan :最佳执行计划
-
winningPlan.stage :
- 最优执行计划的stage(查询方式),常见的有:
- COLLSCAN/全表扫描:(应该知道就是CollectionScan,就是所谓的“集合扫描”,和mysql中table scan/heap scan类似,这个就是所谓的性能最烂最无奈的由来)
- IXSCAN/索引扫描:(而是IndexScan,这就说明已经命中索引了)
- FETCH/根据索引去检索文档、SHARD_MERGE/合并分片结果、IDHACK/针对_id进行查询
- 最优执行计划的stage(查询方式),常见的有:
-
winningPlan.inputStage :用来描述子stage,并且为其父stage提供文档和索引关键字。
-
winningPlan.stage的child stage : 此处是IXSCAN,表示进行的是index scanning。
-
winningPlan.keyPattern :所扫描的index内容。
-
winningPlan.indexName : winning plan所选用的index。
-
winningPlan.isMultiKey:是否是Multikey,此处返回是false,如果索引建立在array上,此处将是true。
-
winningPlan.direction :此query的查询顺序,此处是forward,如果用了倒叙将显示backward。
-
filter :过滤条件。
-
winningPlan.indexBounds:winningplan所扫描的索引范围,如果没有制定范围就是[MaxKey, MinKey],这主要是直接定位到mongodb的chunck中去查找数据,加快数据读取。
-
rejectedPlans:拒绝的执行计划(其他执行计划(非最优而被查询优化器reject的)的详细返回,其中具体信息与winningPlan的返回中意义相同)
-
serverInfo :MongoDB服务器信息
executionStats示例:
> db.weicol.find({name:"小红"}).explain("executionStats")
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "weijiabintest.weicol",
"indexFilterSet" : false,
"parsedQuery" : {
"name" : {
"$eq" : "小红"
}
},
"winningPlan" : {
"stage" : "COLLSCAN",
"filter" : {
"name" : {
"$eq" : "小红"
}
},
"direction" : "forward"
},
"rejectedPlans" : [ ]
},
"executionStats" : {
"executionSuccess" : true,
"nReturned" : 1,
"executionTimeMillis" : 0,
"totalKeysExamined" : 0,
"totalDocsExamined" : 7,
"executionStages" : {
"stage" : "COLLSCAN",
"filter" : {
"name" : {
"$eq" : "小红"
}
},
"nReturned" : 1,
"executionTimeMillisEstimate" : 0,
"works" : 9,
"advanced" : 1,
"needTime" : 7,
"needYield" : 0,
"saveState" : 0,
"restoreState" : 0,
"isEOF" : 1,
"direction" : "forward",
"docsExamined" : 7
}
},
"serverInfo" : {
"host" : "DESKTOP-2H4GAIN",
"port" : 27017,
"version" : "4.2.2",
"gitVersion" : "a0bbbff6ada159e19298d37946ac8dc4b497eadf"
},
"ok" : 1
}
参数含义:
- executionSuccess 是否执行成功
- nReturned 返回的结果数
- executionTimeMillis 执行耗时
- totalKeysExamined 索引扫描次数
- totalDocsExamined 文档扫描次数
- executionStages 这个分类下描述执行的状态
- stage 扫描方式,具体可选值与上文的相同
- nReturned 查询结果数量
- ecutionTimeMillisEstimate 预估耗时
- works 工作单元数,一个查询会分解成小的工作单元
- advanced 优先返回的结果数
- docsExamined 文档检查数目,与totalDocsExamined一致。检查了总共的个documents,而从返回上面的nReturne数量
常见用法:
第一层,executionTimeMillis
最为直观explain返回值是executionTimeMillis值,指的是我们这条语句的执行时间,这个值当然是希望越少越好。
其中有3个executionTimeMillis,分别是:
executionStats.executionTimeMillis
该query的整体查询时间。
executionStats.executionStages.executionTimeMillisEstimate
该查询根据index去检索document获得数据的时间。
executionStats.executionStages.inputStage.executionTimeMillisEstimate
该查询扫描index所用时间。
第二层,index与document扫描数与查询返回条目数
这个主要讨论3个返回项,nReturned、totalKeysExamined、totalDocsExamined,分别代表该条查询返回的条目、索引扫描条目、文档扫描条目。
这些都是直观地影响到executionTimeMillis,我们需要扫描的越少速度越快。
对于一个查询,我们最理想的状态是:
nReturned=totalKeysExamined=totalDocsExamined
第三层,stage状态分析
那么又是什么影响到了totalKeysExamined和totalDocsExamined?是stage的类型。类型列举如下:
COLLSCAN:全表扫描
IXSCAN:索引扫描
FETCH:根据索引去检索指定document
SHARD_MERGE:将各个分片返回数据进行merge
SORT:表明在内存中进行了排序
LIMIT:使用limit限制返回数
SKIP:使用skip进行跳过
IDHACK:针对_id进行查询
SHARDING_FILTER:通过mongos对分片数据进行查询
COUNT:利用db.coll.explain().count()之类进行count运算
COUNTSCAN:count不使用Index进行count时的stage返回
COUNT_SCAN:count使用了Index进行count时的stage返回
SUBPLA:未使用到索引的
o
r
查
询
的
s
t
a
g
e
返
回
T
E
X
T
:
使
用
全
文
索
引
进
行
查
询
时
候
的
s
t
a
g
e
返
回
P
R
O
J
E
C
T
I
O
N
:
限
定
返
回
字
段
时
候
s
t
a
g
e
的
返
回
对
于
普
通
查
询
,
我
希
望
看
到
s
t
a
g
e
的
组
合
(
查
询
的
时
候
尽
可
能
用
上
索
引
)
:
F
e
t
c
h
+
I
D
H
A
C
K
F
e
t
c
h
+
i
x
s
c
a
n
L
i
m
i
t
+
(
F
e
t
c
h
+
i
x
s
c
a
n
)
P
R
O
J
E
C
T
I
O
N
+
i
x
s
c
a
n
S
H
A
R
D
I
N
G
F
I
T
E
R
+
i
x
s
c
a
n
C
O
U
N
T
S
C
A
N
S
O
R
T
K
E
Y
G
E
N
E
R
A
T
O
R
不
希
望
看
到
包
含
如
下
的
s
t
a
g
e
:
C
O
L
L
S
C
A
N
(
全
表
扫
描
)
,
S
O
R
T
(
使
用
s
o
r
t
但
是
无
i
n
d
e
x
)
,
不
合
理
的
S
K
I
P
,
S
U
B
P
L
A
(
未
用
到
i
n
d
e
x
的
or查询的stage返回 TEXT:使用全文索引进行查询时候的stage返回 PROJECTION:限定返回字段时候stage的返回 对于普通查询,我希望看到stage的组合(查询的时候尽可能用上索引): Fetch+IDHACK Fetch+ixscan Limit+(Fetch+ixscan) PROJECTION+ixscan SHARDING_FITER+ixscan COUNT_SCAN SORT_KEY_GENERATOR 不希望看到包含如下的stage: COLLSCAN(全表扫描),SORT(使用sort但是无index),不合理的SKIP,SUBPLA(未用到index的
or查询的stage返回TEXT:使用全文索引进行查询时候的stage返回PROJECTION:限定返回字段时候stage的返回对于普通查询,我希望看到stage的组合(查询的时候尽可能用上索引):Fetch+IDHACKFetch+ixscanLimit+(Fetch+ixscan)PROJECTION+ixscanSHARDINGFITER+ixscanCOUNTSCANSORTKEYGENERATOR不希望看到包含如下的stage:COLLSCAN(全表扫描),SORT(使用sort但是无index),不合理的SKIP,SUBPLA(未用到index的or),COUNTSCAN(不使用index进行count)
allPlansExecution示例:
> db.weicol.find({name:"小红"}).explain("allPlansExecution")
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "weijiabintest.weicol",
"indexFilterSet" : false,
"parsedQuery" : {
"name" : {
"$eq" : "小红"
}
},
"winningPlan" : {
"stage" : "COLLSCAN",
"filter" : {
"name" : {
"$eq" : "小红"
}
},
"direction" : "forward"
},
"rejectedPlans" : [ ]
},
"executionStats" : {
"executionSuccess" : true,
"nReturned" : 1,
"executionTimeMillis" : 0,
"totalKeysExamined" : 0,
"totalDocsExamined" : 7,
"executionStages" : {
"stage" : "COLLSCAN",
"filter" : {
"name" : {
"$eq" : "小红"
}
},
"nReturned" : 1,
"executionTimeMillisEstimate" : 0,
"works" : 9,
"advanced" : 1,
"needTime" : 7,
"needYield" : 0,
"saveState" : 0,
"restoreState" : 0,
"isEOF" : 1,
"direction" : "forward",
"docsExamined" : 7
},
"allPlansExecution" : [ ]
},
"serverInfo" : {
"host" : "DESKTOP-2H4GAIN",
"port" : 27017,
"version" : "4.2.2",
"gitVersion" : "a0bbbff6ada159e19298d37946ac8dc4b497eadf"
},
"ok" : 1
}
5.mongo索引实现原理
mongo索引类型
MongoDB支持多种类型的索引,包括单字段索引、复合索引、多key索引、文本索引等,每种类型的索引有不同的使用场合。
创建索引命令:
db.collection.createIndex( {}, { })
options参数:
background:后台异步创建索引
unique:是否是唯一索引
name:索引名称
sparse:是否是稀疏索引
partialFilterExpression:值为一个表达式,索引只引用与表达式相匹配的文档
- 单字段索引(Single Field Index)
db.person.createIndex( {age: 1} )
{age: 1} 代表升序索引,也可以通过{age: -1}来指定降序索引,对于单字段索引,升序/降序效果是一样的。
- 复合索引 (Compound Index)
db.person.createIndex( {age: 1, name: 1} )
复合索引是Single Field Index的升级版本,它针对多个字段联合创建索引,先按第一个字段排序,第一个字段相同的文档按第二个字段排序,依次类推,如下针对age, name这2个字段创建一个复合索引。
age字段的取值很有限,即拥有相同age字段的文档会有很多;而name字段的取值则丰富很多,拥有相同name字段的文档很少;显然先按name字段查找,再在相同name的文档里查找age字段更为高效。
关于复合索引的建立(相关连接点这里):
- 建立复合索引,索引中两字段前后顺序与查询条件字段在数量一致的情况下,顺序不影响使用索引查询。
- 当复合索引中的字段数量与查询条件字段数量不一致情况下,以复合索引字段前置优先规则。
使用复合索引
排序字段,需要谨慎,例如下面这个索引
db.person.ensureIndex({x: 1, y: -1})
以下几种排序情况,都可以使用该索引
// 顺序与索引顺序完全一致
db.person.find({}).sort({x: 1, y: -1})
// 复合索引前缀的单个字段排序顺序,可以使用该索引
db.person.find({}).sort({x: 1})
db.person.find({}).sort({x:-1})
// 顺序与索引建立时完全相反,可以命中
db.person.find({}).sort({x:-1, y: 1})
而下面这些排序情况,则不会命中该索引
// 字段的顺序非常重要,先按y排序则不能使用索引
db.person.find({}).sort({y: -1, x: 1})
// 排序顺序要么与索引完全相同,要么完全相反,否则都不能命中
db.person.find({}).sort({x:-1, y: -1})
// 不符合最左匹配原则
db.person.find({}).sort({y: -1})
- 尽量使用索引覆盖查询
Covered Queries (索引覆盖查询),简单地说,就是只查询索引中包含的字段,使得查询计划不必要再次回表多查一次文档数据。由于少了一次回表过程,因此查询效率必然比之较高。
使用时,因注意需同时满足以下条件:
1)所有的查询条件字段都是索引的一部分
2)查询返回的所有字段都是索引的一部分
3)查询条件中不包含等于null (i.e. {"field" : null} or {"field" : {$eq : null}})
的匹配条件
- 多key索引 (Multikey Index)
```shell
{"name" : "jack", "age" : 19, habbit: ["football, runnning"]}
db.person.createIndex( {habbit: 1} ) // 自动创建多key索引
db.person.find( {habbit: "football"} )
当索引的字段为数组时,创建出的索引称为多key索引,多key索引会为数组的每个元素建立一条索引,比如person表加入一个habbit字段(数组)用于描述兴趣爱好,需要查询有相同兴趣爱好的人就可以利用habbit字段的多key索引。
-
稀疏索引
稀疏索引(或者称间隙索引)就是只包含有索引字段的文档的条目,即使索引字段包含一个空值。也就是说间隙索引可以跳过那些索引键不存在的文档。因为他并非包含所有的文档,因此称为稀疏索引。与之相对的非稀疏索引或者说普通索引则包含所有的文档以及为那些不包含索引的字段存储null值。 -
其他索引类型
-
哈希索引(Hashed Index)是指按照某个字段的hash值来建立索引,目前主要用于MongoDB Sharded Cluster的Hash分片,hash索引只能满足字段完全匹配的查询,不能满足范围查询等。
-
地理位置索引(Geospatial Index)能很好的解决O2O的应用场景,比如『查找附近的美食』、『查找某个区域内的车站』等。
-
文本索引(Text Index)能解决快速文本查找的需求,比如有一个博客文章集合,需要根据博客的内容来快速查找,则可以针对博客内容建立文本索引。
-
-
索引额外属性
- 唯一索引 (unique index):保证索引对应的字段不会出现相同的值,比如_id索引就是唯一索引
- TTL索引:可以针对某个时间字段,指定文档的过期时间(经过指定时间后过期 或 在某个时间点过期)
- 部分索引 (partial index): 只针对符合某个特定条件的文档建立索引,3.2版本才支持该特性
- 稀疏索引(sparse index): 只针对存在索引字段的文档建立索引,可看做是部分索引的一种特殊情况
mongo索引使用的数据结构
mongo索引使用的数据结构为B树。
为什么使用B树而不是B+树?
首先看B树和B+树的特性:
B树的查询效率不固定,最好的情况是 O(1),因为它不需要再遍历去找到叶子节点。所以可以认为在做单一数据查询的时候,使用 B 树平均性能更好。但是,由于 B 树中各节点之间没有指针相邻,因此 B 树做一些数据遍历操作不那么适合。
B+ 是在 B 树上改进,它的数据都在叶子节点,同时叶子节点之间还加了指针形成链表。范围选择只需要找到首尾,通过链表就能把所有数据取出来了。
所以mysql之所以选择B+树,一方面也是出于范围查询考虑的,MongoDB 是聚合型数据库,而 B树恰好 key 和 data 域聚合在一起,并且mongo非关系型数据库更多的是单一数据查询,尽可能少的磁盘 IO 是提高性能的有效手段。