diff --git a/notes/Java_设计模式.md b/notes/Java_设计模式.md new file mode 100644 index 0000000..71143b8 --- /dev/null +++ b/notes/Java_设计模式.md @@ -0,0 +1,34 @@ +# 设计模式 + +## 软件设计的原则 + +### 开闭原则 + +定义:软件实体应当对扩展开放,对修改关闭。 + +### 里氏替换原则 + +定义:继承必须保证确保超类所拥有的性质在子类中仍然成立。即子类在继承父类时,除了添加新的方法来新增功能外,尽量避免重写父类方法,因为这会导致整个继承体系的复用性变差。 + +### 依赖倒置原则 + +定义:高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。其核心思想是要面向接口编程,而不是面向实现编程,这样可以降低耦合性,提高系统稳定性,提高代码的可读性和可维护性。 + +### 单一职责原则 + +定义:一个类应该有且仅有一个引起它变化的原则,否则应类应该被拆分。其核心思想是控制类的粒度大小、提高类的内聚性。 + +### 接口隔离原则 + +定义:一个类对另一个类的依赖应该建立在最小的接口上。其核心思想是要为每个特定的功能建立对应的接口,而不是在一个接口中试图去包含所有功能,既要保证相对独立,也要避免过多接口所导致的臃肿。 + +### 迪米特法则 (最少知道原则) + +定义:如果两个软件实体不需要直接通讯,那么就应该避免直接互相调用,而是通过第三方转发该调用,从而降低耦合度,保证模块的相对独立。 + +### 合成复用原则 (组合复用原则) + +定义:应该优先使用组合、聚合等关联关系来实现复用,其次才是考虑使用继承关系。 + +在软件设计当中应该尽量遵守这七大原则,它们之间要求的侧重点不同:开闭原则是总纲,它告诉我们要对扩展开放,对修改关闭;里氏替换原则告诉我们不要破坏继承体系;依赖倒置原则告诉我们要面向接口编程;单一职责原则告诉我们实现类要职责单一;接口隔离原则告诉我们在设计接口的时候要精简单一;迪米特法则告诉我们要降低耦合度;合成复用原则告诉我们要优先使用组合或者聚合关系复用,少用继承关系复用。 + diff --git a/notes/MongoDB_基础.md b/notes/MongoDB_基础.md new file mode 100644 index 0000000..b23c81b --- /dev/null +++ b/notes/MongoDB_基础.md @@ -0,0 +1,334 @@ +# MongoDB 基础 + +## 一、数据类型 + +### 1.1 BSON + +文档 (Documents) 是 MongoDB 中数据的基本单元,它是键值对的有序集合,在数据结构上类似于 JSON,但是由于 JSON 只支持基本的 6 种数据类型:null,布尔,数字,字符串,数组和对象,因此其表达能力有限,所以 MongoDB 基于 JSON 拓展出新的数据格式 BSON 。BSON (Binary JSON) 是类似 JSON 文档的二进制编码序列化格式,与 JSON 类似,但支持更多的数据类型。当前 MongoDB 4.0 的 BSON 支持以下所有的数据类型: + +| Type (类型) | Alias (别名) | Notes (注释) | +| :---------------------- | :-------------------- | :----------------------------------------------------------- | +| Double | “double” | 双精度型 | +| String | “string” | 字符串类型,默认的编码类型为 UTF-8 | +| Object | “object” | 对象类型 | +| Array | “array” | 数据类型 | +| Binary data | “binData” | 二进制数据 | +| Undefined | “undefined” | 该数据类型已经废弃 | +| ObjectId | “objectId” | 对象 id | +| Boolean | “bool” | 布尔值 | +| Date | “date” | 日志类型 | +| Null | “null” | 空 | +| Regular Expression | “regex” | 正则表达式 | +| DBPointer | “dbPointer” | 该数据类型已经废弃 | +| JavaScript | “javascript” | | +| Symbol | “symbol” | 该数据类型已经废弃 | +| JavaScript (with scope) | “javascriptWithScope” | | +| 32-bit integer | “int” | 32 位整型 | +| Timestamp | “timestamp” | 时间戳类型 | +| 64-bit integer | “long” | 64 位整型 | +| Decimal128 | “decimal” | 3.4 版本新增的数据类型,类似 Java 中的 BigDecimal 类型,
用于解决浮点型丢失精度的问题 | +| Min key | “minKey” | | +| Max key | “maxKey” | | + +### 1.2 ObjectId + +在上面的表格中有一个比较重要的数据类型是:ObjectId 。存储在 MongoDB 集合中的每个文档都有一个唯一的 `_id` 字段作为主键,可以在插入的时候手动指定,或者由程序自动生成,这个字段可以是任何的数据类型,默认是 ObjectId 类型。ObjectId 使用 12 字节的存储空间,是一个由 24 个十六进制数字组成的字符串,每个字符串可以存储两个十六进制数字: + ++ 前 4 个字节是从标准纪元开始的时间戳,单位为秒; + ++ 中 5 个字节是由两个部分组成:前 3 个字节是所在主机的唯一标识,通常是主机名的散列值,用于保证不同主机生成不同 ObjectId ,后两个字节是进程标识符 (PID) ,用于保证同一主机上不同进程产生不同的 ObjectId。 + ++ 最后 3 个字节是一个自动增加的计数器,确保相同主机上相同进程在同一秒产生的 ObjectId 也是不同的,即每个进程一秒最多可以拥有 16777216 个不同的 ObjectId (16777216 = (2\*\*8)\*\*3,1 个字节等于 8 位二进制) 。 + + +## 二、新增数据 + +```shell +db.collection.insert() +``` + +在 MongoDB 3.2 之前,插入数据的语法如上,可以用于插入单条或者多条数据。在 3.2 之后,MongoDB 为了增强 API 的语义,增加了如下两个 API ,分别用于显示表达插入单条数据和多条数据的行为。 + +```shell +db.collection.insertOne() +db.collection.insertMany() +``` + +新增单条数据的示例如下。额外需要说明的是,在插入文档前,文档所属的集合不必预先创建,程序会自动创建: + +```json +db.user.insertOne({ + name: "heibai", + age: 26, + birthday: new Date(1998,08,23), + createTime: new Timestamp(), + Hobby: ["basketball", "football", "tennis"] +}) +``` + +新增多条数据的示例如下: + +```json +db.user.insertMany([ + { + name: "hei", + age: 32, + birthday: new Date(1989,08,23), + createTime: new Timestamp(), + Hobby: ["basketball", "football", "tennis"] + }, + { + name: "ying", + age: 46, + birthday: new Date(1978,08,23), + createTime: new Timestamp(), + Hobby: ["basketball", "football", "tennis"] + } +]) + +# 此时会返回新插入的数据的ObjectId +{ + "acknowledged" : true, + "insertedIds" : [ + ObjectId("5d3d0489ad38cd3becc7b03b"), + ObjectId("5d3d0489ad38cd3becc7b03c") + ] +} +``` + +## 三、查询数据 + +### 3.1 基本 API + +查询数据的基本语法如下: + +```shell +db.collection.find(, ) +``` + ++ ``:用于指定查询条件,不加任何条件则默认查询集合中全部数据; ++ ``:可选操作,用于自定查询的返回字段,1 表示该字段包含在返回结果中,0 表示不返回,示例如下: + +```json +db.user.find({},{name: 1, ObjectId:-1})} +``` + +### 3.2 等值查询 + +想要查询某个字段等于指定值的数据,可以使用如下语法: + +```shell +db.user.find({name:"heibai"}) +``` + +上面的语法实际上是 `$eq` 操作的简写形式,如下: + +```json +db.user.find({name: {$eq: "heibai"}}) +``` + +所以如果你想要进行非等值查询,则可以使用 `$ne` 操作符,代表 not equal ,示例如下: + +```json +db.user.find({name: {$ne: "heibai"}}) +``` + +特别的,如果你想允许某个字段等于多个值,可以使用 `$in` 操作符,示例如下: + +```json +db.user.find({name: {$in:["heibai","ying"]} }) +``` + +### 3.3 范围查询 + +Mongodb 提供了比较操作符 `$lt`、`$lte`、`$gt` 和 `$gte` ,分别对应 <、 <=、 >和 >= ,主要用于范围查查询,示例如下: + +```json +db.user.find({age: {$gt: 20, $lt: 40}}) +``` + +### 3.4 逻辑查询 + +Mongodb 提供了逻辑操作符 `$or`、`$and`、`$not`、`$nor` ,用于处理多个条件间的逻辑关系,示例如下: + +查询姓名为 heibai 或者年龄大于 30 岁的所有用户,此时可以使用 $or 操作符: + +```json +db.user.find( { $or: [{ name: "heibai" }, { age: { $gt: 30 } }] }) +``` +查询所有姓名不是以 hei 开头的所有用户,此时可以使用 $not 操作符来配合正则表达式: + +```json +db.user.find({name: {$not: /^hei*/}}) +``` + +如果文档中存在 name 字段,则它的值不能为 heibai,如果文档中存在 age 字段,则它的值不能大于 30 ,只有满足以上两个条件的文档才会被查询出来,示例如下: + +```json +db.user.find( { $nor: [{ name: "heibai" }, { age: { $gt: 30 } }] }) +``` + +$and 操作符的使用率比较低,因为此时更好的方式是把多个条件写到同一个对象中,从而减少额外嵌套。 + +### 3.5 集合查询 + +如果需要查询个人爱好中有 football 的所有用户,即只要集合 Hobby 中存在 football 即可,对应的查询方法如下: + +```json +db.user.find({Hobby: "football"}) +``` +如果想要获取集合中指定位置等于指定值的文档,对应的查询方法如下: + +```json +db.user.find({"Hobby.2": "football"}) +``` +如果想要约束集合必须包含多个指定值,此时可以使用 $all 操作符: + +```json +db.user.find({Hobby:{ $all: ["football", "tennis"]}}) +``` +查询时如果只想返回集合的部分内容,则可以使用 $slice ,$slice 接收一个参数 n,正数表示获取集合的前 n 个参数,负数表示获取集合末尾的 n 个参数,示例如下: + +```json +db.user.find({name: "heibai"},{Hobby:{$slice: 2}}) +``` + +### 3.6 额外操作 + +在查询操作之后,MongoDB 还提供了三个额外的函数 `skip`、`sort` 和 `limit` ,分别用于表示跳过一定量的数据、按照指定规则对数据进行排序和限制返回数据的量,示例如下: + +```shell +db.user.find({}) + .skip(1) + .sort({_id:-1}) + .limit(10) +``` + +## 四、修改数据 + +### 4.1 基本 API + +MongoDB 提供了以下三个 API,分别用于修改单条数据、多条数据和执行单条数据替换: + +```shell +db.collection.replaceOne(, , ) +db.collection.updateOne(, , ) +db.collection.updateMany(, , ) +``` + ++ ``:过滤条件,用于查询需要修改的数据; ++ ``:更改操作或新文档数据; ++ ` `:可选操作,常用的可选操作是 `upsert` ,当其为 true 时,代表如果按照过滤条件没有找到对应的文档,则将待更改的数据插入到集合中;当其为 false 时,如果没有找到数据,则不执行任何操作。示例如下: + +```json +db.user.replaceOne( + { _id: ObjectId("5d3d00a4ad383d3becc7b03a")}, + { + name: "danrenying", + age: 32, + birthday: new Date(1995,08,23), + createTime: new Timestamp(), + Hobby: ["basketball", "football", "tennis"] + }, + {upsert : true} +) +``` + +### 4.2 常规修改器 + +想要对数据进行修改,必须明确表达修改行为,在 MongoDB 中这是通过修改器来实现的,常用的修改器如下: + +#### 1. $set + +用于修改具体的字段,如果待修改的字段不存在,则会新增该字段。示例如下: + +```json +db.user.updateOne( + { name: "danrenying"}, + { $set: {age: 66} } +) +``` + +#### 2. $inc + +用于对指定字段的值进行增加或减少,示例如下: + +```json +db.user.updateOne( + { name: "danrenying"}, + { $inc: {age: -10} } +) +``` + +### 4.3 数组修改器 + +在修改操作中,比较复杂的是对数组数据的修改,为了解决这个问题,MongoDB 提供了一系列的修改器,用于数组操作: + +#### 1. $push + +用于往数组中新增数据,示例如下。使用 `$each` 可以一次添加多个元素: + +```json +db.user.updateOne( + { name: "danrenying"}, + { $push: {"Hobby": {$each: ["film","music"]}} } +) +``` + +#### 2. $addToSet + +该修改器可以把数组当做集 (set) 来使用,即只能添加当前数组中不存在的数据,示例如下: + +```json +db.user.updateOne( + { name: "danrenying"}, + { $addToSet: {"Hobby": {$each: ["film","music"]}} } +) +``` + +#### 3. $pop + +该修改器可以从数组任意一端删除元素,`-1` 代表从数组头删除元素,`1` 代表从数组尾删除元素,示例如下: + +```json +db.user.updateOne( + { name: "danrenying"}, + { $pop: {"Hobby": -1} } +) +``` + +#### 4. index + +对于数组还可以直接修改指定下标位置的元素,示例如下: + +```shell +db.user.updateOne( + { name: "danrenying"}, + { $set: {"Hobby.0": "Cooking"} } +) +``` + +## 五、删除数据 + +MongoDB 提供了以下两个 API 用于删除操作: + +```shell +db.collection.deleteMany() +db.collection.deleteOne() +``` + +使用示例如下: + +```json +db.user.deleteOne( + { name: "danrenying"} +) +``` + + + +## 参考资料 + ++ 官方文档:[MongoDB CRUD Operations](https://docs.mongodb.com/manual/crud/) + ++ Kristina Chodorow . MongoDB权威指南(第2版). 人民邮件出版社 . 2014-01 \ No newline at end of file diff --git a/notes/MongoDB_基础操作.md b/notes/MongoDB_基础操作.md deleted file mode 100644 index e69de29..0000000 diff --git a/notes/MongoDB_索引.md b/notes/MongoDB_索引.md index e69de29..b3b2b21 100644 --- a/notes/MongoDB_索引.md +++ b/notes/MongoDB_索引.md @@ -0,0 +1,357 @@ +# MongoDB 索引 + +## 一、索引简介 + +### 1.1 创建索引 + +和大多数关系型数据库一样,MongoDB 支持使用索引来进行查询优化,采用类似 B-Tree 的数据结构来储存索引和文档的位置信息,同样也支持前缀索引和覆盖索引。在当前最新的 MongoDB 4.0 中,索引的创建语法如下: + +```shell +db.collection.createIndex( , ) +``` + ++ ``:用于指定建立索引的字段和升降序等属性; ++ ` `:可选配置,通常用于指定索引的性质。 + +为方便后面的演示,这里先插入部分测试数据,并针对 name 字段创建一个索引: + +```shell +db.user.insertMany([ + { + name: "heibai", + age: 26, + birthday: new Date(1998,08,23), + createTime: new Timestamp(), + Hobby: ["basketball", "football", "tennis"] + }, + { + name: "hei", + age: 32, + birthday: new Date(1989,08,23), + createTime: new Timestamp(), + Hobby: ["basketball", "tennis"] + }, + { + name: "ying", + age: 46, + birthday: new Date(1978,08,23), + createTime: new Timestamp(), + Hobby: ["tennis"] + } +]) + + +# 创建索引, -1表示以降序的顺序存储索引 +db.user.createIndex( { name: -1 } ) +``` + +### 1.2 查看索引 + +创建索引后可以使用 `getIndexes()` 查看集合的所有索引信息,示例如下: + +```shell +db.user.getIndexes() +``` + +从输出中可以看到默认的索引名为:字段名+排序规则。这里除了我们为 name 字段创建的索引外,集合中还有一个 `_id` 字段的索引,这是程序自动创建的,用于禁止插入相同 `_id` 的文档。 + +```json +{ + "v" : 2, + "key" : { + "_id" : 1 + }, + "name" : "_id_", + "ns" : "test.user" +}, + +{ + "v" : 2, + "key" : { + "name" : -1 + }, + "name" : "name_-1", + "ns" : "test.user" +} +``` + +## 二、索引的类型 + +当前 MongoDB 4.x 支持以下六种类型的索引: + +### 2.1 单字段索引 + +支持为单个字段建立索引,这是最基本的索引形式,上面我们针对 name 字段创建的索引就是一个单字段索引。需要特别说明的是,在为 name 字段创建索引时,我们为其指定了排序规则。但实际上,在涉及单字段索引的排序查询中,索引键的排序规则是无关紧要,因为 MongoDB 支持在任一方向上遍历索引。即以下两个查询都可以使用 `name_-1` 索引进行排序: + +```shell +db.user.find({}).sort({name:-1}) +db.user.find({}).sort({name:1}) +``` + +当前大多数数据库都支持双向遍历索引,这和存储结构有关 (如下图)。在 B-Tree 结构的叶子节点上,存储了索引键的值及其对应文档的位置信息,而每个叶子节点间则类似于双向链表,所以如下图既可以从值为 4 的索引遍历到值为 92 的索引,反之亦然。 + +![b-tree](D:\Full-Stack-Notes\pictures\b-tree.png) + + + +### 2.2 复合索引 + +支持为多个字段创建索引,示例如下: + +```shell +db.user.createIndex( { name: -1,birthday: 1} ) +``` + +需要注意的是 MongoDB 的复合索引具备前缀索引的特征,即如果你创建了索引 `{ a:1, b: 1, c: 1, d: 1 }`,那么等价于在该集合上,还存在了以下三个索引,这三个隐式索引同样可以用于优化查询和排序操作: + +```json +{ a: 1 } +{ a: 1, b: 1 } +{ a: 1, b: 1, c: 1 } +``` + +所以应该尽量避免创建冗余的索引,冗余索引会导致额外的性能开销。即如果你创建了索引 `{ name: -1, birthday: 1} `,那么再创建 `{name:-1}` 索引,就属于冗余创建。 + +对于复合索引还需要注意它在排序上的限制,例如索引 `{a:1, b:-1}` 支持 `{a:1, b:-1}` 和 `{a:-1, b:1}` 形式的排序查询,但不支持 `{a: - 1, b:-1} ` 或 `{a:1, b:1}` 的排序查询。即字段的排序规则要么与索引键的排序规则完全相同,要么完全相反,此时才能进行双向遍历查找。 + +### 2.3 多键索引 + +如果索引包含类型为数组的字段,MongoDB 会自动为数组中的每个元素创建单独的索引条目,这就是多键索引。MongoDB 使用多键索引来优化查询存储在数组中的内容。创建示例如下: + +```shell +db.user.createIndex( { Hobby: 1 } ) +``` + +### 2.4 哈希索引 + +为了支持基于哈希分片,MongoDB 提供了哈希索引,通过对索引值进行哈希运算然后计算出所处的分片位置。语法如下: + +```shell +db.collection.createIndex( { _id: "hashed" } ) +``` + +采用哈希运算得到的结果值会比较分散, 所以哈希索引不能用于范围查询,只能用于等值查询。 + +### 2.5 地理空间索引 + +为了支持对地理空间坐标数据的有效查询,MongoDB提供了两个特殊索引: + +- 使用平面几何的 2d 索引,主要用于平面地图数据 (如游戏地图数据)、连续时间的数据; +- 使用球形几何的 2dsphere 索引,主要用于实际的球形地图数据。 + +这些数据通常是用于解决实际的地理查询,如附近的美食、查询范围内所有商家等功能。其创建语法如下: + +```shell +db..createIndex( { : "2d" , + : } , + { } ) +db.collection.createIndex( { : "2dsphere" } ) +``` + +### 2.6 文本索引 + +MongoDB 支持全文本索引,用于对指定字段的内容进行全文检索。其创建语法如下: + +```shell +db..createIndex( { field: "text" } ) +``` + +需要注意的是一个集合最多可以有一个文本索引,但一个文本索引可以包含多个字段,语法如下: + +```shell +db..createIndex( + { + field0: "text", + field1: "text" + } + ) +``` + +创建文本索引是一个非常昂贵的操作,因为创建文本索引时需要对文本进行语义分析和有效拆分,还需要将拆分后的关键词存储在内存中,这对设备的运算能力和存储空间都有非常高的要求,同时也会降低 MongoDB 的性能,所以需要谨慎使用。 + +## 三、索引的性质 + +创建索引时,可以传入第二个参数 ` ` 用于指定索引的性质,常用的索引性质如下: + +### 3.1 唯一索引 + +唯一索引可以确保在同一个集合中唯一索引列的值只出现一次。 示例如下: + +```shell +db.user.createIndex( { name: -1,birthday: 1}, { unique: true }) +``` + +此时再执行下面的操作就会报错,因为 name = heibai 并且 birthday = new Date(1998,08,23) 的数据已经存在。 + +```shell +db.user.insertOne({ + name: "heibai", + birthday: new Date(1998,08,23) +}) +``` + +上面这种情况比较明显,但是如果你执行下面这个操作两次,你会发现只有第一次能够插入成功,第二个就会报 duplicate key 异常。这是因为在唯一索引的约束下,不存在这种状态也会被当做一种唯一值。 + +```shell +db.user.insertOne({ + age: 12 +}) +``` + +想要解决这个问题,就需要用到索引的稀疏性。 + +### 3.2 稀疏性 + +为了解决上面的问题,我们需要为索引添加稀疏性。由于索引不能修改,所以只能先将上面的索引先删除,然后再创建,并为其指定 `sparse` 属性为 true,具体的创建语句如下: + +```shell +db.user.dropIndex("name_-1_birthday_1") +db.user.createIndex( { name: -1,birthday: 1}, { unique: true,sparse: true}) +``` + +此时你再多次执行上面的插入语句就能插入成功。原因是对于稀疏索引而言,它仅包含具有索引字段的文档的索引信息,即使索引字段的值为 null 也可以,但不能缺少相应的索引字段。如果缺少,则相应的文档就不会被包含在索引信息中。 + +### 3.3 部分索引 + +部分索引主要用于为符合条件的部分数据创建索引,它必须与 partialFilterExpression 选项一起使用。 partialFilterExpression 选项可以使用以下表达式来确定数据范围: + +- 等式表达式(即 字段: 值 或使用 $eq 运算符); +- $exists: true 表达式; +- $gt、$gte、$lt、$lte 操作符; +- $type 操作符; +- 处于顶层的 $and 操作符。 + +使用示例如下: + +```shell +db.user.createIndex( + { name: -1 }, + { partialFilterExpression: { age: { $gt: 30 } } } +) +``` + +### 3.4 TTL 索引 + +TTL 索引允许为每个文档设置一个超时时间,当一个文档达到超时时间后,就会被删除。TTL索引的到期时间等于索引字段的值 + 指定的秒数,示例如下: + +```shell +db.user.createIndex( { "birthday": 1 }, { expireAfterSeconds: 60 } ) +``` + +这里我们在 birthday 字段上建立 TTL 索引只是用于演示,实际上 TTL 索引主要是用于那些只需要在特定时间内保存的数据,如会话状态、临时日志等。在使用 TTL 索引时,还有以下事项需要注意: + +- TTL 属性只能用于单字段索引,不支持复合索引。 +- 建立 TTL 索引的字段的类型只能是 Date 类型,时间戳类型也不可以。 +- 如果字段是数组,并且索引中有多个日期值,则 MongoDB 会使用数组中的最早的日期值来计算到期时间。 +- 如果文档中的索引字段不是日期或包含日期值的数组,则文档将不会过期。 +- 如果文档不包含索引字段,则文档不会过期。 + +## 四、删除索引 + +删除索引的语法比较简单,只需要调用 `dropIndex` 方法,可以传入索引的名称也可以传入索引的定义,示例如下: + +``` +db.user.dropIndex("name_-1") +db.user.dropIndex({ name: -1,birthday: 1}) +``` + +如果想要删除全部的索引,则可以调用 `dropIndexes` 方法,需要注意的是建立在 `_id` 上的默认索引是不会被删除的。 + +```shell +db.collection.dropIndexes() +``` + +另外这个命令会获取对应数据库的写锁,并会阻塞其他操作,直到索引删除完成。 + +## 五、EXPLAIN + +### 5.1 输出参数 + +MongoDB 的 `explain()` 方法和 MySQL 的 explain 关键字一样,都是用于显示执行计划的相关信息。示例如下: + +```shell +db.user.find({name:"heibai"},{name:1,age:1}).sort({ name:1}).explain() +``` + +此时执行计划的部分输出如下: + +```json +"inputStage" : { + "stage" : "FETCH", + "inputStage" : { + "stage" : "IXSCAN", + "keyPattern" : { + "name" : -1, + "birthday" : 1 + }, + "indexName" : "name_-1_birthday_1", + "isMultiKey" : false, + "multiKeyPaths" : { + "name" : [ ], + "birthday" : [ ] + }, + "isUnique" : true, + "isSparse" : true, + "isPartial" : false, + "indexVersion" : 2, + "direction" : "backward", + "indexBounds" : { + "name" : [ + "[\"heibai\", \"heibai\"]" + ], + "birthday" : [ + "[MaxKey, MinKey]" + ] + } + } +} +``` + +输出结果中内层的 inputStage.stage 的值为 `IXSCAN`,代表此时用到了索引进行扫描,并且 indexName 字段显示了对应的索引为 name_-1_birthday_1。而外层 inputStage.stage 的值为 `FETCH`,代表除了从索引上获取数据外,还需要去对应的文档上获取数据,因为 age 信息并不存储在索引上。这个输出可以证明 MongoDB 是支持前缀索引的,且单键索引支持双向扫描。 + +### 5.2 覆盖索引 + +这里我们对上面的查询语句略做修改,不返回 age 字段和默认的 _id 字段,语句如下: + +```shell +db.user.find({name:"heibai"},{_id:0, name:1}).sort({ name:1 }).explain() +``` + +此时输出结果如下。可以看到该查询少了一个 `FETCH` 阶段。代表此时只需要扫描索引就可以获取到所需的全部信息,这种情况下 name_-1_birthday_1 索引就是这一次查询操作的覆盖索引。 + +```json +"inputStage" : { + "stage" : "IXSCAN", + "keyPattern" : { + "name" : -1, + "birthday" : 1 + }, + "indexName" : "name_-1_birthday_1", + "isMultiKey" : false, + "multiKeyPaths" : { + "name" : [ ], + "birthday" : [ ] + }, + "isUnique" : true, + "isSparse" : true, + "isPartial" : false, + "indexVersion" : 2, + "direction" : "backward", + "indexBounds" : { + "name" : [ + "[\"heibai\", \"heibai\"]" + ], + "birthday" : [ + "[MaxKey, MinKey]" + ] + } +} +``` + + + +## 参考资料 + +1. 官方文档:[Indexes](https://docs.mongodb.com/manual/indexes/) 、[sort-on-multiple-fields](https://docs.mongodb.com/manual/tutorial/sort-results-with-indexes/#sort-on-multiple-fields) +2. Kristina Chodorow . MongoDB权威指南(第2版). 人民邮件出版社 . 2014-01 + diff --git a/notes/MongoDB_聚合.md b/notes/MongoDB_聚合.md index e69de29..342b9a5 100644 --- a/notes/MongoDB_聚合.md +++ b/notes/MongoDB_聚合.md @@ -0,0 +1,367 @@ +# MongoDB 聚合操作 + +## 一、聚合简述 + +在日常开发中,我们通常需要对存储数据进行聚合分析后,再返回给客户端。MongoDB提供了三种聚合的方式,分别是聚合管道,map-reduce 函数和单用途聚合方法。 + +## 二、聚合管道 + +MongoDB 的聚合操作类似于流水线处理,文档会依次进入多个管道阶段并执行相应的操作。这里先插入部分演示数据: + +```shell +db.employees.insertMany([ + { + emp_no: 10001, + name: {firstName:"Georgi",lastName:"Facello"}, + age: 26, + gender: "F", + hobby: ["basketball", "football"] + }, + { + emp_no: 10002, + name: {firstName:"Bezalel",lastName:"Simmel"}, + age: 32, + gender: "M", + hobby: ["basketball", "tennis"] + }, + { + emp_no: 10003, + name: {firstName:"Parto",lastName:"Bamford"}, + age: 46, + gender: "M", + hobby: [] + }, + { + emp_no: 10004, + name: {firstName:"Chirstian",lastName:"Koblick"}, + age: 40, + gender: "F", + hobby: ["football", "tennis"] + } +]) +``` + +一个简单的聚合操作如下,这个聚合操作会经过两个阶段的数据处理: + ++ 第一个管道阶段为 $match:会筛选出所有性别值为 F 的雇员的文档,然后输出到下一个管道操作中; ++ 第二个管道阶段为 $project:用于定义返回的字段内容,这里返回 fullname 字段,它由 firstName + lastName 组成。 + +```shell +db.employees.aggregate([ + { $match: { gender: "F" } }, + { $project: + { fullName: + { $concat: ["$name.firstName", "$name.lastName"]} + } + } +]) +``` + +所以最后的输出结果如下: + +```json +{ + "_id" : ObjectId("5d3fe6488ba16934ccce999d"), + "fullName" : "GeorgiFacello" +}, +{ + "_id" : ObjectId("5d3fe6488ba16934ccce99a0"), + "fullName" : "ChirstianKoblick" +} +``` + +在当前最新的 MongoDB 4.x 中,MongoDB 提供了将近 30 个管道阶段,用于满足不同数据处理的需求。以下主要介绍常用几个管道阶段,如果想要了解全部的管道阶段,可以参见官方文档:[Aggregation Pipeline Stages](https://docs.mongodb.com/manual/reference/operator/aggregation-pipeline/) + +### 1.1 $match + +$match 主要用于筛选符合条件的数据,通常应该把 $match 放在尽量靠前的位置,这时候它会利用索引来优化查询,同时还可以降低后续阶段所需要处理的数据量。示例如下: + +```shell +db.employees.aggregate([ + { $match: { gender: "F" } } +]) +``` + +### 1.2 $project + +$project 主要用于定义需要返回的字段,1 代表包含该字段,0 代表不包含,除了可以作用于顶层字段外,还可以作用于内嵌字段。同时 $project 还支持使用表达式将多个字段或变量进行组合,并作为新的字段返回。示例如下: + +```shell +db.employees.aggregate([ + { + $project: { + _id: 0, + "name.firstName": 1, + gender: 1, + fullName: { $concat: ["$name.firstName", "$name.lastName"] } + } + } +]) +``` + +从 MongoDB 3.6 开始,还可以在聚合表达式中使用 $project + 变量 REMOVE 来按照条件定义返回字段,设置为 REMOVE 变量的字段将会从 $projection 的输出中排除。示例如下: + +```shell +db.employees.aggregate([ + { + $project: { + hobby: { + $cond: { + if: { $eq: [ [], "$hobby" ] }, + then: "$$REMOVE", + else: "$hobby" + } + } + } + } +]) +``` + +这里判断当文档的 hobby 属性为空数组时,则其 hobby 属性不会被输出到下一个管道阶段。 + +### 1.3 $group + +$group 管道阶段和大多数关系型数据库中的 group by 字句功能类似,都是用于分组计算。示例如下: + +```json +db.employees.aggregate( + [ + { $group : { + _id : "$gender", + totalAge: { $sum: "$age"}, + avgAge: { $avg: "$age" }, + count: { $sum: 1 } + } + } + ] +) +``` + +上面的语句会按照性别进行分组,并计算分组后两组人的总年龄、平均年龄和总人数,输出如下: + +```json +{ + "_id" : "M", + "totalAge" : 78, + "avgAge" : 39, + "count" : 2 +}, +{ + "_id" : "F", + "totalAge" : 66, + "avgAge" : 33, + "count" : 2 +} +``` + +如果你想计算所有员工的年龄总和、平均年龄、以及员工总数,则可以将 $group 管道阶段的 _id 字段设置为 null ,语句如下: + +```shell +db.employees.aggregate( + [ + { $group : { + _id : null, + totalAge: { $sum: "$age"}, + avgAge: { $avg: "$age" }, + count: { $sum: 1 } + } + } + ] +) + +# 输出如下 +{ + "_id" : null, + "totalAge" : 144, + "avgAge" : 36, + "count" : 4 +} +``` + +### 1.4 $unwind + +$unwind 将文档按照数组中的每一个元素进行拆分,类似于大多数流式计算中的 flatMap 算子。其语法格式如下: + +```shell +{ + $unwind: + { + path: , + includeArrayIndex: , + preserveNullAndEmptyArrays: + } +} +``` + ++ **path**:用于展开的数组字段; + ++ **includeArrayIndex**:用于显示对应元素在原数组的位置信息; + ++ **preserveNullAndEmptyArrays**:如果用于展开的字段值为 null 或空数组时,则对应的文档不会被输出到下一阶段。如果想要输出到下一阶段则需要将该属性设置为 true。示例语句如下: + +```json +db.employees.aggregate( [ + {$project: {_id: 0, emp_no: 1, hobby:1}}, + { $unwind: + { path: "$hobby", + includeArrayIndex: "arrayIndex", + preserveNullAndEmptyArrays: true + } + } +] ) +``` + +此时输出内容如下。如果 preserveNullAndEmptyArrays 的值为 false 或者没有设置,则 10003 这条数据不会被输出。 + +```json +{"emp_no":10001,"hobby":"basketball","arrayIndex":0}, +{"emp_no":10001,"hobby":"football","arrayIndex":1}, + +{"emp_no":10002,"hobby":"basketball","arrayIndex":0}, +{"emp_no":10002,"hobby":"tennis","arrayIndex":1}, + +{"emp_no":10003,"arrayIndex":null}, + +{"emp_no":10004,"hobby":"football","arrayIndex":0}, +{"emp_no":10004,"hobby":"tennis","arrayIndex":1} +``` + +### 1.5 $sort + +$sort 主要用于排序操作,需要注意的是如果可以,应当尽量将该操作放置在管道的第一阶段,从而可以利用索引进行排序,否则就需要使用内存进行排序,这时排序操作就会变得相当昂贵,需要额外的内存和计算资源的开销。 + +示例如下: + +```shell +db.employees.aggregate([ + {$skip: 2} , + {$sort: {age: 1}}, + {$limit: 10} +]) +``` + +### 1.6 $limit + +限制返回文档的数量。 + +### 1.7 $skip + +跳过一定数量的文档。 + +### 1.8 $lookup + +```shell +db.titles.insertMany([ + { + emp_no: 10001, + title: "Senior Engineer" + }, + { + emp_no: 10002, + title: "Staff" + }, + { + emp_no: 10003, + title: "Senior Engineer" + }, + { + emp_no: 10004, + title: "Engineer" + }, + { + emp_no: 10004, + title: "Senior Engineer " + } +]) +``` + +```shell +db.employees.aggregate([ + { + $lookup: + { + from: "titles", + localField: "emp_no", + foreignField: "emp_no", + as: "emp_title" + } + } +]) + + +db.employees.aggregate([ + { + $lookup: + { + from: "titles", + let: { gender: "$gender"}, + pipeline: [ + { $match: + { $expr: { $eq: [ "$$gender","M"]}} + } + ], + as: "emp_title" + } + } +]) + + + +db.employees.aggregate([ + { + $lookup: + { + from: "titles", + pipeline: [ + { $match: + { $expr: { $eq: [ "$title","M"]}} + } + ], + as: "emp_title" + } + } +]) +``` + +### 1.9 $out + +$out 用于将数据写入指定的集合,它必须是管道中的最后一个阶段。如果指定的集合不存在,则会自动新建;如果指定的集合存在,它会覆盖原有集合的数据。其实际的步骤如下: + +- 创建临时集合; +- 将索引从现有集合复制到临时集合; +- 将文档插入临时集合中; +- 调用 db.collection.renameCollection(target, true) 方法将临时集合重命名为目标集合。 + +$out 的使用示例如下: + +```shell +db.employees.aggregate([ + { $out: "emps"} +]) +``` + +### 1.10 自动优化 + +在大多数情况下 MongoDB 会按照我们定义管道的先后顺序执行管道操作,但是某些情况下,MongoDB 会在不影响结果的前提下,改变管道执行顺序,从而获得更好的性能表现。常见的优化策略如下: + +#### $project or $addFields + $match + +当投影操作后面有匹配操作时,MongoDB 会将 $match 阶段中不需要进行投影操作的字段的过滤条件提前到投影操作前执行。 + +#### $sort + $match + +当排序操作后面有匹配操作时,会将匹配操作提前,以减少需要排序的数据量。 + +#### $project + $skip + +当投影操作后面有跳过操作时,会先执行跳过操作,从而减少需要进行投影操作的数据量。 + +#### $sort + $limit + +当排序操作在限制操作之前时,如果没有中间阶段会修改文档数量 (例如 $unwind,$group),优化器会将 $limit 合并到 $sort中。如果在 $sort 和 $limit 之间存在修改文档数量的管道阶段,MongoDB 将不会执行合并。 + +了解这些优化策略可以有助于我们在开发中合理设置管道的顺序。想要了解全部的优化策略,可以参阅 MongoDB 的官方文档:[Aggregation Pipeline Optimization](https://docs.mongodb.com/manual/core/aggregation-pipeline-optimization/) + +## 三、MapReduce + +## 四、单用途聚合方法 \ No newline at end of file diff --git a/pictures/b-tree.png b/pictures/b-tree.png new file mode 100644 index 0000000..1b13b0f Binary files /dev/null and b/pictures/b-tree.png differ