本文主要介绍 MongoDB 数据库的基本应用,采用“CentOS Linux release 7.6.1810 (Core)”的 Linux 版本作为 MongoDB 的安装系统。主要包括以下内容:
1. 聚合操作
2. 全文检索
3. 地理空间查询
4. 数据建模
1. 聚合操作
MongoDB 聚合计算可以采用 PipeLine、MapReduce 两种方式,在速度上Pipeline 比 MapReduce 要快,但是 MapReduce 可以在多个 Server 上进行并行计算,它是一个分布式的计算模型。
1) 使用 PipeLine 方式进行聚合操作
PipeLine 的核心是采用db.collection.aggregate() 方法完成聚合运算,它有两个限制,第一个限制:单个的聚合运算耗费的内存不能超过20%;第二个限制:返回的结果集限制在16M,所以,它不适合数据量比较大的运算。
1. $mathch、$project运算符
$match: 过滤进入 PlpeLine 的数据。
$procject: 指定提取的列,其中:1表示提取列,0不提取列。
1) 获取指定的列
db.emp.aggregate(
{$match:{"deptno":{$eq:10}}},
{$project:{"ename":1, "sal":1, "deptno":1}})
2. $group、$sum 运算符
1) 获取每个部门的工资总额
db.emp.aggregate(
{$project:{"deptno":1, "sal":1}},
{$group:{"_id":"$deptno", "salTotal":{$sum:"$sal"}}})
2) 获取每个部门、每个职位的工资总额
db.emp.aggregate(
{$project:{"deptno":1, "job":1, "sal":1}},
{$group:{"_id":{"deptno":"$deptno","job":"$job"}, "salTotal":{$sum:"$sal"}}})
同等SQL: SELECT deptno, job, SUM(sal) FROM emp GROUP BY deptno, job
2) 使用 MapReduce 方式进行聚合操作
特点:可以实现非常复杂的计算逻辑;并且可以在多台 Server 进行并行计算。
缺点:执行速度会比较慢。
参考文档:https://docs.mongodb.com/manual/reference/operator/aggregat ion/
1. MapReduce 的原理
来源于 Google 的一篇论文"MapReduce",问题:PageRank 搜索排名(比如百度搜索出来的网页排名)
2. 示例
1) 计算每个职位的总人数
var map1 = function() { emit(this.job, 1) }
var reduce1 = function(job, count) { return Array.sum(count) }
db.emp.mapReduce(map1, reduce1, {"out":"mrdemo1"})
db.mrdemo1.find()
数据计算流程:
2) 计算每个部门的工资总额
var map2 = function() { emit(this.deptno, this.sal) }
var reduce2 = function(deptno, sal) { return Array.sum(sal) }
db.emp.mapReduce(map2, reduce2, {"out":"mrdemo2"})
db.mrdemo2.find()
3. Map 和 Reduce 的 TroubleShooting
1) 跟踪 Map 的执行
1. 重写 emit 方法
var emit = function(key, value) {
print("emit exec, key:" + key + ", value:" + value);
}
var map2 = function() { emit(this.deptno, this.sal) }
2. 测试一条数据
emp7839 = db.emp.findOne({"_id":7839})
map2.apply(emp7839)
3. 测试多条数据
var mycursor = db.emp.find()
while(mycursor.hasNext()) {
var mydoc = mycursor.next();
map2.apply(mydoc);
}
2) 跟踪 Reduce 的执行
1. 简单测试
var myvalue1 = [5, 10, 20]
var reduce1 = function(job, count) {
return Array.sum(count);
}
测试:reduce1("mykey", myvalue1)
2. Reduce 的value 包含多个值
var myvalue2 = [{"sal":1000, comm:5},{"sal":2000, comm:8}]
var reduce2 = function(key, values) {
result = {"sal":0, "comm":0};
for(var i = 0; i < values.length; i++) {
result.sal += values[i].sal;
result.comm += values[i].comm;
}
return result;
}
测试:reduce2("mykey", myvalue2)
2. 全文检索
全文检索对每一个词建立一个索引,指明该词在文档中出现的次数和位置,当用户查询时,检索程序就根据事先建立的索引进行查找,并将查找的结果返回给用户。这个过程类似于通过字典中的检索字查文字的过程。
MongoDB 从2.4版本开始才支持全文检索,目前支持多种语言的全文索引。
1) 全文索引(Text Index)
首先,在对应的collection 上创建 Text Index;一个collection 上只能创建一个 Text Index;但是这个全文索引可以包含多个列值,例如:
db.stores.createIndex({"name":"text","description":"text"})
2) 使用 $text、$search 操作符,执行全文检索
注意:检索时不区分大小写
1. 查询包含coffee、java、shop 的文档
db.stores.find({$text:{$search:"coffee java shop"}})
2. 使用 explain() 方法查看执行计划
db.stores.find({$text:{$search:"coffee java shop"}}).explain("executionStats")
3. 查询包含"coffee shop"这个整体的文档(使用转义字符\")
db.stores.find({$text:{$search:"\"coffee shop\""}})
4. 查询包含java 或者shop, 但不包含 coffee.(通过"-"号来完成)
db.stores.find({$text:{$search:"-coffee java shop"}})
5. 使用 $meta 运算符(使用$meta 计算textScore),查询并排序
MongoDB 默认是不排序的,但是全文检索可以通过计算文本检索的相关性来进行排序的,相关性越高的分值就越高,我们就可以通过分值来进行排序。
db.stores.find(
{$text:{$search:"coffee java shop"}},
{score:{$meta:"textScore"}}
).sort({score:{$meta:"textScore"}})
其中:
{$meta:"textScore"}是固定写法,表示将计算出来的分值保存在score 的列上;
sort({score:{$meta:"textScore"}}) 表示根据什么列来进行排序。
6. 中文检索
db.stores.find({$text:{$search:"蛋糕"}})
3) 在聚合操作中使用全文检索
首先,创建全文索引:db.books.createIndex({"subject":"text"})
1. 查询 Java Hadoop PHP 图书的销售总量
db.books.aggregate([
{$match:{$text:{$search:"Java Hadoop PHP"}}},
{$group:{"_id":"$subject", "totals":{$sum:"$quantity"}}}
])
2. 查询 Java Hadoop PHP 图书的销售总量,并排序
db.books.aggregate([
{$match:{$text:{$search:"Java Hadoop PHP"}}},
{$group:{"_id":"$subject", "totals":{$sum:"$quantity"}}},
{$sort:{score:{$meta:"textScore"}}}
])
3. 查询 Java Hadoop PHP 图书的销售总量,并排序,并且销售总量大于85本
db.books.aggregate([
{$match:{$text:{$search:"Java Hadoop PHP"}}},
{$group:{"_id":"$subject", "totals":{$sum:"$quantity"}}},
{$sort:{score:{$meta:"textScore"}}},
{$match:{"totals":{$gt:85}}}
])
3. 地理空间查询
MongoDB 支持地理数据的查询,可以将地理信息的数据保存到 GeoJSON 的格式中,本章将讨论 MongoDB 的地理特性。
1) 什么是 GeoJSON 数据?
格式:<field>:{type:<GeoJSON type}, coordinates:<coordinates>}
其中:GeoJSON type 可以是以下的类型:
Point |
点(经度、维度) |
LineString |
线 |
Polygon |
多边形 |
MultiPoint |
多点 |
MultiLineString |
多线 |
MultiPolygon |
多面 |
GeometryCollection |
混合数据类型 |
2) 查询某个位置,5000米的地理数据信息
db.myaddress.find({"loc":{$near:{$geometry:{$type:"Point", "coordinates":[118.790611,32.047616]}, $maxDistance:5000}}})
如果 Error: unable to find index for $geoNear query
需要在地理的数据上建立一个索引,例如:
db.myaddress.ensureIndex({"loc":"2dsphere"}) #"2dsphere"表示"Point"类型的索引
参考文档:https://docs.mongodb.com/manual/geospatial-queries/
4. 数据建模
1) 数据建模概述
MongoDB 与关系型数据库的建模还是有许多不同,因为MongoDB 支持内嵌对象和数组类型。MongoDB 建模有两种方式,一种是内嵌(embedded), 另一种是连接(references)。
l 内嵌(embedded):嵌入文档通过把数据存储到一个独立文档结构中来获取数据之间的关系。
l 连接(references):类似关系型数据库中的外键,将多个数据文档通过连接进行关联。
2) 验证文档的有效性
MongoDB 在更新和插入操作期间可以验证文档。验证规则使用validator 选项在每个集合上指定,这个validator 选项用一个文档具体实现验证规则和表达式。
1. createCollection() 方法,创建一个文档,并指定验证规则
1) 创建文档并指定验证规则
db.createCollection("people", {validator:{$and:[
{"name":{$type:"string"}},
{"gender":{$in:["Female", "Male"]}},
{"phone":{$type:"string"}},
{"email":{$regex:/@126\.com$/}}
]}})
2) 插入数据
正确:db.people.insert({"_id":1, "name":"Tom", "phone":"86-1588796", "email":"tom@126.com", "gender":"Female"})
错误:db.people.insert({"_id":2, "name":"Mike", "phone":"86-1588796", "email":"tom@sina.com", "gender":"Male"})
日志:"errmsg" : "Document failed validation"
2. runCommand() 方法,给已存在的文档添加验证规则
1) 给emp 的集合添加验证规则
db.runCommand({
collMod:"emp",
validator:[{"ename":{$exists:true}}, {sal:{$exists:true}}]
})
2) 验证级别参数:validationLevel
1. strict: 应用验证规则到所有的插入和更新操作中,默认的验证级别。
插入数据:
db.testvali1.insert({"_id":1, "name":"Mike", "email":"mike@126.com", "deptno":10})
db.testvali1.insert({"_id":2, "name":"Mary", "deptno":10})
添加 strict 的验证规则:
db.runCommand({
collMod:"testvali1",
validator:{"email":{$exists:true}},
validationLevel:"strict"
})
一次插入多条数据(只会插入验证成功的数据):
db.testvali1.insert([
{"_id":3, "name":"Jerry", "email":"jerry@126.com", "deptno":10},
{"_id":4, "name":"Tom", "deptno":10}
])
2. moderate: 对于不满足验证要求的文档,将不验证文档有效性,不会执行对应操作。
插入数据:
db.testvali2.insert({"_id":1, "name":"Mike", "email":"mike@126.com", "deptno":10})
db.testvali2.insert({"_id":2, "name":"Mary", "deptno":10})
添加moderate的验证规则:
db.runCommand({
collMod:"testvali2",
validator:{"email":{$exists:true}},
validationLevel:"moderate"
})
执行更新操作
db.testvali2.update({"deptno":10}, {$set:{"email":"abc@sina.com"}})
3. 接受或拒绝无效文档
接受或拒绝参数 validationAction 参数,该参数有两个可选值:
warn: 验证失败时可以执行操作,但会记录错误日志
error: 验证失败时不会执行操作,该值应该是默认值?
db.createCollection("testvali3", {
validator:{$and:[
{"name":{$type:"string"}},
{"gender":{$in:["Female", "Male"]}}
]},
validationAction:"warn"
})
插入数据:
db.testvali3.insert({"name":"abeam", "gender":"Male"})
db.testvali3.insert({"name":"abc", "gender":"abc"})
3) 模型设计
1. 嵌入文档的一对一模型
它比引用文档更有优势,例如:
2. 嵌入文档的一对多关系模型
它比引用文档更有优势,例如:
3. 文档引用的一对多关系模型
相比内嵌文档模型,可减少数据冗余。它适合什么样的场景呢?如图:
4. 父引用和子引用的模型树结构
父引用就是将父ID保存在子节点中,而子引用则是将子ID以集合的方式保存在父节点中,如图:
5. 祖先数组的模型树结构
6. 具体化路径的模型树结构
7. 内嵌集的模型树结构