对MongoDB设计模式的理解和常用查询总结
2017-08-27 18:57
127 查看
一、MongoDB的特点
mongo的特点一图以蔽之:先说说关系型数据库:
这里的“关系”究竟指的是什么?我们不说大概念和纯理论,只说最实在的东西——关系型数据库中的“表”代表着一类“实体”,实体之间会存在一定关系,比如用户实体和订单实体,一个用户可以有多个订单,而一个订单只属于一个用户,这就是一对多的关系。比如商品和类别,一个商品可以归属多个类别,而一个类别下会有多个商品,这就是多对多的关系。
在关系型数据库中,通过在表中添加存储外键的列来表示这些关系。“主键”阿“外键”阿这些键,本质上就是对一个实体的引用,在一个实体中存储了另一个实体的引用,这样的话,实体之间的关系信息是不是也就有了呢?
这样在查询时,通过主键和外键的关联,就可查询实体与实体间的关系。在修改实体时,由于实体之间的关系仅用引用来表示,所以实体的信息仅存在一处,而引用是不会变的不用修改的,这样也只需修改一处的实体信息即可。
再说说mongo:
mongo的设计目标可不仅仅是简单的键值存储,它也可以做到表达现实世界中纷繁复杂的实体和实体之间的关系,所以它也必须能够处理一对多和多对多的关系,那它是如何做到的呢?
mongo中一个实体由一个文档(document)表示。文档的属性值可以是基本类型和引用,就和RDBMS的表的列的值一样,但与之不同的是,文档的属性值还可以是:
(1)另一个完整的文档而非引用
(2)数组,且数组的元素既可以是基本类型和引用,也可以是一个完整的文档
mongo可以利用文档中的数组,无比直观地表示一对多和多对多的关系。当然也可以像RDBMS那样,在一个文档中存储另一个文档的键来表示这种关系,只不过不能像RDBMS可以通过基于join的连接查询一次查出这种关系,mongo必须经过两次查询(第一次查出符合查询要求的实体,第二次查出与该实体有关系的其它实体)。
同时由于可以值可以是另一个完整的文档而非引用,使其可以设计反范式的数据模型。
二、mongoDB的设计模式
2.1、嵌入与引用
嵌入:包括文档的属性值是一个完整文档,也包括文档的数组属性的元素是一个完整的文档。引用:就是指文档的属性值是另一个文档id,或文档的数组实行的元素是另一个文档id。
嵌入是反范式的,而引用是符合范式的设计。
嵌入提供了一些查询的性能优势,而引用提供了更多的灵活性。
选择规则:当子文档从不会出现在父文档以外的环境中时使用嵌入方式,否则在单独的集合里存储子文档。
2.2、一对多
(1)像RDBMS那样,在一个文档中存储另一个文档的键来表示这种关系(2)利用文档的数组,数组的元素是另一个完整的文档
(3)利用文档的数组,数组的元素是另一个文档的id
2.3、多对多
(1)利用文档的数组,数组的元素是另一个文档的id2.4、树
树的本质是“级联的一对多关系”。要查询树的话,常规做法是递归地进行一对多的查询。树形数据结构并不是mongo所擅长的,但可以通过一种冗余数据的技巧——物化路径的方式,简化树的查询。遵循物化路径模式,每个树节点都包含一个path字段,这个字段存储了每个级联节点祖先的ID,根节点path为空。
//根节点 { _id: ObjectId("000"); path: null; } //一级节点 { _id: ObjectId("111"); path: 000; } //二级节点 { _id: ObjectId("222"); path: 000:111; } //...
这样在查询特定子树时,比如要查询上述一级节点下的子树,只需 find({path: /^000/}),要查询上述二级节点下的子树,只需 find({path: /^000:111/}),都只要查询一次即可。但如果要修改和删除树的某一节点,就需要修改该节点下所有节点的path,这就是反范式设计的代价。
三、mongoDB的查询
mongo有个特色,它标榜为文档数据库,我们只能将一个个文档存入其中。此外,mongo还使用文档来表示命令,比如查询条件命令、更新操作命令、参数设置命令都是一个个文档。3.1、简单查询
(1)范围查询符$lt、$gt、$lte、$gte、$ne,后接基本类型,且仅当它们含有相同类型的值时才进行比较,
$exsit,后接布尔类型
db.users.find({'birth_year': {'$gt': 1985, '$lte': 2015}}) db.users.find({'birth_year': {'$exist': true}})
(2)布尔运算符
$not,后接文档命令,
$nor、$and、$or,后接文档命令数组
db.users.find({'age': {'$not': {'$lte': 30}}}) db.users.find({ '$or': [ {'age': '30'}, {'level': 'VIP'} ] })
(3)点操作符
mongo中的点操作符非常强大,它不仅可以深入内嵌对象的内部进行查询,也可以像操作一个对象一样去操作对象数组。
{ _id: ObjectId("xxx"), address: { name: 'home', city: 'NanJing' } } //查询住址在南京的用户 db.users.find({'address.city': 'Nanjing'}); //------------------手动分割---------------------- { _id: ObjectId("xxx"), address: [ { name: 'home', city: 'Nanjing' }, { name: 'work', city: 'BeiJing' } } //查询所有住址中有在南京的用户 db.users.find({'address.city': 'Nanjing'}); //查询所有住址都不在南京的用户 db.users.find({'address.city': {$ne:'Nanjing'}}); //查询第一个住址在南京的用户 db.users.find({'address.0.city': 'Nanjing'}); //查询家庭住址在南京的用户 db.users.find({ 'address': { '$eleMatch':{ name: 'home', city: 'Nanjing' } } });
(4)数组操作符
mongo可以利用数组表示实体之间的一对多和多对多的关系,所以对数组操作的支持程度,影响着mongo查询基于数组的实体关系的便利度。上面的点操作符在很大程度上给予了数组操作方便,除此之外还有一些:
$eleMatch,如果提供的所有的查询条件在相同的子文档中,则匹配,
$size,如果子文档数组大小与提供的数值相同,则匹配,
$in,如果任意参数在引用集合里,则匹配,
$all,如果所有参数在引用集合里,则匹配,
$nin,如果没有参数在引用集合里,则匹配,
以上三个操作符都必须后接数组,但应用的属性既可以只拥有一个值,也可以拥有多个值(数组)
唯一比较遗憾的就是在mongo中无法直接查询数组的大小,只能使用$size操作符给定一个值去判断数组的大小等不等于该值。我也实在不明白mongo为何不提供这个功能。
(5)JavaScript查询运算符
以上都是mongo提供给你的查询操作,你还可以自己编写js脚本代码来定制你自己的查询操作。
db.getCollection('emrRecord').find({ '$where': "function() { return this.医院编码 != '1002'}" }) db.getCollection('emrRecord').find({ '$where': "this.医院编码 != '1002'" })
就如同js中的eval()函数一样强大,但要注意js注入攻击的可能性和性能损失问题。
(6)定位选择返回文档中的一部分
db.getCollection('emrRecord').find({}) .skip(100), .limit(200), .sort({住院号:1, 病历名称:1})
虽然skip和limit可以实现分页查询,但是当skip的值非常大的时候,mongo执行的效率是非常低的。因为它会非常蠢的去依次扫描跳过skip参数指定数量的文档。另外,调用skip、limit和sort的顺序并不重要。
(7)选择返回文档的子集
以上的所有查询都相当于在 select *,能不能 select name, age,只选择想要的属性呢?当然可以:
//返回的文档只包含name属性 db.users.find({}, {'name': 1}); //返回的文档不包含name属性 db.users.find({}, {'name': 0}); //返回的文档的address数组只包含前三条 db.users.find({}, {'address': {'$slice': 3}}); //返回的文档的address数组只包含前最后一条 db.users.find({}, {'address': {'$slice': -1}}); //返回的文档的address数组只包含第三到第五条 db.users.find({}, {'address': {'$slice': [2,5]}});
3.2、聚合查询
聚合框架常用操作符:db.collection.aggregate([ {$project: ...}, {$match: ...}, //上节中所有的查询条件命令文档都可以作为$match的值 {$unwind: ...}, //将文档中通过数组表示的一对多关系,转换成形似关系型数据库中的表现形式 {$group: ...}, {$skip: ...}, {$limit: ...}, {$sort: ...}, {$out: ...}, //将管道的输出结果保存到指定集合中。若集合不存在则会创建一个,若存在则会完全取代现有集合 ])
(1) $project
具有文档重塑功能:
//返回的文档只有areaCode字段 db.getCollection('patient').aggregate([ { $project: {areaCode: 1} } ]) //将areaCode重命名为地区编码 db.getCollection('patient').aggregate([ { $project: {地区编码: '$areaCode'} } ]) //将first_name和last_name字段的组合重塑为一个全新的文档 db.getCollection('patient').aggregate([ { $project: {name: {first: '$first_name', last: '$last_name'}} } ])
字符串重塑:
db.getCollection('patient').aggregate([ { $project: {name: {$concat: ['$first_name', ' ', '$last_name']}, //字符串拼接 firstInitial: {$substr: ['$first_name', 0, 1]}, //获取子串 usernameUpperCase: {$toUpper: '$username'}} //大小写转换 } ])
日期重塑:
db.getCollection('patient').aggregate([ { $project: { 一年中的某一天: {$dayOfyear: '$出院时间'}, 一月中的某一天: {$dayOfMonth: '$出院时间'}, 一周中的某一天: {$dayOfWeek: '$出院时间'}, 日期的年份: {$year: '$出院时间'}, 日期的月份: {$month: '$出院时间'}, 一年中的某一周: {$week: '$出院时间'}, 日期中的小时: {$hour: '$出院时间'}, 日期中的分钟: {$minute: '$出院时间'}, 日期中的秒: {$second: '$出院时间'}, 日期中的毫秒: {$millisecond: '$出院时间'}} } ])
集合重塑:
db.getCollection('basUser').aggregate([ { $project: {并集: {$setUnion: ['$病区列表', '$科室列表']}, 交集: {$setIntersection: ['$病区列表', '$科室列表']}, 差集: {$setDifference: ['$病区列表', '$科室列表']}, 是否为子集: {$setIsSubset: ['$病区列表', '$科室列表']}, 是否完全相同: {$setEquals: ['$病区列表', '$科室列表']}} } ])
(2) $group
$group中必须有_id字段以定义按什么字段来分组。当为分组定义_id字段时,可以使用一个或者多个存在的字段:
//根据一个字段进行分组 db.getCollection('patient').aggregate([ { $group: { _id: '$科室ID', count: {$sum: 1} } } ]) //根据多个字段进行分组 db.getCollection('patient').aggregate([ { $group: { _id: {科室ID: '$科室ID', 病区ID: '$病区ID'}, count: {$sum: 1} } } ])
在 $group内使用的函数:
db.getCollection('patient').aggregate([ { $group: { _id: '$科室ID', 组内某字段的和: {$sum: '$age'}, 组内某字段的平均值: {$avg: '$age'}, 组内某字段的最大值: {$max: '$age'}, 组内某字段的最小值: {$min: '$age'}, 组内某字段的第一个值: {$first: '$name'}, //当有前缀$sort才有意义 组内某字段的最后一个值: {$last: '$name'}, //当有前缀$sort才有意义 组内某字段的非重复值组成的数组: {$addToSet: '$科室名称'}, 组内某字段的所有值组成的数组: {$push: '$科室名称'} } } ])
参考书籍
《MongoDB 实战》 —— Kyle Banker等相关文章推荐
- Css常用效果总结 — 快速查询笔记
- MySQL开发中常用的查询语句总结
- thinkPHP数据查询常用方法总结【select,find,getField,query】
- SQL查询语句常用总结
- hbase常用查询总结
- oracle中常用查询总结
- MySQL开发中常用的查询语句总结
- [转载]MySQL开发中常用的查询语句总结
- 关于mysql 简单的查询语句 以及常用函数的 总结
- Oracle一些常用的查询命令总结(持续更新)
- SQL Server数据库查询优化的常用方法总结
- 关于mysql 简单的查询语句 以及常用函数的 总结
- Gentle.Net学习笔记二:常用查询数据的方法总结
- oracle 常用查询总结
- 关于mysql 简单的查询语句 以及常用函数的 总结
- 关于C++ 从命令行输入数据 常用输方法cin的一些理解总结
- 关于mysql 简单的查询语句 以及常用函数的 总结
- 对常用的软件开发模型的总结与个人理解_Phoenix-晶
- SQL SERVER 中常用函数的总结,方便开发的时候查询
- postgreSql 常用查询总结