【文档型数据库】
MongoDB里的每条记录都是一个文档,其是一个由键值对组成的数据结构,MongoDB文档类似JSON对象,字段的值也可以包含其他文档、数组和文档数组
{
name: "sue",
age: 26,
status: "A",
groups: ["news","sports"]
}
使用文档的优势有:
- 文档(对象)符合多数编程语言的原生数据类型
- 内嵌的文档和数组可以减少昂贵join的需要
- 动态模式支持文档结构的多样式变化
【关键特性】
1. 高性能
MongoDB提供高效地数据持久化,尤其是:
- 支持内嵌的数据模式以减小数据库的系统IO活动
- 支持索引以加快查询,且可对内嵌的文档和数据创建索引
2. 丰富的查询语言
MongoDB支持丰富的查询语言,除了读写操作(CRUD),还支持:
- 数据聚合
- 文本搜索
- 地理位置查询
3. 高可用
MongoDB的复制能力,称为复本集,提供:
- 自动故障切换
- 数据冗余
一套复本集是一组MongoDB实例,它们存储相同的数据集合,通过冗余来提升数据可用性
4. 水平扩展
MongoDB的核心功能之一,就是其支持水平扩展性:
- 数据分片把数据分布在整个集群中
- MongoDB 3.4支持在分片键的基础上创建数据区,在一个数据平衡的集群中,MongoDB只会把读写请求发送到数据区里的分片上
5. 支持多种存储引擎
MongoDB支持多种存储引擎,例如:
- WiredTiger
- MMAPv1
另外,MongoDB提供插件式的存储引擎API,以支持为MongoDB开发的第三方存储引擎
== 数据库和集合 ==
MongoDB以BSON文档形式存储数据记录在集合中,集合存储在数据库中
【数据库】
MongoDB中,数据库用于保存集合,集合用于保存文档
在mongo shell里,要选择使用的数据库,可发出 use 语句,如下:
> use myDB
如果某个数据库不存在,当第一次存储数据到该库时,MongoDB会自动创建,因为,可以切换到一个尚不存在的数据库,执行以下操作:
> use myNewDB
> db.myNewCollection1.insert( { x: 1 } )
insert()操作即会创建myNewDB库,也会创建myNewCollection1集合,如果它们尚不存在的话
【集合】
MongoDB把文档存储在集合里,集合类似于关系型数据库里的表
如果集合不存在,MongoDB会在第一次对其存储数据时自动创建该集合
> db.myNewCollection2.insert( { x: 1 } )
> db.myNewCollection3.createIndex( { y: 1 } )
insert()和createIndex()操作会创建各自的集合,如果它们尚不存在的话
MongoDB也提供 db.createCollection() 方法来手动创建一个集合,并可带有多个选项,例如集合最大大小,或文档检验规则,如果不需要设置这些选项,可不必手动创建集合,因为在第一次对其存储数据时,MongoDB会自动创建该集合
默认地,MongoDB的集合不须要它的文档都有相同的固定模式,即在同一个集合里的文档不需要有相同的字段和数据类型,不过从MongoDB 3.2开始,可以在update和insert操作时为集合指定文档检验规则
要改变集合中文档的结构,如增加一个新字段、移除已有字段,或将某字段的值改成新的类型,直接以新结构更新文档即可
== 视图 ==
从MongoDB 3.4开始,MongoDB支持通过已经存在的集合创建只读视图,且也可以通过视图再创建出新视图
【创建视图】
创建或定义一个视图的mongo shell命令如下:
> db.createView(, , , )
【行为】
视图呈现以下行为:
> 只读
视图是只读的,对视图的写操作会报错
> 索引
视图使用底层集合的索引,包含查询和排序操作
> 排序
不能对视图指定 $natural 排序方式,如下例操作是错误的:
db.view.find().sort({$natural: 1})
> 映射的限制
视图上的find()操作不支持的映射操作有 $、$elemMatch、$slice和$meta
> 不能改变的名字
视图无法重命名,只能删了重建
> 视图创建
在读操作期间按需要计算视图,作为基础集合管道的一部分,MongoDB在视图上执行读操作,因此,视图不支持如下操作:
- db.collection.mapReduce()
- $text操作,因为聚合中的$text操作仅适用于第一阶段
- geoNear命令和$geoNear管道阶段
如果被用于创建视图的聚合管道屏蔽(抑制)了_id字段,那么视图中的文档也不会有_id字段
> 分片视图
如果视图的底层集合是分片的,那么视图也被认为是分片的,因此,不能在$lookup和$graphLookup操作的from字段上指定一个分片的视图
> 视图和集合
- 在创建视图时可以指定一个默认的排序规则(collation),如果未指定该规则,视图默认的排序规则是二进制对比排序,而且,视图不会继承集合的默认排序规则
- 视图上的字符串对比使用视图默认的排序规则,一个企图改变或覆盖视图默认排序规则的操作会以报错失败
- 如果从一个视图创建另一个视图,不能在新视图上指定不同于源视图的排序规则
- 如果在多个视图上执行一个聚合操作,例如$lookup或$graphLookup,所有视图必须有相同的排序规则
> 公开的视图定义
列出集合的操作,如db.getCollectionInfos()和db.getCollectionNames(),在输出中都会包含视图
视图定义是公开的,视图上的db.getCollectionInfos()和explain操作会包含视图定义中的管道,因此,要避免在视图定义中直接涉及到敏感的字段和数据
【删除视图】
要删除视图,对视图使用db.collection.drop()方法即可
== 覆盖集合 ==
【概述】
覆盖(Capped)集合是一个大小固定的集合,其支持高吞吐量的insert操作,和基于写入顺序的读取操作,覆盖集合的工作方式类似于环形缓存:一旦集合填满分配的空间,会用新文档覆盖集合中最旧的文档
【行为】
> 写入顺序
覆盖集合严格保持数据写入顺序,作为返回结果,查询不需要索引即可按写入顺序返回结果集,由于没有索引的开销,覆盖集合能支持更高的写入吞吐量
> 自动移除最旧文档
为了能存放新文档,覆盖集合自动移除集合里最旧的文档,不需要执行明确的移除操作,例如,在复制集中用于存储操作日志的oplog.rs集合就是覆盖集合
以下场景可以考虑使用覆盖索引:
- 存储由系统生成的日志信息,没有索引开销的覆盖集合的文档写入速度,接近于将日志信息直接写入文件系统,而且,内置的先进先出属性保持着事件发生的顺序
- 通过覆盖集合缓冲少量数据,因为缓冲的读比写重,要么确保该集合总是驻留在内存中,要么接受些许索引带来的写入负载
> _id索引
覆盖集合有一个_id字段,并且该字段上默认就有一个索引
【限制和建议】
> 更新
如果要更新覆盖集合里的文档,为这些更新操作创建一个索引可以避免全集合扫描
> 文档大小
如果一个更新或替换操作改变了文档的大小,该操作会失败
> 文件删除
不能从覆盖集合里删除文档,要移除索引里的所有文档,使用drop()方法删除该覆盖集合再重建
> 分片
不能对覆盖集合进行分片
> 查询性能
使用自然顺序来获得集合中最新写入的文档是非常高效地,这类似于tail一个日志文件
> 聚合输出
聚合管道操作符$out不能输出结果集到一个覆盖集合
【使用】
> 创建一个覆盖集合
要创建覆盖集合必须明确使用 db.createCollection() 方法,当创建时必须指定集合的最大字节大小,MongoDB会预先分配空间,该大小包含少量的内部开销
db.createCollection( "log", { capped: true, size: 100000 } )
如果size字段小于或等于4096,则集合大小上限为4096字节,否则,MongoDB将增大给予的大小到256的整数倍
另外,还可以用max字段指定集合可容纳的最大文档数
db.createCollection( "log", { capped: true, size: 5242880, max: 5000 } )
无论是否指定max参数,size参数都是必须的,如果集合在达到最大文档数之前达到了最大大小限制,MongoDB仍然会移除最旧的文档
> 查询一个覆盖集合
如果在覆盖集合上执行无排序的find()命令,MongoDB保证结果集的顺序同数据写入顺序一致,若要按反序返回文档,要配合使用参数$natural为-1的sort()方法
db.cappedCollection.find().sort( { $natural: -1 } )
> 检查是否为覆盖集合
使用isCapped()方法以确定集合是否为覆盖集合
db.collection.isCapped()
> 将普通集合转换成覆盖集合
使用convertToCapped命令,并指定集合最大字节大小
db.runCommand({"convertToCapped": "mycoll", size: 100000});
也可同时使用max参数指定集合最大文档数
警告:该命令会发起一个全局写锁,堵塞其他所有操作,直到其完成
> 跟踪游标
可以在覆盖索引上使用跟踪游标,类似Unix的 tail -f 命令,可通过跟踪游标持续获得最新写入覆盖集合的文档
== 文档 ==
MongoDB以BSON文档格式存储数据记录,BSON是一种JSON文档的二进制表述格式,但它能比JSON包含更多的数据类型
【文档结构】
MongoDB文档由键值对组成,如下:
{
field1: value1,
field2: value2,
...
fieldN: valueN
}
字段值可以是任何的BSON数据类型,包括其他文档、数组和文档数组,如下:
var mydoc = {
_id: ObjectId("5099803df3f4948bd2f98391"),
name: { first: "Alan", last: "Turing" },
birth: new Date('Jun 23, 1912'),
death: new Date('Jun 07, 1954'),
contribs: [ "Turing machine", "Turing test", "Turingery" ],
views : NumberLong(1250000)
}
其中:
- _id是一个ObjeckId
- name是一个包含first和last字段的内嵌文档
- birth和death是日期型数据
- contribs是一个字符串数组
- views是一个大数值(NumberLong)类型
> 字段名
字段名为字符串
文档有以下字段名约束:
- 字段名_id是保留字,作为主键,其值必须唯一且不会变更,可以是除数组以外的任意类型
- 字段名不能以美元符($)开头,不能包含点号(.),不能包含null字符
BSON文档可有多个同名字段,一些由MongoDB进程创建的内部文档就会有重复字段,但绝不会在已存在的用户文档上添加重复字段
> 字段值限制
对于有索引的集合,索引字段的值有最大索引键长度限制
【点标号】
MongoDB使用点标号存取数组元素和内嵌(子)文档的字段
> 数组
通过以0为基数的索引位置选择或存取数组的元素,用点号(.)连接数组名和0基数索引位,并用双引号包围,如"."
例如,给出以下文档里的字段:
{
...
contribs: [ "Turing machine", "Turing test", "Turingery" ],
...
}
要选中contribs数组中的第三个元素,使用"contribs.2"
> 内嵌(子)文档
要选择或存取子文档的字段,用点标号连接子文档名和字段名,并且双引号包围,如"."
例如,给出以下文档中的字段:
{
...
name: { first: "Alan", last: "Turing" },
contact: { phone: { type: "cell", number: "111-222-3333" } },
...
}
- 要选中name字段中的last字段,使用"name.last"
- 要选中contact字段中的phone子文档里的number字段,使用"contact.phone.number"
【文档限制】
> 文档大小限制
BSON文档的最大大小为16MB
最大文档大小有助确保单个文档在处理时不会占用过多的RAM,在传输时不会使用过多的带宽,要存储大于16MB的文档,可使用MongoDB提供的GridFS
> 文档字段顺序
MongoDB保持文档在写入时的字段顺序,除了以下二种情况:
- _id字段总是作为文档的第一个字段
- 更新操作,包括字段改名,可能会打乱文档中的字段顺序
从MongoDB 2.6开始,MongoDB会尽量保持文档里字段的顺序
> _id字段
在MongoDB中,每个存储在集合里的文档都须要一个唯一的_id字段,该字段在集合中担任主键,如果一个文档写入时省略了_id字段,MongoDB会自动生成一个ObjectId填充到_id字段,这也适用于带有upset:true参数的update操作
_id字段有以下行为和限制:
- MongoDB默认在创建集合时就会在_id字段上创建一个唯一索引
- _id字段总是文档中的第一个字段,返回文档时默认会包含_id字段,同样也是第一个字段
- _id字段可以是除了数组以外的任何BSON数据格式
注意:为确保复制功能,不要在_id字段存储BSON的正则表达式
以下是_id字段的通常赋值方式:
- ObjectId
- 原生的唯一标示(如身份证号),这可以节省一个字段,并免去额外索引
- 代码生成的自增长数值
- 代码生成的UUID,更高效的方式是将UUID存储成BSON的BinData类型
多数MongoDB客户端(驱动)会自动包含_id字段,并在把写入发送到MongoDB之前为其生成ObjectId值,即使客户端发送的文档没有_id字段,MongoDB进程也会增加该字段,并为其生成ObjectId值
【文档结构的其他用途】
除了定义数据记录,MongoDB始终在使用文档结构,包括且不限于:
> 查询过滤
指定查询过滤条件,以确定哪些记录被选取,用于读取、更新和删除操作
使用:表达式指定等式条件和查询操作符条件
{
: ,
: { : },
...
}
> 指定更新
在执行db.collection.update()操作时,使用更新操作符将指定字段的数据修改成更新规范式文档
{
: { : , ... },
: { : , ... },
...
}
> 创建索引
索引规范式文档定义了创建索引的字段和索引类型:
{ : , : , ... }
== BSON类型 ==
在MongoDB中,BSON是一种用于存储文档和远程处理调用的二进制序列格式
BSON支持以下数据类型作为文档中的值,每种数据类型有一个相应的数值和字段串别名,可使用$type操作符通过BSON类型查询文档
Type Number Alias
Double 1 double
String 2 string
Object 3 object
Array 4 array
Binary data 5 binData
ObjectId 7 objectId
Boolean 8 bool
Date 9 date
Null 10 null
Regular Expression 11 regex
JavaScript 13 javascript
JavaScript(with scope) 15 javascriptWithScope
32-bit integer 16 int
Timestamp 17 timestamp
64-bit integer 18 long
Decimal128 19 decimal
Min key -1 minKey
Max key 127 maxKey
【ObjectId】
ObjectId唯一有序,且生成快速,由12个字节组成:
- 4字节,该ObjectId生成的unix时间戳
- 3字节,机器标识码
- 2字节,MongoDB进程号(PID)
- 3字节,计数器,起始于随机值
在MongoDB里,每个存储在集合里的文档都必须有一个担当主键的唯一_id字段,如果一个写入的文档省略了_id字段,MongoDB驱动会自动添加该字段,并为其生成一个ObjectId值,对于使用了 upsert:true 选项的更新操作写入的文档也是如此
MongoDB客户端为_id字段生成唯一ObjectId有以下二个额外的好处:
- 可以使用ObjectId.getTimestamp()方法,通过ObjectId得到文档的创建时间
- 在存储ObjectId值的_id字段上排序,几乎等同于按数据写入时间排序
在同一秒内,ObjectId值的次序与它们的生成时间并不是严格对应的,在多核或多线程操作系统上,同一秒内生成的ObjectId值不能严格表明数据写入的顺序,客户端间的时钟偏差也会导致不严格的数据顺序,更何况ObjectId值是由客户端驱动生成的
【字符串(String)】
BSON字符串(string)的编码为UTF-8,通常情况,每个编程语言的驱动在序列化和反序列化BSON时会把该语言的字符串格式转换成UTF-8,这使得BSON字符串可以轻松的存储尽可能多的国际字符,另外,MongoDB的$regex查询的正则字符串也支持UTF-8
【时间戳(timestamp)】
BSON有一个特殊的时间戳类型为MongoDB内部使用,与常规的Date类型没有关系,BSON时间戳值是64位的
- 前32位是time_t值,Unix时间戳
- 后32位是一个递增的序号,在一秒内的操作数
在一个MongoDB实例中,BSON时间戳值总量唯一的
在复制集里,oplog有一个ts字段,该字段的值就是BSON时间戳,表示操作的执行时间
如果写入的文档在顶层字段里有一个空的BSON时间戳,MongoDB实例将用当前时间戳替换之,例如,创建一个有时间戳值的文档:
> var a = new Timestamp();
> db.test.insertOne( { ts: a } );
然后执行 db.test.find() 操作将返回一个类似如下的文档:
{ "_id" : ObjectId("542c2b97bac0595474108b48"), "ts" : Timestamp(1412180887, 1) }
如果ts是一个子文档里的字段,MongoDB就不会管它了,保持空时间戳值写入集合
【日期(date)】
BSON日期是一个64位整型,表示从unix纪元(1970-01-01)起的毫秒数,且是有符号的,负数表示1970年之前的日期
举例,使用new Data()创建日期类型;
> var mydate1 = new Date()
举例,使用ISOData()创建日期类型:
> var mydate2 = ISODate()
举例,将日期值转换成字符串返回:
> mydate1.toString()
举例,将日期值中的月份转换成字符串返回:
> mydate1.getMonth()
月份从0开始,即一月为0
== 比较与排序顺序 ==
当不同的BSON类型之间比较值时,MongoDB使用以下比较顺序,从最低到最高:
1. MinKey (internal type)
2. Null
3. Numbers (ints, longs, doubles, decimals)
4. Symbol, String
5. Object
6. Array
7. BinData
8. ObjectId
9. Boolean
10. Date
11. Timestamp
12. Regular Expression
13. MaxKey (internal type)
【数值型】
MongoDB会在比较前将数值型进行转换,转换成同一类型
【字符串】
> 二进制比较
在比较字符串时,MongoDB默认使用简单二进制方式比较
> 校验集
从MongoDB 3.4起,校验集允许用户为字符串比较指定规则,例如大小写和重音符号
校验集语法如下:
{
locale: ,
caseLevel: ,
caseFirst: ,
strength: ,
numericOrdering: ,
alternate: ,
maxVariable: ,
backwards:
}
当指定校验集,locale字段是强制的,其他字段选用
如果集合上未指定校验集,或未在操作时指定校验集,MongoDB使用简单二进制比较
【数组】
对数组的小于比较或升序排序,是对数组里最小值比较,对数组的大于比较或降序排序,是对数组里最大值比较
当一个数组元素(如[1])与普通数据(如2)比较时,同1和2之间的比较是一样的,对于空数组(如[]),在比较时视为小于null或无此字段
【日期和时间戳】
日期对象顺序先于时间戳对象
【不存在的字段】
不存在的字段在比较时视为空的BSON对象,如此,在文档{}和{a:null}上a字段的排序顺序是相等的
【二进制数据】
MongoDB按以下顺序排序BinData:
- 首先,数据长度或大小
- 其次,1字节的子类型
- 最后,按字节依次比较数据
== MongoDB扩展的JSON ==
JSON只能表示BSON所支持的数据类型的子集,为保存类型信息,MongoDB为JSON格式增加了以下扩展:
- 严格模式,任何JSON解析器都能将严格模式标示解析成键值对,然而,只有MongoDB内部JSON解析器能识别该格式传达的类型信息
- mongo shell模式,MongoDB内部JSON解析器和mongo shell能够解析该模式
各数据类型的标示都依赖于被解析的JSON的上下文
【解析器和支持的格式】
> 严格模式输入
以下能解析严格模式里的标示并识别类型信息
- REST接口
- mongoimport工具
- 各MongoDB工具的--query选项
其他JSON解析器,包括mongo shell和db.eval(),能够将严格模式标示解析成键值对,但不能识别出类型信息
> mongo shell模式输入
以下能解析mongo shell模式里的标示并识别类型信息
- mongo shell
- REST接口
- mongoimport工具
- 各MongoDB工具的--query选项
> 严格模式输出
mongoexport、REST和HTTP接口可以在严格模式输出数据
> mongo shell模式输出
bsondump可以在mongo shell模式输出
【BSON数据类型和相关标示】
以下分别介绍严格模式和mongo shell模式的BSON类型和相关标示
> 二进制(data_binary)
Strict Mode : { "$binary": "", "$type": "" }
mongo Shell Mode : BinData ( , )
- 是二进制字符串的base64标示
- 是一个象征数据类型的单字节标示,在严格模式里,它是一个十六进制字符串,在mongo shell模式里,它是一个整型
> 日期(data_date)
Strict Mode : { "$date": "" }
mongo Shell Mode : new Date ( )
在严格模式里,是一个带时间区字段的ISO-8601数据格式,时间区模板为YYYY-MM-DDTHH:mm:ss.mmm
MongoDB的JSON解析器当前不支持导入Unix纪元之前日期的ISO-8601字符串,当格式化纪元前日期或超过系统的time_t类型范围的过去日期,需使用以下格式:
{ "$date" : { "$numberLong" : "" } }
在mongo shell模式里,JSON标示是64位有符号的整型,其值为UTC纪元起的毫秒数
> 时间戳(data_timestamp)
Strict Mode : { "$timestamp": { "t": , "i": } }
mongo Shell Mode : Timestamp( , )
- 是32位无符号整型的JSON标示,其值为自纪元起的秒数
- 是一个32位无符号整型的递增数
> 正则表达式(data_regex)
Strict Mode : { "$regex": "", "$options": "" }
mongo Shell Mode : //
- 是一个有效的JSON字符的字符串
- 是一个包含有效JSON字符和未转意的双引号的字符串,但不包含未转意的正斜杠(/)
- 是一个包含由字母所表示的正则选项的字符串
- 是一个包含字符'g'、'i'、'm'和's'的字符串,由于JavaScript和mongo shell标示支持的选项范围有限,任何不合适的选项在转换到该标示时都将被丢弃
> OID(data_oid)
Strict Mode : { "$oid": "" }
mongo Shell Mode : ObjectId( "" )
是一个有24个字符的十六进制字符串
> DB引用(data_ref)
Strict Mode : { "$ref": "", "$id": "" }
mongo Shell Mode : DBRef("", "")
- 是一个有效的JSON字符的字符串
- 是任何有效的JSON扩展类型
> 未定义类型(data_undefined)
Strict Mode : { "$undefined": true }
mongo Shell Mode : undefined
JavaScript/BSON的未定义类型的标示
不能在查询文档里使用undefined,假设以下文档写入people集合:
db.people.insert( { name : "Sally", age : undefined } )
那么,以下查询都会返回错误:
db.people.find( { age : undefined } )
db.people.find( { age : { $gte : undefined } } )
不过,可以用$type来查询未定义数据,如下:
db.people.find( { age : { $type : 6 } } )
该查询会返回所有age字段值为undefined的文档
> 最小值(data_minkey)
Strict Mode : { "$minKey": 1 }
mongo Shell Mode : MinKey
BSON数据类型的最小值标示,在比较和排序时小于所有其他类型
> 最大值(data_maxkey)
Strict Mode : { "$maxKey": 1 }
mongo Shell Mode : MaxKey
BSON数据类型的最大值标示,在比较和排序时大于所有其他类型
> 大数值(data_numberlong)
Strict Mode : { "$numberLong": "" }
mongo Shell Mode : NumberLong( "" )
大数值是64位有符号整型,必须用引号包围,否则会被当作浮点数,导致精度丢失
例如,以下命令以NumberLong类型写入9223372036854775807,一个有引号一个无引号
db.json.insert( { longQuoted : NumberLong("9223372036854775807") } )
db.json.insert( { longUnQuoted : NumberLong(9223372036854775807) } )
当你读取文档时,longUnquoted值已经变化了,而longQuoted保持正确
{ "_id" : ObjectId("54ee1f2d33335326d70987df"), "longQuoted" : NumberLong("9223372036854775807") }
{ "_id" : ObjectId("54ee1f7433335326d70987e0"), "longUnquoted" : NumberLong("-9223372036854775808") }