核心概念
字符串:所有字段串必须使用UTF-8编码
数字:bson指定了三种数据类型:double,int,long。
时间:从unix纪元计时开始时间值使用64b整数的毫秒值表示,负值表示之前的时间。javascript的月份是从0开始的,这意味着new Date(2020,5,11)表示2020年6月11日。
虚拟类型:如果要指定时区,可以存储时区的字段比如zone:'est',这就是虚拟类型。
可以使用db.collectionName.find({fieldName:{$type:1}})查看bson的类型。
要点
mongo的_id字段:每个mongo文档都需要一个_id,如果创建时没有_id,就会专门创建一个mongo ObjectID添加到文档里
即席查询(Ad Hoc)是用户根据自己的需求,灵活的选择查询条件,系统能够根据用户的选择生成相应的统计报表。即席查询与普通应用查询最大的不同是普通的应用查询是定制开发的,而即席查询是由用户自定义查询条件的(代码层面的关联查询)。
mongo和rdbms关系型数据库不同,他们的列名和行名是分离的,而mongo的key铭会存储在文档里,会影响数据大小,因此key的命名最好能够短小精悍。
文档限制:bson文档大小限制16mb,嵌套最大深度100.数据库设置的插入操作上限是16mb,最有效的大量插入在这个限制下可正常工作。
关于数据库选择与建模的知识点
1、应用访问的模式是什么?你需要分析需求,不仅仅是落实schema设计,还有选用什么数据库,理解应用的访问模式是目前为止schema设计最重要的方面。在设计之前必须问许多问题:读/写的比率是多少?查询是不是简单?查询一个key还是更复杂的key?是否需要聚合查询?数据量是多少?
2、数据的基本单位是什么?在rbdms里,有行和列的表。在键值数据库里,不同的键指向不同的值,在mongodb里,数据的基本单位是bson文档。
3:、数据库的功能是什么?rdbms功能 ad-hoc查询以及连接后写入sql。简单键值存储允许通过key获取数据。mongodb支持ad hoc查询,但不支持连接查询。数据库更新数据的方式也不同。rdbms允许使用sql进行复杂的更新,可以在事务里包含多个更新并支持原子性和回滚。mongodb不支持事务,但它支持另外一个原子更新操作,可以更新复杂结构的文档数据。键值数据库,你可以更新一个值,但每次更新都意味着完全替换一个值。
4、如何记录生成好的唯一ID或者主键?几乎所有的数据库系统设计中,schema都有记录的唯一key。选择key的策略会影响如何访问和存储数据。mongodb选择_id字段里存储的值作为主键,这个默认生成的策略不错,但如果你在多个机器上分片存储数据时,就需要重新定制规则了。
常用语法
切换数据库:use test
插入语法:db.user.insert({username:'zm'})
文档数量:db.user.count()
删除语法:db.foo.remove({test:'a'}) remove操作不会删除集合,类似于sql的delete
删除集合及附带的索引数据:db.foo.drop()
列举命令:db.help()
查找文档
查找语法:db.user.find({})
查找集合内所有:db.getCollection('user').find({})
简单查找:
db.user.find({username:'zm'})
AND查找:
db.user.find({$and:[{_id:ObjectId("5fcc905da1b26862b3589132")},{username:'zm'}]})
OR查找 :
db.user.find({$or:[{username:'other'},{username:'zm'}]})
查找所有喜欢电影《泰坦尼克》的用户:
db.user.find({'favorites.movies':'泰坦尼克'}).pretty()
范围查找:
首先创建一个集合
for(i =0 ;i<2000;i++){
db.numbers.insert({num:i})
}
大于查询:db.numbers.find({num:{'$gt':1000}})
小于查询:db.numbers.find({num:{'$lt':1000}})
设定上限限查询:db.numbers.find({num:{'$gt':1000,'$lt':1005}})
此外:$gte表示大于等于,$lte表示小于等于,$ne表示不等于
更新文档
更新至少需要两个参数,一个是指定要更新的文档,一个是定义如何修改文档。默认update()只更新一个文档。
更新一个文档:
db.user.update({username:'zm'},{$set:{country:'china'}})
替换更新:文档被替换为只包含country字段的文档,username字段被删除,特别注意。
db.user.update({username:'zm'},{country:'china'})
如果不需要某个字段,可以用$unset操作符删除:
db.user.update({username:'zm'},{$unset:{country:1}})
更新复杂数据:添加一个包含两个键的新对象,它包含你喜欢的城市和电影。
db.user.update({username:'zm'},{$set:{favorites:{cities:['china','chicago'],movies:['泰坦尼克','阿甘正传']}}})
高级更新:如果喜欢《泰坦尼克》的用户也喜欢《辛特勒的名单》,如果用$set需要重新编写并发送整个电影数组,最好使用$push或$addToSet,这两个命令都是往数组中添加数据,但是第二个是唯一的,可以避免重复添加数据。
db.user.update({'favorites.movies':'泰坦尼克'},{$addToSet:{'favorites.movies': '辛特勒的名单'}},false,true)
这个代码的含义,第一个参数是查询条件,匹配到喜欢《泰坦尼克》的用户,第二个参数使用$addToSet添加《辛特勒的名单》到列表中,第三个参数false,控制是否允许upsert,这个命令告诉更新操作,当一个文档不存在的时候是否插入它,这取决于更新操作是操作符更新还是替换更新。第三个参数是true,表示是否是多个更新,默认情况下,MongoDB更新只针对第一个匹配的文档,如果想更新所有匹配的文档,就必须显示指定这个参数。
索引和explain()
查看执行计划:
db.numbers.find({num:{'$gt':1500}}).explain('executionStats')
创建索引:
db.numbers.createIndex({num:1})
查看索引:db.numbers.stats()
数据库管理
所有数据库命令实现都有一个共同点,就是它们都在一个叫做$cmd的虚集合上实现查询。
数据库命令原理:
this.getCollection("$cmd").findOne(obj)
手动设计一个统计命令的方法如:
db.$cmd.findOne({collstats:'numbers'})
等价于
db.numbers.stats()
又如
db.numbers.save({num:123})实际只是对insert()和update()方法的包装,如果要保存的对象没有_id字段,就会添加字段,然后调用insert(),否则就执行更新操作。
查看系统中所有数据库列表信息(占用空间):show dbs
查看当前数据库的所有集合:show collections
低级别数据库和集合分析:
db.stats() db.numbers.stats()
等价于
db.runCommand({dbstats:1}) db.runCommand({collstats:'numbers'})
数据模型设计
电商商品文档例子
{
"_id" : 1,
"slug" : 1,
"desc" : "产品1",
"detail" : {
"weight" : 47,
"weight_units" : "lbs"
},
"pricing" : {
"retail" : 4200,
"sale" : 4000
},
"type_ids" : [
1,
2
],
"price_history" : [
{
"retail" : 5000,
"sale" : 4000
},
{
"retail" : 5200,
"sale" : 5000
}
]
}
slug:用户友好的永久链接通常叫做slug。用于将字符串 中的所有空格替换成连接符(-),并将所有字符转换为小写。 这样其实就生成了一个 slug ,可以很好的用于创建 URI
db.products.createIndex({slug:1},{unique:true})
如上例子,如果插入重复值会抛出异常,这时就可以尝试不同的值,这种这段通常唯一索引,以加速查询和确保唯一。
内嵌文档:如果key叫details,指向一个子文档,这个key与_id字段不同,他允许我们在现有文档里进行查询,details字段这种动态属性数据可以提供很好的扩展点。
我们也可以在同一个文档里存储商品当前和过去的价格。pricing键指向当前的零售价和批发价,price_history引用了一个完整的数组价格选项,存储历史的价格变更。
一对多关系:可以将需要关联的表的_id存储进文档里
多对多关系:mongodb不支持join连接,需要不同的多对多策略,可以定义关联文档的_id数组,数组里面多个_id作为指向其他文档的指针。
tips:mongodb 不允许我们查看数组对象的大小,但是我们可以定义一个字段缓存数组的大小。
盖子集合: 有上限的集合,不允许删除单个文档,也不能增加文档的大小,为日志而设计
db.createCollection("users.actions",{capped:true,size:10000,max:2})
如上例子:表示创建一个盖子集合,集合最大是10000kb,只能包含2个文档。
ttl集合: 生存时间集合这个功能是通过一种特殊索引实现的,创建方式如下:
db.a.createIndex({time_filed:1},{expireAfterSeconds:30})
命令会在 time_filed字段上创建索引,这个字段会定期检查时间戳,如果与当前时间间隔超过expireAfterSeconds,就会自动被删除,ttl集合有几个限制:1、不能在_id字段建立 2、不能在已经有索引的字段上建立 3、不能在盖子集合上建立 4、不能组合ttl索引,只有第一个时间有效。
忽略、限制和排序
db.products.find().skip(1).sort({'pricing.sale':1}).limit(2)
这些查找可以使用索引。
投影
投影可以返回限制字段,只需要在find中加入一个额外参数如下:
db.products.find({},{_id:1}).skip(1).sort({'pricing.sale':1}).limit(2)
模糊匹配
mongo支持正则匹配,前缀搜索可以使用索引,但不是所有正则表达式都能使用索引
范围查找
db.products.find({'pricing.sale':{$gt:1000,$lt:5000 }})
集合操作符
$in : 如果任意参数在集合里存在,则匹配。
db.products.find({'type_ids':{'$in':[1,3]}})
$all:如果所有参数在集合里存在,则匹配
db.products.find({'type_ids':{'$all':[1,3]}})
$nin:返回没有给定元素匹配的文档
db.products.find({'type_ids':{'$nin':[3]}})
注意:$in和$all可以利用索引,$ne和$nin不能使用索引,尽量找不同方式来表示这种查询。
布尔运算符
$ne 并不等同于运算符,在操作中由于不使用索引,一般要结合一个其他的运算符
db.getCollection('products').find({'from':"珠海","slug":{'$ne':1}})
$not 不匹配mongodb的运算符或者正则表达式查询结果,$not非常有用,因为他可以查询出不符合表达式条件的结果
db.getCollection('products').find({'slug':{'$not':{'$lt':2}}})
与下面的差别是$返回大于1的文档的同时也返回没有slug字段的文档,而$gt只返回大于1的文档
db.getCollection('products').find({'slug':{'$gt':1}})
$or 表示使用两个不同的关键字逻辑分离两个值,有个重点:如果可能值得范围是相同的关键字,就使用$in替代。$or采用的是数组选择器,每个选择器可以是任意复杂和可能自身包含其他查询运算符,$nor工作原理大致与$or相同,当且仅当没有其查询选择器为真时逻辑为真。
db.getCollection('products').find({'$or':[{'slug':1},{'desc':'产品3'}]})
$exists 检索文档中是否存在该字段 db.product.find({'details.color':{$exists:true}})
数组操作符:$elemMatch 如果提供的所有词语在相同的子文档中,则匹配
$size 如果子文档数组大小与提供的文本值相同,则匹配
注意,无论字段指向子文档还是子文档的数组,都可以使用相同的点符号db.product.find({'address.name':'home','address.state':'NY'})
正则表达式:以下代码用于查询出最好或者最坏的用户评论文本,i表示区分大小写。
db.reviews.find({'user_id':xxx,'text':/best|worst/i})
其他查询符
取模:$mod[(quotient),{result}] 如果元素除以除数符合结果则匹配 db.orders.find({subtotal:{$mod:[3,0]}})
类型:$type 如果元素类型符合指定的bson类型则匹配(符号类型在最新的bson规范中被弃用)
文本搜索: $text 允许建立文本所以得字段上执行文本搜索
查询选择
映射:选择子集的字段,使用映射可以最小化网络延迟和反序列化。
$slice
映射通常定义为返回字段合集db.users.find({},{'username',1})
返回前12条和返回后5条,可以使用$slice
db.product.find({},{'reviews':{$slice:12}}) db.product.find({},{'reviews':{$slice:-5}})
分页,以下为跳过24条并限制12条的代码,最后注意,使用$slice后不会阻止其他返回字段,要限制字段必须明确这样做。
db.products.find({},{'review':{$slice:[24,12]},{'reviews.rating',1}})
排序:
db.reviews.find({}).sort({'rating':-1})
聚合
聚合管道操作包含下面几个部分:
$product 指定输出文档里的字段,对应sql的select
$match 选择要处理的文档,与find()类似,对应sql的where,having
$limit 限制传递给下一步的文档数量
$skip 跳过一定数量的文档
$unwind 扩展数组,为每个数组入口生成一个输出文档,对应sql的join
$group 根据key来分组文档,对应sql的group
$sort 排序文档
$geoNear 选择某个地理位置附近的文档
$out 把管道的结过写入某个集合
$redact 控制特定数据的访问
exp1:统计所有商品的评价总数
db.reviews.aggregate([$group:{_id:'$product_id', -------根据product_id分组文档
count:{$sum:1}}]) -------计算每个商品的评价数量
exp2:选择唯一商品进行计算
product = db.product.findOne({'slug':'9292'})
db.reviews.aggregate([{$match:{product_id:product['_id']}}, --------只选择一个商品
{$group:{_id:'$product_id',count:{$sum:1}}}
]).next; -----------返回结果集的第一个文档
注意:$match要在$group前面。
exp3:计算评价的平均值
product = db.product.findOne({'slug':'9292'})
db.reviews.aggregate([{$match:{product_id:product['_id']}}, --------只选择一个商品
{$group:{_id:'$product_id',average:{$avg:'$rating'},count:{$sum:1}}} -------计算商品的平均分
]).next; -----------返回结果集的第一个文档
exp4:为每个评分统计评价数
db.reviews.aggregate([{$match:{product_id:product['_id']}}, --------只选择一个商品
{$group:{_id:'$rating'},count:{$sum:1}}} ------根据评分值分组并统计数量
]).toArray(); -----------把结果光标转换为数组
exp5:$unwind使用类别id数组来连接每个商品文档
db.product.aggregate([
{$project:{category_ids:1}}, -------只传递类别id给下一步,默认传递_id特性
{$unwind:'$gategory_ids'}, --------为每个category_ids中的入口项目创建一个文档
{$group:{_id:'$gategory_ids',count:{$sum:1}}},
{$out:'countByCategory'} -------$out把聚合结果写入到集合countByCategory中
])
exp6:根据年、月统计2020年的订单数据
db.orders.aggregate([
{$match:{data:{$gte:new Date(2020,0,1)}}},
{$group:{_id:{year:{$year:'data'},mouth:{$month:'data'}},
count:{$sum:1},
total:{$sum:'$sub_total'},
{$sort:{_id:-1}}
}}
])
$group函数:
$addToSet 为组里唯一的值创建一个数组
$first 组里的第一个值,只有前缀$sort才有意义
$last 组里的最后一个值,只有前缀$sort才有意义
$max 组里某个字段的最大值
$min 组里某个字段的最小值
$avg 某个字段的平均值
$push 返回组内所有值得数组,不去重
$sum 求组内所有值得和
$addToSet和$push的差别,$addToSet,某个给定的值不能在集合里出现两次,而$push创建的数组里可以多次出现。
重塑文档:
最简单的重塑就是一个字段进行重命名,生成一个新字段,也可以通过修改或者创建一个新文档来重塑文档
exp7:读取用户的名和性,创建一个同时包含first和last的name字段
db.users.aggregate([
{$match:{name:'ziming'}},
{$project:{name:{first:'$first_name',last:'$last_name'}}}
])
字符串函数:
$concat 连接2个或者多个字符串为一个字符串
$strcasecmp 大小写敏感比较,返回数字
$substr 获取字符串的子串
$toLower 转换为小写字符串
$toUpper 转换为大写字符串
exp8:下面例子使用了3个函数,$concat,$substr,$toUpper
db.users.aggregate([
{$match:{name:'ziming'}},
{$project:{name:{$concat:['$first_name',' ','$last_name']}, ----------姓名使用空格连接
firstInintal:{$substr:['$first_name',0,1]}, ----------名字第一个字符初始化
usernameUpperCase:{$toUpper:'username'} -----------修改username为大写字母
}}
])
算术运算函数:
$add 求和
$divide 除法
$mod 求余数
$multiply 乘积
$subtract 减法
日期函数:
$dayOfYear 一年365天中的每一天
$dayOfMonth 一个月中的一天
$dayOfWeek 一周中的某一天,1表示周日
$year 日期的年份
$month 日期的月份,1-12
$week 一年中的某一周 ,0-53
$hour 日期中的小时,0-23
$minute 日期中的分钟,0-59
$second 日期中的秒,0-59
$millisecond 日期中的毫秒,0-999
逻辑函数:
$and true 与操作,如果数组里的所有值都为true则返回true
$or true 与操作,如果数组里的有一个值为true则返回true
$cmp 如果两个数相等就返回0
$cond if ... then... else 条件逻辑
$eq $ne 两个值是否相等
$gt $gte $lt $gte 逻辑判断
$ifNull 把null值、表达式转换为特定的值
$not 取反操作
集合函数:
$setEquals 如果两个集合的元素完全相同,则为true
$setIntersection 返回两个集合的公共元素
$setDifference 返回差集
$setUnion 合并集合
$setIsSubset 如果第二个集合为第一个集合的子集,则为true
$anyElementTrue 如果某个集合元素为true,则为ture
$allElementTrue 如果所有的集合元素都为true,则为true
其他函数:
$meta 文本搜索
$size 数组大小,可用于判断数组是否包含某个元素或者是否为空
$map 对数组的每个成员应用表达式,允许我们处理数组,并通过数组的元素执行函数生成一个新的数组。
$let 定义表达式内使用的变量
$literal 返回表达式的值,而不评估它,允许我们避免初始化字段值为0、1、$的问题
聚合管道选项:
explain 运行管道并且只返回管道的详细处理信息,支持三种模式,queryPlanner,executionStats,allPlansExecution
allowDiskUse 使用磁盘存储数据,通常来说,使用该参数会影响性能,但如果遇到大数据量的情况,使用这个参数可以确保安全。ram内存限制100MB数据
cursor 指定初始批处理大小,聚合管道返回的光标支持如下调用:
hasNext 确定结果集是否包含下一个元素
next 返回下一个文档
toArray 以数组返回结果
forEach 遍历结果集的每一行
map 遍历结果集的每一行,返回一个结果数组
itcount 返回结果数量
pretty 格式化结果数组
格式如下:
db.product.aggregate([
{$project:{category_ids:1}},
{$unwind:'$gategory_ids'},
{$group:{_id:'$gategory_ids',count:{$sum:1}}},
{$out:'countByCategory'}
],{explain:true,allowDiskUse:true,cursor:{batchSize:1}})
更新、原子操作和删除
更新语法:db.users.update(selector,update,option)
语法提示:更新操作符使用前缀表示符,而查询操作使用中缀表示符
原子文档处理:db.users.findAndModify({query:{user_id:xxx},update:{$set:{state:"finish"}}})
多文档更新: db.product.update({},{$addToSet:{tags:'cheap'}},{multi:true})
upserts:有个问题非常常见,就是当文档存在更新时,文档不存在时插入数据,可以使用该参数,如果没有匹配的文档,就会插入新的文档,新文档的字段是查询选择器和目标更新文档的逻辑合并。db.users.update(selector,update,{upserts:true})
$inc:增加或减去一个值
$set和$unset:设置特点的key的值,可以使用$set。删除key,使用$unset,例子:db.col.update({_id:123},{$unset:{temp:1}})
注意:在数组的单个元素上使用$unset会设置值为null而不是删除元素,要删除数组元素可以使用$pull和$pop。
$rename:修改Key的名字可以使用如下命令:db.col.update({_id:123},{$rename:{'temp':'temperature'}})
$setOnInsert:在upsert的时候,有时候要注意,不能重写某些数据,这时若只想新增的数据就会非常有用,而不会修改数据。
实例: db.product.update({_id:123},{$inc:{quantiry:1},$setOnInsert:{state:'available'}},{upsert:true})
数组更新操作符:
$push、$pushAll、$each。若要在数组后面追加值,$push是最好的选择,如果要在一次更新里添加多个更新,则可以组合使用$each和$push,示例:db.product.update({id:123},{$push:{tags:{$each:['a','b','c']}}}),$pushAll可以添加多个值到数组上,但已过时。
$slice:它必须和$push、$each一起使用,允许用来裁剪数组的大小,删除旧的值
示例:db.temps.update({id:123},$push:{temps:{$each:[91,92],$slice:-4}})推送到数组后,从开头删除值,只留下后4个数
$sort:当使用$push和$slice时,有时候要排序后再删除它们,可以使用该操作符。
$addToSet也会往数组后面添加值,但是他只会添加不存在的值。
$pop:删除最后一个推进的项目
$bit:按位操作符,或(or)和与(and)都可以使用。
$pull和$pullAll:$pull是$pop的复杂形式,使用$pull可以通过值精确指定要删除的元素。
示例:db.product.update({id:123},{$pull:{tags:'a'}})
db.product.update({id:123},{$pullAll:{tags:['a','b']}})
位置操作符$:允许我们定位要更新的元素
findAndMotify命令
有许多参数可以控制此命令的功能:
query 查询选择器
update 指定更新的文档
remove 布尔值,如果为true,则返回删除的对象。默认是false。
new 布尔值,如果为true,则返回修改后的文档。默认为false,意味着返回最初的文档
sort 指定排序的方向
fileds 如果只要返回部分字段,可以指定他们
upsert 布尔值,为true时,findAndMotify为upsert操作,当文档不存在时创建他
删除命令
示例:db.reviews.remove({user_id:123},{$isolated:true})
$isolated操作符可以保持操作独立,不会让路,在分片集合中无法使用。
索引
唯一索引:唯一索引会保证在所有文档的唯一性。db.users.createIndex({username:1},{unique:true})
稀疏索引:索引默认是密集型的,但如果尝试插入两个null值会失败。这时可以创建一个稀疏索引。在稀疏索引中,只有包含某些索引键值的文档才会出现。创建稀疏索引要做到指定{sparse:true}。还有一种需要创建稀疏索引的情况,集合中大量文档不包含所有的键值时,创建稀疏索引可以使查询更加高效,因为它不会增加索引的大小,也不会在删除或添加文档时更新索引。
多键索引:当索引字段为数组时,可以通过多键索引实现,意味着针对这些数组任意值在索引上的查询都会定位到文档。即:多个索引入口或者键值,引用同一个文档。
哈希索引:当数据分布不均匀时,ha sh函数可以创造均匀性,它对于分片集合非常有用。限制:1不支持范围查询2:不支持多键hash3浮点数在哈希之前转换为整数,如4.2和4.3拥有相同的哈希索引。例子:db.product.createIndex({product_id:'hashed'})
单键索引:每个索引入口对应文档里的单个值,默认的索引在_id上。
复合索引:虽然从mongo2.6开始,可以在一个查询里使用多个索引,但最好还是只使用一个索引。创建复合索引时的顺序很重要,通常按照规定,查询需要词语的精确匹配,第二个指定组合索引键值的范围。
创建和删除索引:创建使用createIndex()删除使用dropIndex()检查索引规范使用getIndexSpec().
后台索引:如果在生产环境无法停止数据库访问时,可以指定在后台创建索引,虽然构建索引还要占用写锁,但此过程允许其他用户读写数据库。可在浏览最小的时期完成。创建后台索引需要在声明索引的时候指定{background:true}
离线索引:离线索引通常需要复制一个新的服务器节点,然后在此服务器上创建索引,并允许此服务器复制主服务器的数据。
碎片整理:db.values.reIndex()会为集合重新建立索引。重建索引会占用写锁,导致实例无法使用,最好脱机进行。
查询优化
使用mong o日志实现简单的查询工作:grep -E ‘[0-9]+ms’ mongod.log,100ms是很高的门槛,在启动mong o时,可以使用--slowms参数设置。
使用profilter分析器,默认情况下禁用了这个工具,开启命令如下:use stocks;db.setProfilinglevel(2).分析范围通常时某个数据库,分析级别设置为最详细级别2,它会分析每个读写操作,记录慢查询耗时超时要设置监控级别为1,禁用分析器设置为0.分析器只会记录操作超时的操作,传递毫秒作为第二个参数;db.setProfilingLevel(1,50)。监控结果保持在一个特殊的盖子集合system.profile里。
查看耗时超过150ms的操作;db.system.profile.find(mills:{$gt:150}),盖子集合维护了自然的插入顺序,可以使用db.system.profile.find().sort({$natural:-1}).limit(5).pretty()
使用explain()查看查询信息。分析cursor字段如果使用了索引该字段应该为btreeCursor,理想情况下nscanned应该与n尽量接近,第二个是scanandorder字段,该字段介绍查询慢的具体原因该字段会在查询优化器无法使用索引返回排序集合时出现true,出现true时需要在排序字段上创建索引db.values.createIndex({close:1})
查询优化器规则:1避免scanandorder2使用索引约束满足所有的字段3如果查询包含范围,则选择最后一个key使用索引来帮助处理范围和排序
参考文章 https://segmentfault.com/a/1190000010826809