#### 第20章: #### MongoDB #### 20.1 MongoDB 基础 MongoDB是由C++编写的高性能、高可用性且易扩展的内存数据库。其特点: - 数据存储格式BSON({"name":"tom"})。 - 面向集合存储,易于存储对象类型和JSON形式的数据。 - 模式自由:一个集合可以存储一个键值对文档,也可以存储多个键值对文档。例如: ``` { "name":"tom" } { "name":tom, "classNum":3 } ``` 以上是两个文档,每个文档可以有一个或者多个键值对 - 支持动态查询:丰富的查询表达式。 - 完整的索引支持:文档内嵌对象和数据都可以创建索引。 - 支持复制和故障恢复:支持主从复制和节点之间的故障恢复。 - 二进制数据存储:MongoDB使用二进制数据存储方式。 - 自动分片:用于支持负载均衡、读写分离。 - 支持多种语言:有多种语言的API。 - MongoDB使用内存映射存储引擎:是一个高效的内存数据库。 - mongodb不支持事务。 ##### MongoDB和传统关系型数据库的差别 - MongoDB`不支持事务`。 - 以JSON类型的方式进行数据查询。 - 文档数据的键值对不固定。例如在MySQL创建表时会先设计每行数据需要的数据列,像名字、年龄、班级等,之后就固定了只能在这些设计了的列范围内操作数据。而MongoDB可以添加只要名字的文档,也可以添加要名字、年龄、班级、体重、身高等不限制条件数量的文档。 - MongoDB会占用一切能占用的内存用于符合它自己逻辑的操作。所以尽量不要把MongoDB和其他服务放在一起。 ##### MongoDB的存储结构 MongoDB的存储结构层次是:数据库包含集合,集合包含文档,文档就是一个或者多个键值对的一条数据。 ##### 数据库 一个MongoDB服务器实例可承载多个数据库,数据库之间完全独立,拥有独立的控制权限,并且在磁盘上不同的数据库放置在不同的文件中。数据库由分为自定义数据库和自带数据库:自定义数据库是开发者用来存储业务数据的,自带数据库是MongoDB服务自身运行所需数据库。 ##### 集合 集合就是一组文档,且是无模式的。无模式指文档可以是五花八门没有条件限制的,比如键值对数量,键的名称、值的名称都可以随意组成。集合里的值还可以是集合,称为`子集合` 。 集合又分为`普通集合`和`固定集合Capped` 。 固定集合是固定大小的性能出色的集合。如果文档超过了其固定空间,则会删除最早的文档为新文档腾出空间。 当第一个文档插入时,集合就会被创建。 合法的集合名: - 集合名不能是空字符串""。 - 集合名不能含有\0字符(空字符),这个字符表示集合名的结尾。 - 集合名不能以"system."开头,这是为系统集合保留的前缀。 - 用户创建的集合名字不能含有保留字符。有些驱动程序的确支持在集合名里面包含,这是因为某些系统生成的集合中包含该字符。除非你要访问这种系统创建的集合,否则千万不要在名字里出现$。  ##### 文档 文档是MongoDB数据的基本单元,可以看作是一条数据。 需要注意的是: 1. 文档中的键/值对是有序的。 2. 文档中的值不仅可以是在双引号里面的字符串,还可以是其他几种数据类型(甚至可以是整个嵌入的文档)。 3. MongoDB区分类型和大小写。 4. MongoDB的文档不能有重复的键。 5. 文档的键是字符串。除了少数例外情况,键可以使用任意UTF-8字符。 6. 每个文档都有一个特殊的键"_id",在所在文档中是唯一的。 7. 键、值区分大小写。 文档键命名规范: - 键不能含有\0 (空字符)。这个字符用来表示键的结尾。 - .和$有特别的意义,只有在特定环境下才能使用。 - 以下划线"_"开头的键是保留的(不是严格要求的)。 ##### 元数据 数据库的信息是存储在集合中。它们使用了系统的命名空间: ``` dbname.system.* ``` 在MongoDB数据库中名字空间 <dbname>.system.* 是包含多种系统信息的特殊集合(Collection),如下: | 集合命名空间 | 描述 | | ------------------------ | ----------------------------------------- | | dbname.system.namespaces | 列出所有名字空间。 | | dbname.system.indexes | 列出所有索引。 | | dbname.system.profile | 包含数据库概要(profile)信息。 | | dbname.system.users | 列出所有可访问数据库的用户。 | | dbname.local.sources | 包含复制对端(slave)的服务器信息和状态。 | 对于修改系统集合中的对象有如下限制。 在{{system.indexes}}插入数据,可以创建索引。但除此之外该表信息是不可变的(特殊的drop index命令将自动更新相关信息)。 {{system.users}}是可修改的。 {{system.profile}}是可删除的。 ##### MongoDB 数据类型 下表为MongoDB中常用的几种数据类型。 | 数据类型 | 描述 | | ------------------ | ------------------------------------------------------------ | | String | 字符串。存储数据常用的数据类型。在 MongoDB 中,UTF-8 编码的字符串才是合法的。 | | Integer | 整型数值。用于存储数值。根据你所采用的服务器,可分为 32 位或 64 位。 | | Boolean | 布尔值。用于存储布尔值(真/假)。 | | Double | 双精度浮点值。用于存储浮点值。 | | Min/Max keys | 将一个值与 BSON(二进制的 JSON)元素的最低值和最高值相对比。 | | Array | 用于将数组或列表或多个值存储为一个键。 | | Timestamp | 时间戳。记录文档修改或添加的具体时间。 | | Object | 用于内嵌文档。 | | Null | 用于创建空值。 | | Symbol | 符号。该数据类型基本上等同于字符串类型,但不同的是,它一般用于采用特殊符号类型的语言。 | | Date | 日期时间。用 UNIX 时间格式来存储当前日期或时间。你可以指定自己的日期时间:创建 Date 对象,传入年月日信息。 | | Object ID | 对象 ID。用于创建文档的 ID。 | | Binary Data | 二进制数据。用于存储二进制数据。 | | Code | 代码类型。用于在文档中存储 JavaScript 代码。 | | Regular expression | 正则表达式类型。用于存储正则表达式。 | ##### ObjectId ObjectId 类似唯一主键,可以很快的去生成和排序,包含 12 bytes(字节,不是比特位),含义是: - 前 4 个字节表示创建 **unix** 时间戳,格林尼治时间 **UTC** 时间,比北京时间晚了 8 个小时 - 接下来的 3 个字节是机器标识码 - 紧接的两个字节由进程 id 组成 PID - 最后三个字节是随机数 :-: ![](https://img.kancloud.cn/5f/7f/5f7f6a2e5f3e49efaa2c118a02dc987c_400x81.png) MongoDB 中存储的文档必须有一个 _id 键。这个键的值可以是任何类型的,默认是个 ObjectId 对象 由于 ObjectId 中保存了创建的时间戳,所以你不需要为你的文档保存时间戳字段,你可以通过 getTimestamp 函数来获取文档的创建时间: ``` > var newObject = ObjectId() > newObject.getTimestamp() ISODate("2017-11-25T07:21:10Z") ``` ObjectId 转为字符串 ``` > newObject.str 5a1919e63df83ce79df8b38f ``` ##### 字符串 BSON 字符串都是 UTF-8 编码。 ##### 时间戳 BSON 有一个特殊的时间戳类型用于 MongoDB 内部使用,与普通的日期类型不相关。 时间戳值是一个64位的值。其中: - 前32位是一个 time_t 值(与Unix新纪元相差的秒数) - 后32位是在某秒中操作的一个递增的`序数` 在单个mongod实例中,时间戳值通常是唯一的。 在复制集中,oplog 有一个 ts 字段。这个字段中的值使用BSON时间戳表示了操作时间。 `BSON 时间戳类型主要用于 MongoDB 内部使用。在大多数情况下的应用开发中,你可以使用 BSON 日期类型。`。 ##### 日期 表示当前距离 Unix新纪元(1970年1月1日)的毫秒数。日期类型是有符号的, 负数表示1970年之前的日期。 ``` > var mydate1 = new Date() //格林尼治时间 > mydate1 ISODate("2018-03-04T14:58:51.233Z") > typeof mydate1 object > var mydate2 = ISODate() //格林尼治时间 > mydate2 ISODate("2018-03-04T15:00:45.479Z") > typeof mydate2 object ``` 这样创建的时间是日期类型,可以使用 JS 中的 Date 类型的方法。 返回一个时间类型的字符串: ``` > var mydate1str = mydate1.toString() > mydate1str Sun Mar 04 2018 14:58:51 GMT+0000 (UTC) > typeof mydate1str string ``` 或者 ``` > Date() Sun Mar 04 2018 15:02:59 GMT+0000 (UTC) ``` #### 20.2 MongoDB的日志 - 系统日志:系统日志在MongoDB数据库中很重要,它记录着MongoDB启动和停止的操作,以及服务器在运行过程中发生的任何异常信息。 - Journal日志:用于数据恢复,记录了所有的写操作。 - oplog日志(主从日志):Replica Sets复制集用于在多台服务器之间备份数据。MongoDB的复制功能是使用操作日志oplog实现的,操作日志包含了主节点的每一次写操作。oplog是主节点的local数据库中的一个固定集合。备份节点通过查询这个集合就可以知道需要进行复制的操作。 - 慢查询日志:MongoDB中使用系统分析器来查找耗时过长的操作。默认情况下,系统分析器处于关闭状态,不会进行任何记录。可以在shell中运行db.setProfilingLevel()开启分析器。 ``` db.setProfilingLevel(level,<slowms>) 0=off 1=slow 2=all ``` 第一个参数是指定级别,不同的级别代表不同的意义,0表示关闭,1表示默认记录耗时大于100毫秒的操作,2表示记录所有操作。第二个参数则是自定义“耗时过长"标准,比如记录所有耗时操作500ms的操作: ``` db.setProfilingLevel(1,500); ``` #### 20.3 MongoDB的启动关闭 ##### 安装及启动 将MongoDB的压缩文件从官网上下载下来后解压。 :-: ![](https://img.kancloud.cn/e4/97/e497cf062361e0fac634514b2e742978_1354x431.png) 1. 使用MongoDB的二进制文件启动: - 在使用二进制启动MongoDB服务 ``` /usr/bin/mongod --path=/mongodb/data ``` 其中/usr/bin/mongod是mongoDB解压文件的二进制运行程序路径。--path指定数据库文件路径。 2. 通过环境变量启动: - 将MongoDB的二进制运行文件加入环境变量。 - 使用环境变量启动。 ``` mongod --path=/mongodb/data ``` 其中mongod是加入到环境变量的MongoDB的二进制运行文件。--path指定数据库文件路径。 3. 将MongoDB加入服务,通过systemctl命令控制MongoDB服务的启动。 4. MongoDB服务可以通过配置文件方式启动,当修改了配置文件mongod.conf后: ``` mongod --config /etc/mongod.conf ``` 或 ``` mongod -f /etc/mongod.conf ``` ##### 关闭 1. 不是后台运行的情况下使用Ctrl+C命令在Shell界面结束MongoDB服务。 2. 运行MongoDB的客户端,使用admin用户,再使用shutdownServer关闭。 ``` cd /usr/bin/ mongo use admin shutdownServer(); ``` 3. kill 命令停止进程。 4. 如果MongoDB服务注册了服务,可以使用systemctl命令关闭。 ``` service mongod stop; //centos7以下 ``` ``` systemctl stop mongod;//centos7以上 ``` #### 20.4 连接MongoDB 启动MongoDB服务后,其他端需要连接MongoDB服务进行操作。 标准 URI 连接语法: ``` mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/[database][?options]] ``` - **mongodb://** 这是固定的格式,必须要指定。 - **username:password@** 可选项,如果设置,在连接数据库服务器之后,驱动都会尝试登录这个数据库 - **host1** 必须的指定至少一个host, host1 是这个URI唯一要填写的。它指定了要连接服务器的地址。如果要连接复制集,请指定多个主机地址。 - **portX** 可选的指定端口,如果不填,默认为27017 - **/database** 如果指定username:password@,连接并验证登录指定数据库。若不指定,默认打开 test 数据库。 - **?options** 是连接选项。如果不使用/database,则前面需要加上/。所有连接选项都是键值对name=value,键值对之间通过&或;(分号)隔开。 标准的连接格式包含了多个选项(options): | 选项 | 描述 | | ------------------- | ------------------------------------------------------------ | | replicaSet=name | 验证replica set的名称。 Impliesconnect=replicaSet. | | slaveOk=true\|false | true:在connect=direct模式下,驱动会连接第一台机器,即使这台服务器不是主。在connect=replicaSet模式下,驱动会发送所有的写请求到主并且把读取操作分布在其他从服务器。false: 在 connect=direct模式下,驱动会自动找寻主服务器. 在connect=replicaSet 模式下,驱动仅仅连接主服务器,并且所有的读写命令都连接到主服务器。 | | safe=true\|false | true: 在执行更新操作之后,驱动都会发送getLastError命令来确保更新成功。(还要参考 wtimeoutMS).false: 在每次更新之后,驱动不会发送getLastError来确保更新成功。 | | w=n | 驱动添加 { w : n } 到getLastError命令. 应用于safe=true。 | | wtimeoutMS=ms | 驱动添加 { wtimeout : ms } 到 getlasterror 命令. 应用于 safe=true. | | fsync=true\|false | true: 驱动添加 { fsync : true } 到 getlasterror 命令.应用于 safe=true.false: 驱动不会添加到getLastError命令中。 | | journal=true\|false | 如果设置为 true, 同步到 journal (在提交到数据库前写入到实体中). 应用于 safe=true | | connectTimeoutMS=ms | 可以打开连接的时间。 | | socketTimeoutMS=ms | 发送和接受sockets的时间。 | 例子: ``` mongodb://admin:123456@localhost/test ``` 账号admin,密码123456,连接到localhost的test数据库。 #### 20.5 PHP7操作MongoDB 首先安装适合PHP7的MongoDB扩展(注意自己的路径): ``` /usr/local/php7/bin/pecl install mongodb ``` 接下来我们打开 php.ini 文件,添加 **extension=mongodb.so** 配置。 ##### 连接 ``` $manager = new MongoDB\Driver\Manager("mongodb://localhost:27017"); //对应标准URI链接语法 ``` ##### 插入数据 ``` <?php $bulk = new MongoDB\Driver\BulkWrite; $document = ['_id' => new MongoDB\BSON\ObjectID, 'name' => '测试数据1']; $_id= $bulk->insert($document); $manager = new MongoDB\Driver\Manager("mongodb://localhost:27017"); $writeConcern = new MongoDB\Driver\WriteConcern(MongoDB\Driver\WriteConcern::MAJORITY, 1000); $result = $manager->executeBulkWrite('collec1.test1', $bulk, $writeConcern); ?> ``` ##### 读取数据 ``` <?php $manager = new MongoDB\Driver\Manager("mongodb://localhost:27017"); // 插入数据 $bulk = new MongoDB\Driver\BulkWrite; $bulk->insert(['x' => 1, 'name'=>'baidu', 'url' => 'http://www.baidu.com']); $bulk->insert(['x' => 2, 'name'=>'Google', 'url' => 'http://www.google.com']); $bulk->insert(['x' => 3, 'name'=>'taobao', 'url' => 'http://www.taobao.com']); $manager->executeBulkWrite('collec1.test1', $bulk); $filter = ['x' => ['$gt' => 1]]; $options = [ 'projection' => ['_id' => 0], 'sort' => ['x' => -1], ]; // 查询数据 $query = new MongoDB\Driver\Query($filter, $options); $cursor = $manager->executeQuery('collec1.test1', $query); foreach ($cursor as $document) { print_r($document); } ?> ``` 输出结果为: ``` stdClass Object ( [x] => 3 [name] => taobao [url] => http://www.taobao.com ) stdClass Object ( [x] => 2 [name] => Google [url] => http://www.google.com ) ``` ##### 更新数据 ``` <?php $bulk = new MongoDB\Driver\BulkWrite; $bulk->update( ['x' => 2], ['$set' => ['name' => 'baidu', 'url' => 'www.baidu.com']], ['multi' => false, 'upsert' => false] ); $manager = new MongoDB\Driver\Manager("mongodb://localhost:27017"); $writeConcern = new MongoDB\Driver\WriteConcern(MongoDB\Driver\WriteConcern::MAJORITY, 1000); $result = $manager->executeBulkWrite('collec1.test1', $bulk, $writeConcern); ?> ``` ##### 删除数据 ``` <?php $bulk = new MongoDB\Driver\BulkWrite; $bulk->delete(['x' => 1], ['limit' => 1]); // limit 为 1 时,删除第一条匹配数据 $bulk->delete(['x' => 2], ['limit' => 0]); // limit 为 0 时,删除所有匹配数据 $manager = new MongoDB\Driver\Manager("mongodb://localhost:27017"); $writeConcern = new MongoDB\Driver\WriteConcern(MongoDB\Driver\WriteConcern::MAJORITY, 1000); $result = $manager->executeBulkWrite('collec1.test1', $bulk, $writeConcern); ?> ``` #### 20.6 命令 ##### 数据库命令 切换数据库 ``` use dbname; ``` 切换时没有数据库会创建数据库。 查询显示所有数据库: ``` show dbs; ``` 删除当前使用数据库: ``` db.dropDatabase(); ``` 从指定主机克隆数据库: ``` db.cloneDatabase("192.168.200.1"); ``` 会克隆指定主机的同名数据库到当前数据库。 从指定主机上复制数据库数据到某个数据库: ``` db.copyDatabase('db1','test1',192,168,200,1); ``` 将192.168.200.1的db1数据库数据复制到本机test1数据库中。 查看当前使用的数据库: ``` db.getName(); ``` 显示当前db状态: ``` db.status(); ``` db版本: ``` db.version(); ``` 查看当前db的链接机器地址: ``` db.getMongo(); ``` 查看之前的错误信息: ``` db.getPrevError(); ``` 清除错误信息: ``` db.resetError(); ``` ##### 集合命令 创建集合 ``` db.createCollection("t1"); ``` 或创建固定集合: ``` db.createCollection("t2",{size:30,capped:true,max:100}); ``` 上面创建了固定集合t2,固定大小30,限制空间大小为30,最大条数为100。 - capped:设置是否为固定集合。 - size:设置使用的空间大小,没有不限制。 - max:设置最大文档条数。 - autoIndexId:是否使用`_id`作为索引。 size优先级高于max。 显示当前数据库集合: ``` show collections; ``` 使用集合: ``` db.getCollection('coll1'); db.coll1; //数字集合不能使用这种方式 ``` 查询集合的数据条数: ``` db.collname.count(); ``` 查看集合数据字节大小: ``` db.collname.dataSize(); ``` 查看集合索引大小: ``` db.collname.totalIndexSize(); ``` 为集合分配的空间大小: ``` db.collname.storageSize(); ``` 显示结合总大小,包括索引、数据、分配的空间大小: ``` db.collname.totalSize(); ``` 显示当前集合所在的db: ``` db.collname.getDB(); ``` 显示当前集合状态: ``` db.collname.status(); ``` 显示当前db所有集合的状态信息: ``` db.printCollectionsStatus(); ``` 删除当前集合: ``` db.collname.drop(); ``` 重命名集合: ``` db.collname1.renameCollection('collname2'); ``` ##### 文档命令 写入文档: ``` db.user.insert({"name":"tom"}); ``` 或者: ``` db.user:save({"name":"tom"}); ``` save比insert多一项写入功能。 查看文档: ``` db.user.find(); ``` 更新文档: MongoDB使用sava()和update()来更新集合中的文档。 save(): ``` db.user.save({"_id":Object('56321315464asdz5x4caqhg354432d'),"name":"tom","age":18}); ``` update(): ``` db.collection.update( 查询条件, 整个文档或修改器, upsert:boolean, multi:boolean或multi文档, writeConcern:异常信息等级 ); ``` 参数说明: 参数一:查询条件用于定位到需要修改的文档,与find语句使用方式一样。 参数二:文档或修改器。参数为文档时,传入的文档会替代现有文档。参数是修改其时,会根据修改器文档做相应改动。 参数三:upsert设置如果不存在记录的情况是否写入新文档。可选参数,默认不写入。 参数四:multi设置false只更新找到的第一条记录,设置true更新根据条件查找到的所有记录,默认false。 参数五:可选,抛出异常的级别。保障更新操作可靠性。 例子: ``` db.user.update( {"name":"tom"}, {$set:{"age":20,"company":"baidu"}}, true, {multi:true}, WriteConnern.SAFE ); ``` 删除文档: ``` db.collection.remove( 查询条件, justOne:boolean, wriconcern:异常信息等级 ); ``` 参数说明: 参数一:查询条件用于定位到需要删除的文档,与find语句使用方式一样。 参数二:是否只删除一条。默认false,删除所有匹配的。 参数三:抛出异常级别。 ``` db.user.remove({"name":"tom"},1); db.user.remove({"name":"tom"}); ``` 更新文档并返回文档: ``` db.user.findAndModify({ query:{age:{$gte:25}}, sort:{age:1}, update:{$set:{name:'a2'},$inc:{age:2}} }); ``` 删除文档并返回文档: ``` db.user.findAndModify({ query:{age:{$gte:25}}, sort:{age:1}, remove:true }); ``` 查询满足条件的文档数量: ``` db.user.count({ $or:[{age:14},{age:15}] }); ``` ##### 索引命令 索引用于加速对该键的查询。索引通常能够极大的提高查询的效率,如果没有索引,MongoDB在读取数据时必须扫描集合中的每个文件并选取那些符合查询条件的记录。 这种扫描全集合的查询效率是非常低的,特别在处理大量的数据时,查询可以要花费几十秒甚至几分钟,这对网站的性能是非常致命的。 索引是特殊的数据结构,索引存储在一个易于遍历读取的数据集合中,索引是对数据库表中一列或多列的值进行排序的一种结构。 创建索引: ``` db.user.ensureIndex({age:1}); db.user.ensureIndex({name:1,age:1}); ``` 集合可以创建单索引或复合索引。1表示数据以升序排列,-1表示数据以降序排列。并且依照从左向右排序的原则。上面的第二行表示创建复合索引先按name的升序排列,再按age的降序排列。 索引参数: | Parameter | Type | Description | | ------------------ | ------------- | ------------------------------------------------------------ | | background | Boolean | 建索引过程会阻塞其它数据库操作,background可指定以后台方式创建索引,即增加 "background" 可选参数。 "background" 默认值为**false**。 | | unique | Boolean | 建立的索引是否唯一。指定为true创建唯一索引。默认值为**false**. | | name | string | 索引的名称。如果未指定,MongoDB的通过连接索引的字段名和排序顺序生成一个索引名称。 | | dropDups | Boolean | **3.0+版本已废弃**。在建立唯一索引时是否删除重复记录,指定 true 创建唯一索引。默认值为 **false**. | | sparse | Boolean | 对文档中不存在的字段数据不启用索引;这个参数需要特别注意,如果设置为true的话,在索引字段中不会查询出不包含对应字段的文档.。默认值为 **false**. | | expireAfterSeconds | integer | 指定一个以秒为单位的数值,完成 TTL设定,设定集合的生存时间。 | | v | index version | 索引的版本号。默认的索引版本取决于mongod创建索引时运行的版本。 | | weights | document | 索引权重值,数值在 1 到 99,999 之间,表示该索引相对于其他索引字段的得分权重。 | | default_language | string | 对于文本索引,该参数决定了停用词及词干和词器的规则的列表。 默认为英语 | | language_override | string | 对于文本索引,该参数指定了包含在文档中的字段名,语言覆盖默认的language,默认值为 language. | 查询集合所有索引: ``` db.user.getIndexes(); ``` 查看集合总索引记录大小: ``` db.user.totalIndexSize(); ``` 读取当前集合的所有index信息: ``` db.user.reIndex(); ``` 删除指定索引: ``` db.user.dropIndex("Index1"); ``` 删除集合所有索引: ``` db.user.dropIndexes(); ``` ##### 查询命令 MongoDB使用find()来进行文档查询。有两个参数: - 参数1:查询条件。 - 参数2:指定返回字段。_id默认返回。 ``` db.user.find({'name':'tom'},{'age':1}); ``` 返回name为tom的文档,并且只按升序返回age字段。 游标用于存放find执行结果并且每次只能取一条: ``` var cursor = db.user.find(); while(cursor.hasNext()){ var temp = cursor.next(); print(temp.name); } ``` 游标实现了迭代器,可以使用foreach: ``` var cursor = db.user.find(); cursor.forEach(function(temp)){ print(temp.name); } ``` 与操作: ``` db.user.find({'name':'tom','age':18}); ``` 或操作$or: ``` db.user.find({$or:[{'name':'tom'},{'age':18}]}); ``` 大于操作$gt: ``` db.user.find({age:{$gt:18}}); ``` 小于操作$lt: ``` db.user.find({age:{$lt:18}}); ``` 大于等于操作$gte: ``` db.user.find({age:{$gte:18}}); ``` 小于等于操作$lte: ``` db.user.find({age:{$lte:18}}); ``` 类型查询$type: ``` db.user.find({'name':{$type":2}); db.user.find({'name':{$type":'string'}); ``` | Type | Number | Alias | Notes | | ---------------------- | ------ | --------------------- | ------------------ | | Double | 1 | "double" | | | String | 2 | "string" | | | Object | 3 | "object" | | | Array | 4 | "array" | | | Binary data | 5 | "binData" | | | Undefined | 6 | "underfined" | Deprecated | | ObjectId | 7 | "objectId" | | | Boolean | 8 | "bool" | | | Date | 9 | "date" | | | Null | 10 | "null" | | | Regular Expression | 11 | "regex" | | | DBPointer | 12 | "dbPointer" | Deprecated | | JavaScript | 13 | "javascript" | | | Symbol | 14 | "symbol" | Deprecated | | JavaScript(with scope) | 15 | "javascriptWithScope" | | | 32-bit integer | 16 | "int" | | | Timestamp | 17 | "timestamp" | | | 64-bit integer | 18 | "long" | | | Decimal128 | 19 | "decimal" | new in version 3.4 | | Min key | -1 | "minKey" | | | Max key | 127 | "maxKey" | | ​ 数据类型与$type参数对应表 是否存在$exists: ``` db.user.find({"age":{$exists:true}}); ``` 取模操作$mod: ``` db.user.find({"age":{$mod:[10,0]}}); ``` 不等于$ne: ``` db.user.find({"age":{$ne:18}}); ``` 包含$in: ``` db.user.find({"age":{$in:[18,20,21]}}); ``` 不包含$nin: ``` db.user.find({"age":{$nin:[18,20,21]}}); ``` 反匹配$not: ``` db.user.find({$not:{"age":{$in:[18,20,21]}}}); ``` ##### 聚合查询 MongoDB 中聚合(aggregate)主要用于处理数据(诸如统计平均值,求和等),并返回计算后的数据结果。类似于SQL的count(*)。 语法: ``` >db.COLLECTION_NAME.aggregate(AGGREGATE_OPERATION) ``` 例子: ``` { _id: ObjectId(7df78ad8902c) title: 'MongoDB Overview', description: 'MongoDB is no sql database', by_user: 'li', tags: ['mongodb', 'database', 'NoSQL'], likes: 100 }, { _id: ObjectId(7df78ad8902d) title: 'NoSQL Overview', description: 'No sql database is very fast', by_user: 'li', tags: ['mongodb', 'database', 'NoSQL'], likes: 10 }, { _id: ObjectId(7df78ad8902e) title: 'Neo4j Overview', description: 'Neo4j is no sql database', by_user: 'wang', tags: ['neo4j', 'database', 'NoSQL'], likes: 750 }, ``` 使用aggregate()计算结果如下: ``` > db.mycol.aggregate([{$group : {_id : "$by_user", num_tutorial : {$sum : 1}}}]) { "result" : [ { "_id" : "li", "num_tutorial" : 2 }, { "_id" : "wang", "num_tutorial" : 1 } ], "ok" : 1 } ``` 聚合一般和group组合使用。 一些聚合的表达式: | 表达式 | 描述 | 实例 | | --------- | ---------------------------------------------- | ------------------------------------------------------------ | | $sum | 计算总和。 | db.mycol.aggregate([{$group : {_id : "$by_user", num_tutorial : {$sum : "$likes"}}}]) | | $avg | 计算平均值 | db.mycol.aggregate([{$group : {_id : "$by_user", num_tutorial : {$avg : "$likes"}}}]) | | $min | 获取集合中所有文档对应值得最小值。 | db.mycol.aggregate([{$group : {_id : "$by_user", num_tutorial : {$min : "$likes"}}}]) | | $max | 获取集合中所有文档对应值得最大值。 | db.mycol.aggregate([{$group : {_id : "$by_user", num_tutorial : {$max : "$likes"}}}]) | | $push | 在结果文档中插入值到一个数组中。 | db.mycol.aggregate([{$group : {_id : "$by_user", url : {$push: "$url"}}}]) | | $addToSet | 在结果文档中插入值到一个数组中,但不创建副本。 | db.mycol.aggregate([{$group : {_id : "$by_user", url : {$addToSet : "$url"}}}]) | | $first | 根据资源文档的排序获取第一个文档数据。 | db.mycol.aggregate([{$group : {_id : "$by_user", first_url : {$first : "$url"}}}]) | | $last | 根据资源文档的排序获取最后一个文档数据 | db.mycol.aggregate([{$group : {_id : "$by_user", last_url : {$last : "$url"}}}]) | ##### 管道 管道在Unix和Linux中一般用于将当前命令的输出结果作为下一个命令的参数。 MongoDB的聚合管道将MongoDB文档在一个管道处理完毕后将结果传递给下一个管道处理。管道操作是可以重复的。 表达式:处理输入文档并输出。表达式是无状态的,只能用于计算当前聚合管道的文档,不能处理其它的文档。 这里我们介绍一下聚合框架中常用的几个操作: - $project:修改输入文档的结构。可以用来重命名、增加或删除域,也可以用于创建计算结果以及嵌套文档。 - $match:用于过滤数据,只输出符合条件的文档。$match使用MongoDB的标准查询操作。 - $limit:用来限制MongoDB聚合管道返回的文档数。 - $skip:在聚合管道中跳过指定数量的文档,并返回余下的文档。 - $unwind:将文档中的某一个数组类型字段拆分成多条,每条包含数组中的一个值。 - $group:将集合中的文档分组,可用于统计结果。 - $sort:将输入文档排序后输出。 - $geoNear:输出接近某一地理位置的有序文档。 1、$project实例 ``` db.article.aggregate( { $project : { title : 1 , author : 1 , }} ); ``` 这样的话结果中就只还有_id,tilte和author三个字段了,默认情况下_id字段是被包含的,如果要想不包含_id话可以这样: ``` db.article.aggregate( { $project : { _id : 0 , title : 1 , author : 1 }}); ``` 2.$match实例 ``` db.articles.aggregate( [ { $match : { score : { $gt : 70, $lte : 90 } } }, { $group: { _id: null, count: { $sum: 1 } } } ] ); ``` $match用于获取分数大于70小于或等于90记录,然后将符合条件的记录送到下一阶段$group管道操作符进行处理。 3.$skip实例 ``` db.article.aggregate( { $skip : 5 }); ``` 经过$skip管道操作符处理后,前五个文档被"过滤"掉。 #### 20.7 MongoDB 关系 MongoDB 中的关系可以是: - 1:1 (1对1) - 1: N (1对多) - N: 1 (多对1) - N: N (多对多) ##### 嵌入式关系 使用嵌入式方法,我们可以把用户地址嵌入到用户的文档中: ``` { "_id":ObjectId("52ffc33cd85242f436000001"), "contact": "987654321", "dob": "01-01-1991", "name": "Tom Benzamin", "address": [ { "building": "22 A, Indiana Apt", "pincode": 123456, "city": "Los Angeles", "state": "California" }, { "building": "170 A, Acropolis Apt", "pincode": 456789, "city": "Chicago", "state": "Illinois" }] } ``` 嵌入式关系的缺点是当嵌入的文档不断增加,数据量变大会影响性能。 ##### 引用式关系 MongoDB 引用有两种: - 手动引用(Manual References) - DBRefs ##### 手动引用 引用式关系是设计数据库时经常用到的方法,这种方法把用户数据文档和用户地址数据文档分开,通过引用文档的 **id** 字段来建立关系。 ``` { "_id":ObjectId("52ffc33cd85242f436000001"), "contact": "987654321", "dob": "01-01-1991", "name": "Tom Benzamin", "address_ids": [ ObjectId("52ffc4a5d85242602e000000"), ObjectId("52ffc4a5d85242602e000001") ] } ``` 我们可以读取这些用户地址的对象id(ObjectId)来获取用户的详细地址信息。 这种方法需要两次查询,第一次查询用户地址的对象id(ObjectId),第二次通过查询的id获取用户的详细地址信息。这样对性能的影响不会太大,这种是`手动引用`。 ``` >var result = db.users.findOne({"name":"Tom Benzamin"},{"address_ids":1}) >var addresses = db.address.find({"_id":{"$in":result["address_ids"]}}) ``` ##### DBRefs引用 DBRef的形式: ``` { $ref : , $id : , $db : } ``` 三个字段表示的意义为: - $ref:集合名称 - $id:引用的id - $db:数据库名称,可选参数 以下实例中用户数据文档使用了 DBRef, 字段 address: ``` { "_id":ObjectId("53402597d852426020000002"), "address": { "$ref": "address_home", "$id": ObjectId("534009e4d852427820000002"), "$db": "runoob"}, "contact": "987654321", "dob": "01-01-1991", "name": "Tom Benzamin" } ``` **address** DBRef 字段指定了引用的地址文档是在 runoob 数据库下的 address_home 集合,id 为 534009e4d852427820000002。 以下代码中,我们通过指定 $ref 参数(address_home 集合)来查找集合中指定id的用户地址信息: ``` >var user = db.users.findOne({"name":"Tom Benzamin"}) >var dbRef = user.address >db[dbRef.$ref].findOne({"_id":(dbRef.$id)}) ``` 实例返回了 address_home 集合中的地址数据: ``` { "_id" : ObjectId("534009e4d852427820000002"), "building" : "22 A, Indiana Apt", "pincode" : 123456, "city" : "Los Angeles", "state" : "California" } ``` #### 20.8 索引 ##### MongoDB 覆盖索引查询 官方的MongoDB的文档中说明,覆盖查询是以下的查询: - 所有的查询字段是索引的一部分。 - 所有的查询返回字段在同一个索引中。 (与MySQL的索引覆盖基本一致:如果一个索引包含(或覆盖)所有需要查询的字段的值,称为‘覆盖索引’。即只需扫描索引而无须回表。) 由于所有出现在查询中的字段是索引的一部分, MongoDB 无需在整个数据文档中检索匹配查询条件和返回使用相同索引的查询结果。 因为索引存在于RAM中,从索引中获取数据比通过扫描文档读取数据要快得多。 ##### 使用覆盖索引查询 ``` { "_id": ObjectId("53402597d852426020000002"), "contact": "987654321", "dob": "01-01-1991", "gender": "M", "name": "Tom Benzamin", "user_name": "tombenzamin" } ``` 我们在 users 集合中创建联合索引,字段为 gender 和 user_name : ``` >db.users.ensureIndex({gender:1,user_name:1}) ``` 现在,该索引会覆盖以下查询: ``` >db.users.find({gender:"M"},{user_name:1,_id:0}) ``` ##### 数组字段索引和子文档索引 例子(集合users): ``` { "address": { "city": "Los Angeles", "state": "California", "pincode": "123" }, "tags": [ "music", "cricket", "blogs" ], "name": "Tom Benzamin" } ``` 以上文档包含了 address 子文档和 tags 数组。 ##### 数组字段索引 对集合中的数组 tags 建立索引。 使用以下命令创建数组索引: ``` >db.users.ensureIndex({"tags":1}) ``` 创建索引后可以这样检索集合的 tags 字段: ``` >db.users.find({tags:"cricket"}) ``` ##### 子文档索引 为子文档创建索引,: ``` >db.users.ensureIndex({"address.city":1,"address.state":1,"address.pincode":1}) ``` 创建索引后可以使用子文档的字段来检索数据: ``` >db.users.find({"address.city":"Los Angeles"}) ``` 查询表达不一定遵循指定的索引的顺序,mongodb 会自动优化。所以上面创建的索引将支持以下查询: ``` >db.users.find({"address.state":"California","address.city":"Los Angeles"}) ``` 同样支持以下查询: ``` >db.users.find({"address.city":"Los Angeles","address.state":"California","address.pincode":"123"}) ``` ##### MongoDB 索引限制 ##### 额外开销 每个索引占据一定的存储空间,在进行插入,更新和删除操作时MongoDB会维护索引对索引进行操作。所以对集合进行读取操作建议不使用索引。 ##### 内存(RAM)使用 由于索引是存储在内存(RAM)中。如果索引的大小大于内存的限制,MongoDB会删除一些索引,这将导致性能下降。 ##### 查询限制 索引不能被以下的查询使用: - 正则表达式及非操作符,如 $nin, $not, 等。 - 算术运算符,如 $mod, 等。 - $where 子句 可以用explain来查看。使用检测语句explain是个好习惯。 ##### 索引键限制 从2.6版本开始,如果现有的索引字段的值超过索引键的限制,MongoDB中不会创建索引。 ##### 插入文档超过索引键限制 如果文档的索引字段值超过了索引键的限制,MongoDB不会将任何文档转换成索引的集合。 ##### 最大范围 - 集合中索引不能超过64个 - 索引名的长度不能超过128个字符 - 一个复合索引最多可以有31个字段 #### 20.9 MongoDB 查询分析 查询分析可以确保我们所建立的索引是否有效,是性能分析工具。 MongoDB 查询分析常用函数有:explain() 和 hint()。 ##### 使用 explain() explain 操作提供了查询信息,使用索引及查询统计等。有利于我们对索引的优化。 接下来我们在 users 集合中创建 gender 和 user_name 的索引: ``` >db.users.ensureIndex({gender:1,user_name:1}) ``` 现在在查询语句中使用 explain : ``` >db.users.find({gender:"M"},{user_name:1,_id:0}).explain() ``` 以上的 explain() 查询返回如下结果: ``` { "cursor" : "BtreeCursor gender_1_user_name_1", "isMultiKey" : false, "n" : 1, "nscannedObjects" : 0, "nscanned" : 1, "nscannedObjectsAllPlans" : 0, "nscannedAllPlans" : 1, "scanAndOrder" : false, "indexOnly" : true, "nYields" : 0, "nChunkSkips" : 0, "millis" : 0, "indexBounds" : { "gender" : [ [ "M", "M" ] ], "user_name" : [ [ { "$minElement" : 1 }, { "$maxElement" : 1 } ] ] } } ``` 现在,我们看看这个结果集的字段: - **indexOnly**: 字段为 true ,表示我们使用了索引。 - **cursor**:因为这个查询使用了索引,MongoDB 中索引存储在B树结构中,所以这是也使用了 BtreeCursor 类型的游标。如果没有使用索引,游标的类型是 BasicCursor。这个键还会给出你所使用的索引的名称,你通过这个名称可以查看当前数据库下的system.indexes集合(系统自动创建,由于存储索引信息,这个稍微会提到)来得到索引的详细信息。 - **n**:当前查询返回的文档数量。 - **nscanned/nscannedObjects**:表明当前这次查询一共扫描了集合中多少个文档,我们的目的是,让这个数值和返回文档的数量越接近越好。 - **millis**:当前查询所需时间,毫秒数。 - **indexBounds**:当前查询具体使用的索引。 #### 使用 hint() 虽然MongoDB查询优化器一般工作的很不错,但是也可以使用 hint 来强制 MongoDB 使用一个指定的索引。 这种方法某些情形下会提升性能。 一个有索引的 collection 并且执行一个多字段的查询(一些字段已经索引了)。 如下查询实例指定了使用 gender 和 user_name 索引字段来查询: ``` >db.users.find({gender:"M"},{user_name:1,_id:0}).hint({gender:1,user_name:1}) ``` 可以使用 explain() 函数来分析以上查询: ``` >db.users.find({gender:"M"},{user_name:1,_id:0}).hint({gender:1,user_name:1}).explain() ``` #### 20.10 MongoDB 复制(副本集) MongoDB复制是将数据同步在多个服务器的过程。主要作用: - 主从复制。 - 数据冗余、数据安全。 - 硬件故障和服务中断时的数据恢复。 :-: ![](https://img.kancloud.cn/82/9a/829a4d1a0edfd634011168a4a0205380_501x417.png) ​ MongoDB主从复制架构图 MongoDB的主从复制至少是一主一从,可以一主多从。当客户端对MongoDB服务器主节点进行写操作oplog。从服务器节点会定时轮询主服务器节点,将写操作oplog同步,保证主节点与从节点的数据一致性。 #### 20.11 MongoDB其他相关 对MongoDB分片、正则表达式、监控、GridFS,Javascript使用MongoDB等相关知识有兴趣的同学请自行查阅资料。