0. 背景
MongoDB 的一些基础知识和使用。
MongoDB
1. 基础知识
六个简单的概念:
- (1) database(数据库):MongoDB中 也有 数据库 的概念,和关系型数据中的的"数据库"一样的概念。一个 MongoDB 实例中,可以有零个或多个数据库,用于存储数据。
- (2) collections (集合):一个数据库中可以有多个 collections (集合)。它和传统意义上的 table 是一样的东西。
- (3) documents (文档):一个集合由多个 documents (文档)组成。一个文档可以看成是一条数据的记录( row 或者 record)。
- (4) fields (字段:一个文档是由多个 fields (字段)组成。它就是 columns。
- (5) Indexes (索引): MongoDB 的索引和 RDBMS 中的一样。
- (6) Cursors (游标):当你问 MongoDB 拿数据的时候,它会给你返回一个结果集的指针而不是真正的数据,这个指针我们叫它游标。
概念对照表:
MongoDB | 传统的关系型数据库 |
---|---|
database | database, 相同 |
collections | table |
documents | row |
fields | columns |
Indexes | Indexes 相同 |
Cursors | - |
当我们从 MongoDB 获取数据的时候,我们通过 cursor 来操作,读操作会被延迟到需要实际数据的时候才会执行。
核心差异在于,在MongoDB里,collection中的每个documents都可以有自己独立的 field (字段),而关系型数据中每行的字段都智能相同
要点就是,集合不对存储内容严格限制 (所谓的无模式(schema-less))。字段由每个独立的文档进行跟踪处理。
总结:MongoDB 可以每行数据的结构都不同,支持非结构化数据。 区别于 传统的严格结构化数据。
2. 基本操作
2.1 连接到数据库
MongoDB 的 shell MongoDB 的 shell 是一个连接数据库服务的客户端控制台工具。MongoDB 启动shell ,在命令行输入:
代码语言:javascript复制mongo
shell 用的是 JavaScript。 因为这是一个 JavaScript shell,如果你输入的命令漏了 (),你会看到这个命令的源码,拿到一个以 function (...){ 开头的返回的内容。
MongoDB 内部用二进制序列化 JSON 格式,称为 BSON。
如果要操作当前数据库,用 db ,比如 db.help() 或者 db.stats()。
use 用来切换数据库,比如,输入 use learn。
大多数情况下我们会操作集合, 而不是数据库。比如:用 db.COLLECTION_NAME ,比如 db.unicorns.help() 或者 db.unicorns.count()。
注意,除你指定的字段之外,会多出一个 _id 字段。每个文档都会有一个唯一 _id 字段。你可以自己生成一个,或者让 MongoDB 帮你生成一个 ObjectId 类型的。
2.2 插入数据
代码语言:javascript复制db.unicorns.insert({name: 'Aurora',
gender: 'f', weight: 450})
2.3 删除数据
代码语言:javascript复制db.unicorns.remove({})。
2.4 查询
掌握选择器(Selector):MongoDB 的查询选择器就像 SQL 语句里面的 where 一样。
因此,你会在对集合的文档做查找,计数,更新,删除的时候用到它。选择器是一个 JSON 对象,最简单的是就是用 {} 匹配所有的文档。比如可以用 {gender:'f'}。
{field: value} 用来查找那些 field 的值等于 value 的文档。 {field1: value1, field2: value2} 相当于 and 查询。还有
lte,
gte 和 $ne 被用来处理 小于,小于等于,大于,大于等于,和不等于操作。
代码语言:javascript复制db.unicorns.find({gender: 'm',
weight: {$gt: 700}})
db.unicorns.find({gender: {$ne: 'f'},
weight: {$gte: 701}})
2.5 判断是否存在
$exists 用来匹配字段是否存在,比如:
代码语言:javascript复制db.unicorns.find({
vampires: {$exists: false}})
2.6 是否被包含用 $in
'$in' 被用来匹配查询文档在我们传入的数组参数中是否存在匹配值,比如:
代码语言:javascript复制db.unicorns.find({
loves: {$in:['apple','orange']}})
2.7 逻辑操作中的 or,通过 $or 操作符 来操作。
使用 $or 操作符,再给它一个我们要匹配的数组:
代码语言:javascript复制db.unicorns.find({gender: 'f',
$or: [{loves: 'apple'},
{weight: {$lt: 500}}]})
2.8 查询 _id 字段
_id 字段生成的 ObjectId 可以这样查询:
代码语言:javascript复制db.unicorns.find(
{_id: ObjectId("TheObjectId")})
3. 更新数据 (Update)
如果改变一个或者几个字段的值的时候,你应该用 MongoDB 的 $set 操作。
$set 操作
代码语言:javascript复制db.unicorns.update({weight: 590}, {$set: {
name: 'Roooooodles',
dob: new Date(1979, 7, 18, 18, 44),
loves: ['apple'],
gender: 'm',
vampires: 99}})
直接用 update 会产生覆盖效果,谨慎使用:
代码语言:javascript复制db.unicorns.update({name: 'Roooooodles'},
{weight: 590})
数值的增减
$inc 可以用来给一个字段增加一个正/负值
db.unicorns.update({name: 'Pilot'}, {$inc: {vampires: -2}})
增加新自动,用 $push
字段加一个值,通过 $push 操作:
db.unicorns.update({name: 'Aurora'}, {$push: {loves: 'sugar'}})
Upserts
update 语法还支持 upsert 更新,即:在文档中找到匹配值时更新它,无匹配时向文档插入新值。
要使用 upsert 我们需要向 update 写入第三个参数 {upsert:true},示例:
代码语言:javascript复制db.hits.update({page: 'unicorns'},
{$inc: {hits: 1}}, {upsert:true});
db.hits.find();
批量 Updates时,multi 选项需要设为 true:
代码语言:javascript复制db.unicorns.update({},
{$set: {vaccinated: true }},
{multi:true});
db.unicorns.find({vaccinated: true});
4. 查询
字段选择
find 有第二个可选参数,叫做 "projection"。这个参数是我们要检索或者排除字段的列表。
代码语言:javascript复制db.unicorns.find({}, {name: 1});
默认的,_id 字段总是会返回的。我们可以通过这样显式的把它从返回结果中排除 {name:1, _id: 0}。
排序(Ordering)
sort 用于排序,我们指定我们希望排序的字段,以 JSON 方式,其中 1 表示升序 -1 表示降序。比如:
代码语言:javascript复制//heaviest unicorns first
db.unicorns.find().sort({weight: -1})
//by unicorn name then vampire kills:
db.unicorns.find().sort({name: 1,
vampires: -1})
MongoDB 对未经索引的字段进行排序是有大小限制的。如果你试图对一个非常大的没有经过索引的结果集进行排序的话,你会得到个异常。
分页(Paging)
对结果分页可以通过 limit
和 skip
游标方法来实现。比如:
db.unicorns.find()
.sort({weight: -1})
.limit(2)
.skip(1)
通过 limit
和 sort
的配合,可以在对非索引字段进行排序时避免引起问题。
count 计数
shell 中可以直接对一个集合执行 count
,像这样:
db.unicorns.count({vampires: {$gt: 50}})
实际上,count
是一个 cursor
的方法,shell 只是简单的提供了一个快捷方式。
以不提供快捷方式的方法来执行的时候需要这样:
代码语言:javascript复制db.unicorns.find({vampires: {$gt: 50}})
.count()
5. 数据建模
不支持 join
mongoDB 没有 join (链接, 比如 内连接inner Join,外连接out join)。传统数据库中的 join 基本上意味着不可扩展。就是说,如果想把数据水平扩展,你只能放弃在使用join。事实就是,数据之间的关系, 在 MongoDB 中无法直接表达和查询。只能在我们的应用代码中自己实现,需要进行二次查询 find ,把相关数据保存到另一个集合中。
示例:
先添加一个名叫 Leto 的 主管
代码语言:javascript复制db.employees.insert({_id: ObjectId(
"4d85c7039ab0fd70a117d730"),
name: 'Leto'})
然后再加几个工人,把他们的 主管 设置为 Leto:
代码语言:javascript复制db.employees.insert({_id: ObjectId(
"4d85c7039ab0fd70a117d731"),
name: 'Duncan',
manager: ObjectId(
"4d85c7039ab0fd70a117d730")});
db.employees.insert({_id: ObjectId(
"4d85c7039ab0fd70a117d732"),
name: 'Moneo',
manager: ObjectId(
"4d85c7039ab0fd70a117d730")});
(有必要再重复一次, _id 可以是任何形式的唯一值。因为你很可能在实际中使用 ObjectId ,我们也在这里用它。)
当然,要找出 Leto 负责管理的所有工人,只需要执行:
代码语言:javascript复制db.employees.find({manager: ObjectId(
"4d85c7039ab0fd70a117d730")})
数组
示例:
代码语言:javascript复制// 插入一个 manager 是单个对象。
db.employees.insert({_id: ObjectId(
"4d85c7039ab0fd70a117d732"),
name: 'Moneo',
manager: ObjectId(
"4d85c7039ab0fd70a117d730")});
// 插入一个 manager 是 数组。
db.employees.insert({_id: ObjectId(
"4d85c7039ab0fd70a117d733"),
name: 'Siona',
manager: [ObjectId(
"4d85c7039ab0fd70a117d730"),
ObjectId(
"4d85c7039ab0fd70a117d732")] })
注意上面的文档,manager 字段的值既可以是单个对象,也可以是数组。而我们原来的 find 查询依旧可用:
代码语言:javascript复制db.employees.find({manager: ObjectId(
"4d85c7039ab0fd70a117d730")})
内嵌文档
MongoDB 还支持内嵌文档。来试试看向文档插入一个内嵌文档,像这样:
代码语言:javascript复制db.employees.insert({_id: ObjectId(
"4d85c7039ab0fd70a117d734"),
name: 'Ghanima',
family: {mother: 'Chani',
father: 'Paul',
brother: ObjectId(
"4d85c7039ab0fd70a117d730")}})
像你猜的那样,内嵌文档可以用 dot-notation 查询:
代码语言:javascript复制db.employees.find({
'family.mother': 'Chani'})
反规范化(Denormalization)
一些对冗余处理 的讨论。 并不是需要对你文档里的每条数据都做冗余处理。而是说,与其对冗余数据心存恐惧,让它影响你的设计决策,不如在建模的时候考虑什么信息应当属于什么文档。
假设你要写一个论坛应用。
- 传统的方式是通过 posts 中的 userid 列,来关联一个特定的 user 和一篇 post 。这样的建模,在显示 posts 的时候要查询 (链接到) users。
- 一个代替案是“增加冗余字段”,在每篇 post 中都冗余的多存储 name 和 userid 两个字段。这要用到内嵌文档,比如 user: {id: ObjectId('Something'), name: 'Leto'}。缺点是,如果用户可以更新他们的名字,那将不得不对所有的文档都进行更新。
其他选择
记住: 一个独立文档的大小当前被限制在 16MB
。
处理一对多(one-to-many)或者多对多(many-to-many)场景的时候,id 数组通常是一个正确的选择。
内嵌文档经常使用的情形:大多数情况下多是很小的数据块,面对总是被和父节点一起拉取的数据块。
集合的规模讨论:单个大而全?还是拆分小而专?
比如,常见的例子就是博客。你是应该分成一个 posts 集合和一个 comments 集合呢,还是应该每个 post 下面嵌入一个 comments 数组?
MongoDB 的处理方式:MongoDB 的灵活架构允许你把这两种方式结合起来,你可以把评论放在独立的集合中,同时在博客帖子下嵌入一小部分评论 (比如说最新评论) ,以便和帖子一同显示。
这遵守以下的规则:“ 你到底想在一次查询中获取到什么内容,那就怎么做。”
想一想,如果在关系型数据库中,要把上面说的这两种方式结合起来用,“要不要再建一个关联表呢?”
6. MongoDB 适用场景
单一解决方案还是多技术方案? 对于许多项目来说 - 或者说大多数 - 单一解决案是一个明智的选择。只有你自己才知道,引进新技术是否利大于弊。引入MongoDB 往往不会完全替换旧的方案(比如用Mongo替换MySQL),而是说“不用再依赖单一的解决案来处理你的数据”,作为数据存储的局部替代方案,是对你现有数据存储方案能力的局部增强。
比如说用 Lucene 作为关系型数据库的全文检索索引的加强,或者用 Redis 作为持久型 key-value 存储对缓存存储的增强,MongoDB 就是用来保存你的数据能力的处理增强。
写操作(Writes)
MongoDB 可以胜任的一个特殊角色是在日志领域。有两点使得 MongoDB 的写操作非常快。首先,你可以选择发送了写操作命令之后立刻返回,而无须等到操作完成。
如果想让你的数据 "过期" ,基于时间而不是整个集合的大小,你可以用 TTL 索引 ,所谓 TTL 是 "time-to-live" 的缩写。
持久性(Durability)
从 2.0 版的 MongoDB 开始,日志是默认启动的,该功能允许快速恢复服务器,比如遭遇到了服务器崩溃或者停电的情况。
事务(Transactions)
MongoDB 不支持事务。两个代替案:
- 第一个方案,就是各种原子更新操作。比如
set。还有像 findAndModify 命令,可以更新或删除文档之后,自动返回修改过的文档
- 第二个方案,当原子操作不能满足的时候,回到两段提交上来。
地理空间查询(Geospatial)
一个很强大的功能就是 MongoDB 支持 geospatial 索引。这允许你保存 geoJSON 或者 x 和 y 坐标到文档,并查询文档,用如
within 来获取一个矩形或圆中的点。
7. 聚合管道(Aggregation Pipeline)
聚合管道提供了一种方法用于转换整合文档到集合。你可以通过管道来传递文档,就像 Unix 的 "pipe" 一样,将一个命令的输出传递到另第二个,第三个,等等
8. 性能和工具
索引(Index)
创建索引用 ensureIndex :
代码语言:javascript复制// where "name" is the field name
db.unicorns.ensureIndex({name: 1});
删除索引用 dropIndex:
代码语言:javascript复制db.unicorns.dropIndex({name: 1});
可以创建唯一索引,这需要把第二个参数 unique 设置为 true:
代码语言:javascript复制db.unicorns.ensureIndex({name: 1},
{unique: true});
索引可以内嵌到字段中和任何数组字段。我们可以这样创建复合索引:
代码语言:javascript复制db.unicorns.ensureIndex({name: 1,
vampires: -1});
Explain
需要检查你的查询是否用到了索引,你可以通过 explain 方法:
代码语言:javascript复制db.unicorns.find().explain()
复制(Replication)
MongoDB 的复制在某些方面和关系型数据库的复制类似。所有的生产部署应该都是副本集,理想情况下,三个或者多个服务器都保持相同的数据。写操作被发送到单个服务器,也即主服务器,然后从它异步复制到所有的从服务器上。你可以控制是否允许从服务器上进行读操作,这可以让一些特定的查询从主服务器中分离出来,当然,存在读取到旧数据的风险。如果主服务器异常关闭,从服务中的一个将会自动晋升为新的主服务器继续工作。
分片(Sharding)
MongoDB 支持自动分片。分片是实现数据扩展的一种方法,依靠在跨服务器或者集群上进行数据分区来实现。一个最简单的实现是把所有的用户数据,按照名字首字母 A-M 放在服务器 1 ,然后剩下的放在服务器 2。
状态(Stats)
你可以通过 db.stats() 查询数据库的状态。
分析器(Profiler)
可以这样执行 MongoDB profiler :
代码语言:javascript复制db.setProfilingLevel(2);
备份和还原
来备份我们的 learn 数据库导 backup 文件夹,我们需要在控制台或者终端中执行执行
代码语言:javascript复制mongodump --db learn --out backup
如果只还原 unicorns 集合,我们可以这样做:
代码语言:javascript复制mongorestore --db learn --collection unicorns
backup/learn/unicorns.bson
文件导入导入数据 mongoexport 和 mongoimport 是另外两个可执行文件,用于导出和从 JSON/CSV 格式文件导入数据。比如说,我们可以像这样导出一个 JSON:
代码语言:javascript复制mongoexport --db learn --collection unicorns
CSV 格式是这样:
代码语言:javascript复制mongoexport --db learn
--collection unicorns
--csv --fields name,weight,vampires
9. 参考
https://github.com/ilivebox/the-little-mongodb-book/blob/master/zh-cn/mongodb.markdown