1、BSON
BSON是一种类json的一种二进制形式的存储格式,简称Binary JSON,它和JSON一样,支持内嵌的文档对象和数组对象,但是BSON有JSON没有的一些数据类型,如Date和BinData类型。
BSON可以做为网络数据交换的一种存储形式,这个有点类似于Google的Protocol Buffer,但是BSON是一种schema-less的存储形式,它的优点是灵活性高,但它的缺点是空间利用率不是很理想。
BSON有三个特点:轻量性、可遍历性、高效性。
MongoDB使用了BSON这种结构来存储数据和网络数据交换。把这种格式转化成一文档这个概念。因为BSON是schema-free的,所以在MongoDB中所对应的文档也有这个特征,这里的一个Document也可以理解成关系数据库中的一条记录(Record),只是这里的Document的变化更丰富一些,如Document可以嵌套。
MongoDB以BSON做为其存储结构的一种重要原因是其可遍历性。
一个Document的BSON表示:
{
title:"MongoDB",
last_editor:"192.168.1.122",
last_modified:new Data("27/06/2011"),
body:"MongoDB introduction",
categories:["Database","NoSQL","BSON"],
revieved:false
}
一个嵌套的例子:
{
name:"lemo",
age:"12",
address:{
city:"suzhou",
country:"china",
code:215000
}
scores:[
{"name":"english","grade:3.0},
{"name":"chinese","grade:2.0}
]
}
下面我们进入正题,介绍一下BSon是怎么把一个个MongoDB的文档转换成二进制形式进行存储的,在此之前读者需要从BSon官网上大致了解一下其解释的规则,链接会在下面的参考资料中给出。
至于介绍的形式,当然是给出例子,为大家讲解每个转换的步骤。
在介绍转换步骤之前,读者需要了解BSon中有四种基本类型:byte,int32,int64,double
接下来进入正题,看看运用BSon是怎么把该数据转换成二进制形式在非关系数据库中存储的。数据被作为一个Document跟据BSon的规则(具体规则这里不给出,请查看BSon官网的规则说明)需要进一步进行分解为三项int32,e_list,
"\x00"。
先来介绍一下int32。int32需要占用四个字节,但是这个有两点需要特别注意。第一点就是,Document解释成的int32是用来计算该Document的长度的,但这个长度包含本身自己int32四个字节的长度。以本例来算,总共的Document需要102个字节,这102个字节包括本身int32所占的四个字节和后面所解释出的所有的所需的字节长度,当然102个字节在一开始是没法算出来的,只有将该Document都解释完才会算出这个数据。第二点要说明的是,存储长度本身占有四个字节,而这四个字节需要高位存储。至于什么是高位存储,这里只给一个简单的解释,就是四个字节中的最低位字节对应到存储器的最高位,最高为的字节对应到存储器的最低位。
接下来说一下"\x00"。这个对应过来的十六进制相当于是一个结束符,有点类似于程序设计语言中字符串类型最后的"\0"。表示该部分解释的结束。
下面介绍关键的部分e_list。e_list也只是BSon规则中的一个中间形式,即e_list需要进行进一步的解释。e_list的进一步解释成两部分。第一部分是element,第二部分是e_list或是""。先说一下第二部分,在此时第一步解释出的e_list需要将第二部分解释成e_list。为什么呢,因为例子中需要存储的并不只有"Name"一项,在其后面还有"IsGreat"和"Feilds"需要解释,换句话说e_list这一步需要进行递归,继续循环解释。那么什么是后e_list解释成""呢?当解释到最后一项"Fields"时,后面没有需要存储的数据项时,需要将其第二部分解释成"",这还需要注意一点的是""并不占用存储空间,也就是说它是没有长度的,可以理解为""在BSon的解释最后的形式中并不存在。
其实,到这里,对于"Name"键值这部分的存储已经介绍完毕,但是对于整个例子的存储还远没有结束。
读者应该还记得前面提到的e_list,它是递归循环的。第二个e_list同样的被解释成两部分,第一部分是element,第二部分是e_list。第一部分的element跟据数据类型是布尔型的"true"对应到BSon的规则解释成为"\x08",e_name,"\x01"。和之前介绍的这部分的类似"\x08"代表了这种对应关系的类型,而最后的"\x01"则代表为true(如果是false的布尔类型这部分对应的是"\x00")。e_name同样的被唯一解释,这里和"Name"部分一样,不再多说,请读者自己思考。第二部分是e_list同样的被解释成为两部分element和""(因为后面已经没有数据项了)。
对于element,由于对应的数据类型是"Array",将其解释成"\x04",e_name和Document。"\x04"和e_name的分析也请读者按照之前的讲述自己进行分析。对于后面出现的Document,完全是类似于例子最开始部分的那个Document进行解释,读者可以自行分析。但是这里出现了一个问题,因为数组中的数据项并没有相应的键值与其对应。其实不然,BSon是将数组元素的下标(从0开始)作为其键值的。这部分就转换成一个嵌套的Document。
最后这里给出本例子的解释过程和最后解释的二进制形式如下图所示,希望读者可以自己分析完后对照一下。
参考资料:
http://www.cnblogs.com/zeliliu/archive/2012/10/01/2708330.html
2、Aggregation Framework
Aggregation 提供的功能map-reduce也能做(诸如统计平均值,求和等)。官方那个大胖子说这东西比map-reduce简单, map-reduce 我没用过, 不过从使用Aggregation的情况来看, 进行统计等操作还是蛮方便的。
总体而言,Aggregation就是类似 Unix-like中的 管道 的概念,可以将很多数据流串起来,不同的数据处理阶段可以再上一个阶段的基础上再次加工。
比较常用的有:
•$project - 可以重构数据
•$match - 可以实现类似query的功能
•$limit - 限制返回个数,你懂的
•$skip - 同上
•$unwind - 可以将一个包含数组的文档切分成多个, 比如你的文档有 中有个数组字段 A, A中有10个元素, 那么经过 $unwind处理后会产生10个文档,这些文档只有 字段 A不同
•$group - 统计操作, 还提供了一系列子命令
–$avg, $sum …
•$sort - 排序
举个例子:
Document结构:
{
"_id" : ObjectId("509944545"),
"province" : "海南",
"age" : 21,
"subjects" : [
{
"name":"语文",
"score" : 53
},
{
"name":"数学",
"score" : 27
},
{
"name":"英语",
"score" : 35
}
],
"name" : "刘雨"
}
接下来要实现两个功能:
统计上海学生平均年龄
统计每个省各科平均成绩
统计上海学生平均年龄
从这个需求来讲,要实现功能要有几个步骤: 1. 找出上海的学生. 2. 统计平均年龄 (当然也可以先算出所有省份的平均值再找出上海的)。如此思路也就清晰了
首先上 $match, 取出上海学生:
{$match:{'province':'上海'}}
接下来 用 $group 统计平均年龄:
{$group:{_id:’$province’,$avg:’$age’}}
$avg 是 $group的子命令,用于求平均值,类似的还有 $sum, $max ....
统计每个省各科平均成绩
首先更具数据库文档结构,subjects是数组形式,需要先‘劈’开,然后再进行统计
主要处理步骤如下:
- 先用$unwind 拆数组 2. 按照 province, subject 分组并求各科目平均分
$unwind 拆数组
{$unwind:’$subjects’}
按照 province, subject 分组,并求平均分?
{$group:{
_id:{
subjname:”$subjects.name”,
province:’$province’
},
AvgScore:{$avg:”$subjects.score”}
}
支线任务: 将同一省份的科目成绩统计到一起
要做的有一件事,在前面的统计结果的基础上,先用 $project 将平均分和成绩揉到一起,即形如下面的样子:
{$project:{ province:"$_id.province", subjinfo:{"subjname":"$_id.subjname", "avgscore":"$AvgScore"} }
使用 group 再次分组:
{$group:{_id:"$province", avginfo:{$push:"$subjinfo"}}}
至此,功能也就完成了。
参考文献:
http://my.oschina.net/GivingOnenessDestiny/blog/88006
官方文档:
http://docs.mongodb.org/manual/applications/aggregation/
3、ObjectId
在mongodb集合中的每个document中都必须有一个"_id"建,这个键的值可以是任何类型的,在默认的情况下是个Objectid对象。
当我们让一个collection中插入一条不带_id的记录,系统会自动地生成一个_id的key。
> db.t_test.insert({"name":"cyz"})
> db.t_test.findOne({"name":"cyz"}){ "_id" : ObjectId("4df2dcec2cdcd20936a8b817"), "name" : "cyz" }
把 ObjectId("4df2dcec2cdcd20936a8b817")这串值拿出来并对照官网的解析来深入分析。
实际上它是由ObjectId(string)所创建的一组十六进制的字 符,每个字节两位的十六进制数字,总共使用了12字节的存储空间。
下面是官网给出的解释:
TimeStamp
前 4位是一个unix的时间戳,是一个int类别,我们将上面的例子中的objectid的前4位进行提取“4df2dcec”,然后再将他们安装十六进制 专为十进制:“1307761900”。
可以尝试转换格式来显示:
$ date -d '1970-01-01 UTC 1307761900 sec' -u2011年 06月 11日 星期六 03:11:40 UTC
**Machine **
接下来的三个字节,就是 2cdcd2 ,这三个字节是所在主机的唯一标识符,一般是机器主机名的散列值,这样就确保了不同主机生成不同的机器hash值,确保在分布式中不造成冲突,这也就是在同一台机器生成的objectid中间的字符串都是一模一样的原因。
pid
上面的Machine是为了确保在不同机器产生的objectid不冲突,而pid就是为了在同一台机器不同的mongodb进程产生了objectid不冲突,接下来的0936两位就是产生objectid的进程标识符。
increment
前面的九个字节是保证了一秒内不同机器不同进程生成objectid不冲突,这后面的三个字节a8b817,是一个自动增加的计数器,用来确保在同一秒内产生的objectid也不会发现冲突,允许256的3次方等于16777216条记录的唯一性。
mongodb产生objectid还有一个更大的优势,就是mongodb可以通过自身的服务来产生objectid,也可以通过客户端的驱动程序来产生,如果你仔细看文档你会感叹,mongodb的设计无处不在的使用空间换时间的思想,尽管objectid是轻量级,但服务端产生也必须开销时间,所以能从服务器转移到客户端驱动程序完成的就尽量的转移,必须将事务扔给客户端来完成,减低服务端的开销,另还有一点原因就是扩展应用层比扩展数据库层要方便得多。
参考文献:
http://helloyesyes.iteye.com/blog/1089927
http://docs.mongodb.org/manual/reference/object-id/
4、aggregation framework实验部分
下面写了一段python程序生成随机数据到mongodb数据库中,由于中文会报错,所以全部使用了拼音。
#!/usr/bin/env python
# coding=utf-8
from pymongo import MongoClient
from random import randint
name1 = ["yang ", "li ", "zhou "]
name2 = [
"chao",
"hao",
"gao",
"qi gao",
"hao hao",
"gao gao",
"chao hao",
"ji gao",
"ji hao",
"li gao",
"li hao",
]
provinces = [
"guang dong",
"guang xi",
"shan dong",
"shan xi",
"he nan"
]
client = MongoClient('localhost', 27017)
db = client.student
sm = db.smessage
sm.remove()
for i in range(1, 100):
name = name1[randint(0, 2)] + name2[randint(0, 10)]
province = provinces[randint(0, 4)]
new_student = {
"name": name,
"age": randint(1, 30),
"province": province,
"subject": [
{"name": "chinese", "score": randint(0, 100)},
{"name": "math", "score": randint(0, 100)},
{"name": "english", "score": randint(0, 100)},
{"name": "chemic", "score": randint(0, 100)},
]}
print new_student
sm.insert_one(new_student)
print sm.count()
好了,现在数据库里面有100条学生数据了。
现在我要得到广东学生的平均年龄,在mongo控制台输入:
db.smessage.aggregate(
{$match: {province: "guang dong"}},
{$group: { _id: "$province", age:{$avg:"$age"}}}
)
{ "_id" : "guang dong", "age" : 16.05263157894737 }
如果想到得到所有省份的平均年龄,那就更加简单了:
db.smessage.aggregate(
{$match: {province: "guang dong"}}
)
{ "_id" : "guang xi", "age" : 15.19047619047619 }
{ "_id" : "guang dong", "age" : 16.05263157894737 }
{ "_id" : "shan dong", "age" : 17.44 }
{ "_id" : "he nan", "age" : 20 }
{ "_id" : "shan xi", "age" : 16.41176470588235 }
如果想得到广东省所有科目的平均成绩:
db.smessage.aggregate(
{$match: {province: "guang dong"}},
{$unwind: "$subject"},
{$group: { _id: {province:"$province",sujname:"$subject.name"}, per:{$avg:"$subject.score"}}}
)
{ "_id" : { "province" : "guang dong", "sujname" : "chemic" }, "per" : 45.89473684210526 }
{ "_id" : { "province" : "guang dong", "sujname" : "english" }, "per" : 40.68421052631579 }
{ "_id" : { "province" : "guang dong", "sujname" : "math" }, "per" : 67.47368421052632 }
{ "_id" : { "province" : "guang dong", "sujname" : "chinese" }, "per" : 40.05263157894737 }
排上序:
db.smessage.aggregate(
{$match: {province: "guang dong"}},
{$unwind: "$subject"},
{$group: { _id: {province:"$province",sujname:"$subject.name"}, per:{$avg:"$subject.score"}}},
{$sort:{per:1}}
)