目录
MongoDB相关概念
传统的关系型数据库(如MySQL),在数据操作的“三高”需求以及应对Web2.0的网站需求面前,显得力不从心。
解释:“三高”需求:
- High performance - 对数据库高并发读写的需求。
- Huge Storage - 对海量数据的高效率存储和访问的需求。
- High Scalability && High Availability- 对数据库的高可扩展性和高可用性的需求。
而MongoDB可应对“三高”需求,具体的应用场景如:
- 社交场景,使用 MongoDB 存储存储用户信息,以及用户发表的朋友圈信息,通过地理位置索引实现附近的人、地点等功能。
- 游戏场景,使用 MongoDB 存储游戏用户信息,用户的装备、积分等直接以内嵌文档的形式存储,方便查询、高效率存储和访问。
- 物流场景,使用 MongoDB 存储订单信息,订单状态在运送过程中会不断更新,以 MongoDB 内嵌数组的形式来存储,一次查询就能将
订单所有的变更读取出来。 - 物联网场景,使用 MongoDB 存储所有接入的智能设备信息,以及设备汇报的日志信息,并对这些信息进行多维度的分析。
- 视频直播,使用 MongoDB 存储用户信息、点赞互动信息等。
这些应用场景中,数据操作方面的共同特点是:
- 数据量大
- 写入操作频繁(读写都很频繁)
- 价值较低的数据,对事务性要求不高
对于这样的数据,更适合使用MongoDB来实现数据的存储。
什么时候选择MongoDB
- 应用不需要事务及复杂 join 支持
- 新应用,需求会变,数据模型无法确定,想快速迭代开发
- 应用需要2000-3000以上的读写QPS(更高也可以)
- 应用需要TB甚至 PB 级别数据存储
- 应用发展迅速,需要能快速水平扩展
- 应用要求存储的数据不丢失
- 应用需要99.999%高可用
- 应用需要大量的地理位置查询、文本查询
如果上述有1个符合,可以考虑 MongoDB,2个及以上的符合,选择 MongoDB 绝不会后悔。
MongoDB简介
- MongoDB是一个开源、高性能、无模式的文档型数据库,当初的设计就是用于简化开发和方便扩展,是NoSQL数据库产品中的一种。是最像关系型数据库(MySQL)的非关系型数据库。
- 它支持的数据结构非常松散,是一种类似于 JSON 的 格式叫BSON,所以它既可以存储比较复杂的数据类型,又相当的灵活。
- MongoDB中的记录是一个文档,它是一个由字段和值对(field:value)组成的数据结构。MongoDB文档类似于JSON对象,即一个文档认为就是一个对象。字段的数据类型是字符型,它的值除了使用基本的一些类型外,还可以包括其他文档、普通数组和文档数组。
体系结构
MySQL和MongoDB对比:
数据模型
-
MongoDB的最小存储单位就是文档(document)对象。文档(document)对象对应于关系型数据库的行。数据在MongoDB中以BSON(Binary-JSON)文档的格式存储在磁盘上。
-
BSON(Binary Serialized Document Format)是一种类json的一种二进制形式的存储格式,简称Binary JSON。BSON和JSON一样,支持内嵌的文档对象和数组对象,但是BSON有JSON没有的一些数据类型,如Date和BinData类型。
-
BSON采用了类似于 C 语言结构体的名称、对表示方法,支持内嵌的文档对象和数组对象,具有轻量性、可遍历性、高效性的三个特点,可以有效描述非结构化数据和结构化数据。这种格式的优点是灵活性高,但它的缺点是空间利用率不是很理想。
-
Bson中,除了基本的JSON类型:string,integer,boolean,double,null,array和object,mongo还使用了特殊的数据类型。这些类型包括date,object id,binary data,regular expression 和code。每一个驱动都以特定语言的方式实现了这些类型,查看你的驱动的文档来获取详细信息。
BSON数据类型参考列表:
shell默认使用64位浮点型数值。{“x”:3.14}或{“x”:3}。对于整型值,可以使用NumberInt(4字节符号整数)或NumberLong(8字节符号整数),{“x”:NumberInt(“3”)}{“x”:NumberLong(“3”)}
MongoDB的特点
MongoDB主要有如下特点:
-
高性能:
-
MongoDB提供高性能的数据持久性。特别是,对嵌入式数据模型的支持减少了数据库系统上的I/O活动。
-
索引支持更快的查询,并且可以包含来自嵌入式文档和数组的键。(文本索引解决搜索的需求、TTL索引解决历史数据自动过期的需求、地理位置索引可用于构建各种 O2O 应用)
-
mmapv1、wiredtiger、mongorocks(rocksdb)、in-memory 等多引擎支持满足各种场景需求。
-
Gridfs解决文件存储的需求。
-
-
高可用性:
- MongoDB的复制工具称为副本集(replica set),它可提供自动故障转移和数据冗余。
-
高扩展性:
- MongoDB提供了水平可扩展性作为其核心功能的一部分。
- 分片将数据分布在一组集群的机器上。(海量数据存储,服务能力水平扩展)
- 从3.4开始,MongoDB支持基于片键创建数据区域。在一个平衡的集群中,MongoDB将一个区域所覆盖的读写只定向到该区域内的那些片。
-
丰富的查询支持:
- MongoDB支持丰富的查询语言,支持读和写操作(CRUD),比如数据聚合、文本搜索和地理空间查询等。
-
其他特点:如无模式(动态模式)、灵活的文档模型。
Mongodb配置
基本配置:
# mongod.conf
# for documentation of all options, see:
# http://docs.mongodb.org/manual/reference/configuration-options/
# Where and how to store data.
storage:
dbPath: E:\mongodb\data
journal:
enabled: true
# engine:
# wiredTiger:
# where to write logging data.
systemLog:
destination: file
logAppend: true
path: E:\mongodb\log\mongod.log
# network interfaces
net:
port: 27017
bindIp: 127.0.0.1
启动MongoDB服务:
mongod -f /mongodb/single/mongod.conf
如果一旦是因为数据损坏,则需要进行如下操作(了解):
-
删除lock文件:
rm -f /mongodb/single/data/db/*.lock
-
修复数据:
/usr/local/mongdb/bin/mongod --repair --dbpath=/mongodb/single/data/db
基本常用命令
存放文章评论的数据存放到MongoDB中,数据结构参考如下,数据库:articledb
数据库操作
选择和创建数据库
选择和创建数据库的语法格式,如果数据库不存在则自动创建,例如,以下语句创建 spitdb 数据库:
use articledb
一般使用操作conllection的方式隐式创建数据库,这样的操作既会创建数据库又会创建collections
use articledb
db.articlecollection.insertOne({"id":1,"name":"article"})
查看有权限查看的所有的数据库命令
show dbs
或
show databases
在 MongoDB 中,集合只有在内容插入后才会创建! 就是说,创建集合(数据表)后要再插入一个文档(记录),集合才会真正创建。
查看当前正在使用的数据库命令
db
MongoDB 中默认的数据库为 test,如果你没有选择数据库,集合将存放在 test 数据库中。
数据库名可以是满足以下条件的任意UTF-8字符串:
- 不能是空字符串(“”)。
- 不得含有’ '(空格)、.、$、/、\和\0 (空字符)。
- 应全部小写。
- 最多64字节。
有一些数据库名是保留的,可以直接访问这些有特殊作用的数据库。
5. admin: 从权限的角度来看,这是"root"数据库。要是将一个用户添加到这个数据库,这个用户自动继承所有数据库的权限。一些特定的服务器端命令也只能从这个数据库运行,比如列出所有的数据库或者关闭服务器。
6. local: 这个数据永远不会被复制,可以用来存储限于本地单台服务器的任意集合
7. config: 当Mongo用于分片设置时,config数据库在内部使用,用于保存分片的相关信息。
数据库的删除(删除当前数据库)
db.dropDatabase()
集合操作
集合的命名规范:
- 集合名不能是空字符串""。
- 集合名不能含有\0字符(空字符),这个字符表示集合名的结尾。
- 集合名不能以"system."开头,这是为系统集合保留的前缀。
- 用户创建的集合名字不能含有保留字符。有些驱动程序的确支持在集合名里面包含,这是因为某些系统生成的集合中包含该字符。除非你要访问这种系统创建的集合,否则千万不要在名字里出现$。
查看当前库中的表:show tables命令
show collections
或
show tables
集合的隐式创建
当向一个集合中插入一个文档的时候,如果集合不存在,则会自动创建集合。
db.articlecollection.insertOne({"id":1,"name":"article"})
集合的删除
db.collection.drop()
或
db.集合.drop()
如果成功删除选定集合,则 drop() 方法返回 true,否则返回 false。
集合重命名
db.oldcollectionName.renameCollection(“newName”)
备份表
原理就是从当前表中查询出来,循环写入到另一份表中:
db.collectionName.find().forEach(function(x){db.collection.baktableName.insert(x)})
文档CRUD
文档(document)的数据结构和 JSON 基本一样,所有存储在集合中的数据都是 BSON 格式。
文档的插入
单个文档插入
使用insertOne() 或 insert() (不推荐)或save() (弃用)方法向集合中插入文档,语法如下:
db.collection.insertOne(
<document or array of documents>,
{
writeConcern: <document>,
ordered: <boolean>
}
)
参数:
db.comment.insert({"articleid":"100000","content":"今天天气真好,阳光明
媚","userid":"1001","nickname":"Rose","createdatetime":new Date(),"likenum":NumberInt(10),"state":null})
提示:
1)comment集合如果不存在,则会隐式创建
2)mongo中的数字,默认情况下是double类型,如果要存整型,必须使用函数NumberInt(整型数字),否则取出来就有问题了。
3)插入当前日期使用 new Date()
4)插入的数据没有指定 _id ,会自动生成主键值
5)如果某字段没值,可以赋值为null,或不写该字段。
注意:
- 文档中的键/值对是有序的。
- 文档中的值不仅可以是在双引号里面的字符串,还可以是其他几种数据类型(甚至可以是整个嵌入的文档)。
- MongoDB区分类型和大小写。
- MongoDB的文档不能有重复的键。
- 文档的键是字符串。除了少数例外情况,键可以使用任意UTF-8字符。
文档键命名规范:
- 键不能含有\0 (空字符)。这个字符用来表示键的结尾。
- .和$有特别的意义,只有在特定环境下才能使用。
- 以下划线"_"开头的键是保留的(不是严格要求的)。
批量插入
语法:
db.collection.insertMany(
[ <document 1> , <document 2>, ... ],
{
writeConcern: <document>,
ordered: <boolean>
}
)
批量插入多条文章评论:
db.comment.insertMany([
{"_id":"1","articleid":"100001","content":"我们不应该把清晨浪费在手机上,健康很重要,一杯温水幸福你我
他。","userid":"1002","nickname":"相忘于江湖","createdatetime":new Date("2019-08-
05T22:08:15.522Z"),"likenum":NumberInt(1000),"state":"1"},
{"_id":"2","articleid":"100001","content":"我夏天空腹喝凉开水,冬天喝温开水","userid":"1005","nickname":"伊人憔
悴","createdatetime":new Date("2019-08-05T23:58:51.485Z"),"likenum":NumberInt(888),"state":"1"},
{"_id":"3","articleid":"100001","content":"我一直喝凉开水,冬天夏天都喝。","userid":"1004","nickname":"杰克船
长","createdatetime":new Date("2019-08-06T01:05:06.321Z"),"likenum":NumberInt(666),"state":"1"},
{"_id":"4","articleid":"100001","content":"专家说不能空腹吃饭,影响健康。","userid":"1003","nickname":"凯
撒","createdatetime":new Date("2019-08-06T08:18:35.288Z"),"likenum":NumberInt(2000),"state":"1"},
{"_id":"5","articleid":"100001","content":"研究表明,刚烧开的水千万不能喝,因为烫
嘴。","userid":"1003","nickname":"凯撒","createdatetime":new Date("2019-08-
06T11:01:02.521Z"),"likenum":NumberInt(3000),"state":"1"}
]);
插入时指定了 _id ,则主键就是该值,如果某条数据插入失败,将会终止插入,但已经插入成功的数据不会回滚掉。
文档的基本查询
查询数据的语法格式如下:
db.collection.find(<query>, [projection])
参数:
查询所有:
db.comment.find()
或
db.comment.find({})
简单条件来查询:
db.comment.find({userid:'1003'})
如果你只需要返回符合条件的第一条数据,可以使用findOne命令来实现,语法和find一样:
db.comment.findOne({userid:'1003'})
投影查询(Projection Query):
# 默认 _id 会显示,可以设置_id为0来取消
db.comment.find({userid:"1003"},{userid:1,nickname:1,_id:0})
{ "userid" : "1003", "nickname" : "凯撒" }
{ "userid" : "1003", "nickname" : "凯撒" }
嵌套查询:
{
"_id":ObjectId("11223344"),
"account":{
"username":"123",
"password":{
"truth":"123"
"hash":"1hdhh22"
}
}
}
db.userinfo.find({"account.password.truth":"123"})
文档的更新
更新文档的语法:
db.collection.update(query, update, options)
//或
db.collection.update(
<query>,
<update>,
{
upsert: <boolean>,
multi: <boolean>,
writeConcern: <document>,
collation: <document>,
arrayFilters: [ <filterdocument1>, ... ],
hint: <document|string> // Available starting in MongoDB 4.2
}
)
// 或
updateOne / updateMany
参数:
-
覆盖的修改:
如果想修改_id为1的记录,点赞量为1001,输入以下语句:
db.comment.update({_id:"1"},{likenum:NumberInt(1001)})
执行后会发现,这条文档除了likenum字段其它字段都不见了
-
局部修改:
为了解决这个问题,需要使用修改器$set
来实现,命令如下:db.comment.update({_id:"2"},{$set:{likenum:NumberInt(889)}})
-
批量的修改
更新所有用户为 1003 的用户的昵称为 凯撒大帝 。
//默认只修改第一条数据
db.comment.update({userid:"1003"},{$set:{nickname:"凯撒2"}})
//修改所有符合条件的数据
db.comment.update({userid:"1003"},{$set:{nickname:"凯撒大帝"}},{multi:true})
提示:如果不加后面的参数,则只更新符合条件的第一条记录
- 列值增长的修改
如果我们想实现对某列值在原有值的基础上进行增加或减少,可以使用 $inc 运算符来实现。
db.comment.update({_id:"3"},{$inc:{likenum:NumberInt(1)}})
删除文档
删除文档的语法结构:
db.集合名称.remove(条件)
# 全部删除
db.comment.remove({})
高级查询
统计查询
db.comment.count({userid:"1003"})
# 或
db.comment.find({userid:"1003"}).count()
默认情况下 count() 方法返回符合条件的全部记录条数。
高版本的mongoApi废弃了count()方法,如果需要统计数量可以使用:
python的collection.estimated_document_count(),collection.count_documents(filter)
go的collection.CountDocuments(ctx,filter),collection.EstimatedDocumentCount(ctx)
collection.estimated_document_count()
和collection.count_documents(filter)
都是MongoDB的方法,用于获取集合中符合条件的文档数量。然而,它们在计算文档数量时存在一些区别。
-
collection.estimated_document_count()
: 这个方法返回的是集合中所有文档的估计数量。它是一个快速的近似计数方法,不会真正遍历集合中的每个文档。这个方法通常用于获取一个大致的文档数量,而不需要非常精确的结果。它比较适用于大型集合,因为它不需要扫描所有文档来计算数量,而是使用一些统计信息或近似算法进行估算。 -
collection.count_documents(filter)
: 这个方法返回符合指定条件的文档数量。你需要传递一个查询条件(filter)给这个方法,它会扫描集合中的每个文档,并统计满足条件的文档数量。这个方法提供了精确的文档数量,因为它会确切地计算满足条件的文档数目。但是,它可能会比较慢,特别是对于大型集合或复杂的查询条件。
所以,collection.estimated_document_count()
提供了一个快速的估计值,而collection.count_documents(filter)
提供了精确的满足条件的文档数量。你可以根据你的需求选择使用哪个方法。如果你需要一个快速的近似值,可以使用estimated_document_count()
;如果你需要准确的数量,可以使用count_documents(filter)
。
分页列表查询
可以使用limit()方法来读取指定数量的数据,使用skip()方法来跳过指定数量的数据。
>db.COLLECTION_NAME.find().limit(NUMBER).skip(NUMBER)
如果你想返回指定条数的记录,可以在find方法后调用limit来返回结果(TopN),默认值20,例如:
db.comment.find().limit(3)
skip方法同样接受一个数字参数作为跳过的记录条数。(前N个不要),默认值是0
db.comment.find().skip(3)
分页查询:需求:每页2个,第二页开始:跳过前两条数据,接着值显示3和4条数据
//第一页
db.comment.find().skip(0).limit(2)
//第二页
db.comment.find().skip(2).limit(2)
//第三页
db.comment.find().skip(4).limit(2)
排序查询
sort() 方法对数据进行排序,sort() 方法可以通过参数指定排序的字段,并使用 1 和 -1 来指定排序的方式,其中 1 为升序排列,而 -1 是用于降序排列。
skip(), limilt(), sort()三个放在一起执行的时候,执行的顺序是先 sort(), 然后是 skip(),最后是显示的 limit(),和命令编写顺序无关。
db.comment.find().sort({userid:-1,likenum:1})
由于mongodb会先执行find().sort(),因此会将查询到的所有文档(document)放到内存中排序,因此有可能出现mongodb超出内存占用的情况。
解决方案:
- 增大mongodb允许占用的内存大小;
- 使用
cursor.allowDiskUse()
,官方文档:https://www.mongodb.com/docs/manual/reference/method/cursor.allowDiskUse/
正则的复杂条件查询
MongoDB的模糊查询是通过正则表达式的方式实现的。格式为:
db.collection.find({field:/正则表达式/})
或
db.集合.find({字段:/正则表达式/}
# 如果要查询评论的内容中以“专家”开头的,代码如下:
db.comment.find({content:/^专家/})
正则还有一种表达方式,与//
是具有同样的功能
$regex:正则 db.products.find( { sku: { $regex: /^ABC/i } }
二者区别在于,当需要$options时:
使用//表示正则表达式时,可以在正斜杠后面添加一个或多个选项字符来设置匹配选项。常见的选项字符包括:
i:表示不区分大小写。
m:表示多行模式,使^和$匹配每一行的开头和结尾。
s:表示单行模式,使.可以匹配包括换行符在内的任意字符。
x:表示忽略正则表达式中的空白字符和注释。
db.collection.find({ field: /pattern/i })
db.collection.find({ field: { $regex: "pattern", $options: "i" } })
比较查询
db.集合名称.find({ "field" : { $gt: value }}) // 大于: field > value
db.集合名称.find({ "field" : { $lt: value }}) // 小于: field < value
db.集合名称.find({ "field" : { $gte: value }}) // 大于等于: field >= value
db.集合名称.find({ "field" : { $lte: value }}) // 小于等于: field <= value
db.集合名称.find({ "field" : { $ne: value }}) // 不等于: field != value
# 查询age=20的文档:
db.person.find( { age: { $eq: 20 } } )相当于:db.person.find( { age: 20 } )
包含查询
包含使用$in操作符。 示例:查询评论的集合中userid字段包含1003或1004的文档
db.comment.find({userid:{$in:["1003","1004"]}})
不包含使用$nin操作符。 示例:查询评论集合中userid字段不包含1003和1004的文档
db.comment.find({userid:{$nin:["1003","1004"]}})
条件连接查询
如果需要查询同时满足两个以上条件,需要使用$and操作符将条件进行关联。(相 当于SQL的and) 格式为:
$and:[ { },{ },{ } ]
示例:查询评论集合中likenum大于等于700 并且小于2000的文档:
db.comment.find({$and:[{likenum:{$gte:NumberInt(700)}},{likenum:{$lt:NumberInt(2000)}}]})
如果两个以上条件之间是或者的关系,我们使用or操作符进行关联,与前面 and的使用方式相同格式为:
$or:[ { },{ },{ } ]
示例:查询评论集合中userid为1003,或者点赞数小于1000的文档记录
db.comment.find({$or:[ {userid:"1003"} ,{likenum:{$lt:1000} }]})
$nor:一个条件都不满足,查询age既不等于20,sex也不是男的文档:
db.person.find( {$nor: [ { age: 20 },{ sex: "男"} ] } )
去重
distinct是一个用于获取唯一值列表的聚合操作符。它用于查找集合中某个字段的所有不重复的值,并返回一个包含这些唯一值的数组。
# 语法
db.collection.distinct(field, query)
db.collectionName.distinct(‘key’)
假设我们有一个名为"users"的集合,包含了一些用户文档,每个文档都有一个"country"字段表示用户所在的国家。我们可以使用distinct操作来获取所有不重复的国家列表:
db.users.distinct("country")
这将返回一个数组,包含集合中所有不重复的"country"值。
格式化输出
如果需要格式化查询的结果,可以使用pretty()的方法:
聚合查询
MongoDB 中聚合(aggregate)主要用于处理数据(诸如统计平均值,求和等),并返回计算后的数据结果。
有点类似 SQL 语句中的 count(*)。
- 管道在Unix和Linux中一般用于将当前命令的输出结果作为下一个命令的参数。
- MongoDB的聚合管道将MongoDB文档在一个管道处理完毕后将结果传递给下一个管道处理。
- 管道操作是可以重复的。
聚合管道操作,可参考菜鸟文档:菜鸟文档:
命令 | 功能描述 |
$project | 指定输出文档里的字段. |
$match | 选择要处理的文档,与find()类似,和find区别在于$match 操作可以把结果交给下一个管道处理 |
$limit | 限制传递给下一步的文档数量。 |
$skip | 跳过一定数量的文档。 |
$unwind | 扩展数组,为每个数组入口生成一个输出文档。(把列表拆开) |
$group | 根据key来分组文档。 |
$sort | 排序文档。 |
$geoNear | 选择某个地理位置附近的的文档。 |
$out | 把管道的结果写入某个集合。 |
$redact | 控制特定数据的访问。 |
$lookup | 多表关联(3.2版本新增) |
管道聚合操作
表达式 | 描述 | 实例 |
---|---|---|
$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"}}}]) |
集合中的数据如下:
{
_id: ObjectId(7df78ad8902c)
title: 'MongoDB Overview',
description: 'MongoDB is no sql database',
by_user: 'runoob.com',
url: 'http://www.runoob.com',
tags: ['mongodb', 'database', 'NoSQL'],
likes: 100
},
{
_id: ObjectId(7df78ad8902d)
title: 'NoSQL Overview',
description: 'No sql database is very fast',
by_user: 'runoob.com',
url: 'http://www.runoob.com',
tags: ['mongodb', 'database', 'NoSQL'],
likes: 10
},
{
_id: ObjectId(7df78ad8902e)
title: 'Neo4j Overview',
description: 'Neo4j is no sql database',
by_user: 'Neo4j',
url: 'http://www.neo4j.com',
tags: ['neo4j', 'database', 'NoSQL'],
likes: 750
},
现在我们通过以上集合计算每个作者所写的文章数,使用aggregate()计算结果如下:
> db.mycol.aggregate([{$group : {_id : "$by_user", num_tutorial : {$sum : 1}}}])
# _id 表示分组的依据,按照哪个字段进行分组,需要使用$gender表示选择这个字段进行分组
# $sum:1 表示把每条数据作为1进行统计,统计的是该分组下面数据的条数
{
"result" : [
{
"_id" : "runoob.com",
"num_tutorial" : 2
},
{
"_id" : "Neo4j",
"num_tutorial" : 1
}
],
"ok" : 1
}
>
以上实例类似sql语句:
select by_user, count(*) from mycol group by by_user
group by null:
db.stu.aggregate(
{$group:
{
_id:null,
counter:{$sum:1}
}
}
)
# _id:null 表示不指定分组的字段,即统计整个文档,此时获取的counter表示整个文档的个数
数据透视
正常情况在统计的不同性别的数据的时候,需要知道所有的name,需要逐条观察,如果通过某种方式把所有的name放到一起,那么此时就可以理解为数据透视。
-
统计不同性别的学生
db.stu.aggregate( {$group: { _id:null, name:{$push:"$name"} } } ) >>> { "_id" : null, "name" : [ "Alex", "Alex", "Alex", "Slary", "Tom" ] }
-
使用
$$ROOT
可以将整个文档放入数组中db.stu.aggregate( {$group: { _id:null, name:{$push:"$$ROOT"} } } )
-
统计:
db.tb5.aggregate([{"$group":{ "_id":"$name", "count":{"$sum":1},//设置分组计数,查看分组数 "avg_score":{"$avg":"$score"},//设置平均值,从score中找出平均值 "sum_score":{"$sum":"$score"},//设置求和值,从score中找出求和值 "max_score":{"$max":"$score"},//设置最大值,从score中找出最大值 "min_score":{"$min":"$score"}//设置最小值,从score中找出最小值 }}])
管道查询与find的区别
对于投影查询(projection),有两种实现方式:
db.goods.find({},{"_id":false})
db.article.aggregate(
{ $project : {
title : 1 ,
author : 1 ,
}}
)
它们的区别:
-
使用方式:
db.goods.find({}, {"_id": false})
是直接在集合上进行查询,而db.article.aggregate({ $project: { title: 1, author: 1 }})
则使用了聚合管道的方式进行查询。 -
弹性和复杂性:使用聚合框架(aggregate)可以提供更大的灵活性和复杂性。聚合框架允许你使用不同的阶段操作(例如:
$match、$group、$sort
等),并且可以在一个查询中组合多个操作。这样你可以进行更复杂的数据转换、数据计算和数据处理操作。相比之下,find()
查询只提供简单的查询和投影功能。 -
性能:通常情况下,
find()
查询比聚合操作更高效。因为聚合操作需要在多个阶段进行数据处理,可能会引入一定的性能开销。对于简单的投影操作,使用find()
查询更为直接和高效。
综上所述,如果你只需要简单的字段投影,而不需要进行其他复杂的数据处理操作,那么使用 db.goods.find({}, {"_id": false})
可以更加简洁和高效。但如果你需要进行更复杂的数据转换、计算或者多个阶段的操作,那么使用聚合框架 db.article.aggregate({ $project: { title: 1, author: 1 }})
将更加灵活和适用。
再来看几个实例:
# $match用于获取分数大于70小于或等于90记录,然后将符合条件的记录送到下一阶段$group管道操作符进行处理。
db.articles.aggregate( [
{ $match : { score : { $gt : 70, $lte : 90 } } },
{ $group: { _id: null, count: { $sum: 1 } } }
] );
统计男女生人数,按照人数升序,返回第二条数据:
db.stu.aggregate(
{$group:{_id:"$gender",counter:{$sum:1}}},
{$sort:{counter:-1}},
{$skip:1},
{$limit:1}
)
所以,视需求而定,如果你的需求不复杂可以不使用聚合管道,直接使用简单的$
符号和链式操作即可。
$group语法
在聚合管道中,$group
阶段用于对文档进行分组操作,并可以进行各种聚合计算。下面是 $group
阶段的基本语法:
{
$group: {
_id: <expression>, // 指定分组的依据,可以是字段名称或表达式
field1: { <accumulator1> : <expression1> }, // 第一个聚合计算
field2: { <accumulator2> : <expression2> }, // 第二个聚合计算
...
}
}
解释一下上述语法:
_id
字段用于指定分组的依据。可以是字段名称或表达式,决定了如何将文档分组。如果_id
设为null
,则将所有文档视为一组。field1
,field2
, … 是你想在分组结果中包含的字段名,可以是现有的字段或者新创建的计算字段。<accumulator1>
,<accumulator2>
, … 是聚合操作符,用于指定你想要进行的聚合计算,如求和 ($sum
)、计数 ($count
)、平均值 ($avg
) 等。<expression1>
,<expression2>
, … 是表达式,用于指定进行聚合计算的字段或计算公式。
下面是一个示例,演示如何使用 $group
阶段进行分组和聚合计算:
db.collection.aggregate([
{
$group: {
_id: "$category", // 按照 "category" 字段进行分组
totalAmount: { $sum: "$amount" }, // 计算每个分组的 "amount" 字段总和
averageQuantity: { $avg: "$quantity" } // 计算每个分组的 "quantity" 字段平均值
}
}
])
在上述示例中,文档会按照 “category” 字段进行分组。然后,对于每个分组,会计算该分组中所有文档的 “amount” 字段总和,并将结果放入名为 “totalAmount” 的字段中。同时,还会计算该分组中所有文档的 “quantity” 字段的平均值,并将结果放入名为 “averageQuantity” 的字段中。
再来一个例子:对于如下数据:统计出每个country/province下的userid的数量(同一个userid只统计一次),结果中的字段为{country:““,province:””,counter:“*”}
{ "country" : "china", "province" : "sh", "userid" : "a" }
{ "country" : "china", "province" : "sh", "userid" : "b" }
{ "country" : "china", "province" : "sh", "userid" : "a" }
{ "country" : "china", "province" : "sh", "userid" : "c" }
{ "country" : "china", "province" : "bj", "userid" : "da" }
{ "country" : "china", "province" : "bj", "userid" : "fa" }
db.tv3.aggregate(
{$group:{_id:{country:'$country',province:'$province',userid:'$userid'}}},
{$group:{_id:{country:'$_id.country',province:'$_id.province'},count:{$sum:1}}},
{$project:{_id:0,country:'$_id.country',province:'$_id.province',counter:'$count'}}
)
跨表查询$lookup
相当关系型数据库中多表关联查询
属性 | 作用 |
---|---|
from | 同一个数据库下等待被Join的集合。 |
localField | 源集合中的match值,如果输入的集合中,某文档没有 localField这个Key(Field),在处理的过程中,会默认为此文档含有 localField:null的键值对。 |
foreignField | 待Join的集合的match值,如果待Join的集合中,文档没有foreignField值,在处理的过程中,会默认为此文档含有 foreignField:null的键值对。 |
as | 为输出文档的新增值命名。如果输入的集合中已存在该值,则会覆盖掉 |
// 管道是一个切片,里面可以放很多操作
db.collection.aggregate([{
$lookup: {
from: "外键表",
localField: "本表外键字段",
foreignField: "外键表字段",
as: "起别名"
}
})
一对一
有如下标签表和班级表:
db.users.insertMany([{"name":"Alex","class_id":1,"school":1},{"name":"Alex2","class_id":1,"school":2},{"name":"Alex3","class_id":2,"school":1},{"name":"Alex4","class_id":3,"school":2},{"name":"Alex5","class_id":1,"school":1}])
db.class.insertMany([{"name":"1班","id":1},{"name":"2班","id":2},{"name":"3班","id":3}])
db.school.insertMany([{"name":"1院","id":1},{"name":"2院","id":2}])
连表查询:
func main() {
usercol := mongoClient.Database("Purchase").Collection("users")
cursor, _ := usercol.Aggregate(context.Background(), []bson.M{
{
"$lookup": bson.M{
"from": "class",
"localField": "class_id",
"foreignField": "id",
"as": "class",
},
},
{
"$lookup": bson.M{
"from": "school",
"localField": "school",
"foreignField": "id",
"as": "school",
},
},
{
"$project": bson.M{
"_id": false,
"class._id": false,
"school._id": false,
},
},
})
result := make([]map[string]any, 0)
err := cursor.All(context.Background(), &result)
if err != nil {
logger.Error(err)
return
}
for _, val := range result {
fmt.Println(val)
}
}
map[class:[map[id:1 name:1班]] class_id:1 name:Alex school:[map[id:1 name:1院]]]
map[class:[map[id:1 name:1班]] class_id:1 name:Alex2 school:[map[id:2 name:2院]]]
map[class:[map[id:2 name:2班]] class_id:2 name:Alex3 school:[map[id:1 name:1院]]]
map[class:[map[id:3 name:3班]] class_id:3 name:Alex4 school:[map[id:2 name:2院]]]
map[class:[map[id:1 name:1班]] class_id:1 name:Alex5 school:[map[id:1 name:1院]]]
一对多
给users增加tag属性,并且增加tag表:
db.users.insertMany([
{"name":"Alex","class_id":1,"school_id":1,"tag_ids":[1,2,3]},
{"name":"Alex2","class_id":1,"school_id":2,"tag_ids":[1,2]},
{"name":"Alex3","class_id":2,"school_id":1,"tag_ids":[1]},
{"name":"Alex4","class_id":3,"school_id":2,"tag_ids":[2]},
{"name":"Alex5","class_id":1,"school_id":1,"tag_ids":[4]}
])
db.tags.insertMany([
{"name":"tag1","id":1},
{"name":"tag2","id":2},
{"name":"tag3","id":3},
{"name":"tag4","id":4},
])
查询:
func main() {
usercol := mongoClient.Database("Purchase").Collection("users")
cursor, _ := usercol.Aggregate(context.Background(), []bson.M{
{
"$lookup": bson.M{
"from": "class",
"localField": "class_id",
"foreignField": "id",
"as": "class",
},
},
{
"$lookup": bson.M{
"from": "school",
"localField": "school_id",
"foreignField": "id",
"as": "school",
},
},
{
"$lookup": bson.M{
"from": "tags",
"localField": "tag_ids",
"foreignField": "id",
"as": "tags",
},
},
{
"$project": bson.M{
"_id": false,
"class._id": false,
"school._id": false,
"tags._id": false,
"tags.id": false,
"school.id": false,
},
},
})
result := make([]map[string]any, 0)
err := cursor.All(context.Background(), &result)
if err != nil {
logger.Error(err)
return
}
for _, val := range result {
fmt.Println(val)
}
}
map[class:[map[id:1 name:1班]] class_id:1 name:Alex school:[map[name:1院]] school_id:1 tag_ids:[1 2 3] tags:[map[name:tag1] map[name:tag2] map[name:tag3]]]
map[class:[map[id:1 name:1班]] class_id:1 name:Alex2 school:[map[name:2院]] school_id:2 tag_ids:[1 2] tags:[map[name:tag1] map[name:tag2]]]
map[class:[map[id:2 name:2班]] class_id:2 name:Alex3 school:[map[name:1院]] school_id:1 tag_ids:[1] tags:[map[name:tag1]]]
map[class:[map[id:3 name:3班]] class_id:3 name:Alex4 school:[map[name:2院]] school_id:2 tag_ids:[2] tags:[map[name:tag2]]]
map[class:[map[id:1 name:1班]] class_id:1 name:Alex5 school:[map[name:1院]] school_id:1 tag_ids:[4] tags:[map[name:tag4]]]
$unwind拆分数组
要求将tags拆分:
cursor, _ := usercol.Aggregate(context.Background(), []bson.M{
{
"$lookup": bson.M{
"from": "class",
"localField": "class_id",
"foreignField": "id",
"as": "class",
},
},
{
"$lookup": bson.M{
"from": "school",
"localField": "school_id",
"foreignField": "id",
"as": "school",
},
},
{
"$lookup": bson.M{
"from": "tags",
"localField": "tag_ids",
"foreignField": "id",
"as": "tags",
},
},
{
"$project": bson.M{
"_id": false,
"class._id": false,
"school._id": false,
"tags._id": false,
"tags.id": false,
"school.id": false,
},
},
{
"$unwind": bson.M{
"path": "$tags",
"preserveNullAndEmptyArrays": true, // 空的数组也拆分
},
},
})
map[class:[map[id:1 name:1班]] class_id:1 name:Alex school:[map[name:1院]] school_id:1 tag_ids:[1 2 3] tags:map[name:tag1]]
map[class:[map[id:1 name:1班]] class_id:1 name:Alex school:[map[name:1院]] school_id:1 tag_ids:[1 2 3] tags:map[name:tag2]]
map[class:[map[id:1 name:1班]] class_id:1 name:Alex school:[map[name:1院]] school_id:1 tag_ids:[1 2 3] tags:map[name:tag3]]
map[class:[map[id:1 name:1班]] class_id:1 name:Alex2 school:[map[name:2院]] school_id:2 tag_ids:[1 2] tags:map[name:tag1]]
map[class:[map[id:1 name:1班]] class_id:1 name:Alex2 school:[map[name:2院]] school_id:2 tag_ids:[1 2] tags:map[name:tag2]]
map[class:[map[id:2 name:2班]] class_id:2 name:Alex3 school:[map[name:1院]] school_id:1 tag_ids:[1] tags:map[name:tag1]]
map[class:[map[id:3 name:3班]] class_id:3 name:Alex4 school:[map[name:2院]] school_id:2 tag_ids:[2] tags:map[name:tag2]]
map[class:[map[id:1 name:1班]] class_id:1 name:Alex5 school:[map[name:1院]] school_id:1 tag_ids:[4] tags:map[name:tag4]]
$unwind
是 MongoDB 聚合管道中的一个操作符,它用于将包含数组的文档拆分成多个文档,以便进行进一步的操作。它将数组字段展开为多个文档,每个文档包含数组中的一个元素。
$unwind
操作符的语法如下:
{ $unwind: <arrayField> }
其中,<arrayField>
是需要展开的数组字段名。
以下是一个示例,演示如何使用 $unwind
操作符:
假设你有一个 orders
集合,其中的文档结构如下:
{
_id: 1,
items: ["apple", "banana", "orange"]
}
要展开 items
数组字段,可以使用 $unwind
操作符:
db.orders.aggregate([
{ $unwind: "$items" }
])
运行上述聚合管道后,结果将会是:
{ "_id": 1, "items": "apple" }
{ "_id": 1, "items": "banana" }
{ "_id": 1, "items": "orange" }
注意,$unwind
操作会为每个数组元素生成一个文档,因此结果中会有多个文档。
$group再次分组
cursor, _ := usercol.Aggregate(context.Background(), []bson.M{
{
"$lookup": bson.M{
"from": "class",
"localField": "class_id",
"foreignField": "id",
"as": "class",
},
},
{
"$lookup": bson.M{
"from": "school",
"localField": "school_id",
"foreignField": "id",
"as": "school",
},
},
{
"$lookup": bson.M{
"from": "tags",
"localField": "tag_ids",
"foreignField": "id",
"as": "tags",
},
},
{
"$project": bson.M{
"_id": false,
"class._id": false,
"school._id": false,
"tags._id": false,
"tags.id": false,
"school.id": false,
},
},
{
"$unwind": bson.M{
"path": "$tags",
"preserveNullAndEmptyArrays": true, // 空的数组也拆分
},
},
{
"$group": bson.M{ // 分组查询
"_id": "$_id",
"name": bson.M{"$first": "$name"},
},
},
})
map[_id:<nil> name:Alex]
不同类型跨表查询
假设有两个表:(表A和表B)
{
"_id" : ObjectId("5f9faba46b299d1336f9d316"),
"noteCode" : "20201102144804000001",
"userId" : 93,
"title" : "标题",
"content" : "内容"
}
和
{
"_id" : ObjectId("5f9faba46b299d1336f9d317"),
"noteId" : "5f9faba46b299d1336f9d316",
"imgId" : 316,
"imgUrl" : "https://xxx/selection1577778815396.png",
"createTime" : ISODate("2020-11-02T14:48:04.356+08:00")
}
可以看到,表B的noteId是表A的_id
的string
,直接连表肯定是不行的,因此我们需要多做一步转换,将objectid转换为string或将string转为objectid.
将关联ID类型转换为一致(objectId to string)
db.getCollection("note").aggregate(
[{
"$project":
{
// 此处的id相当于是额外在表中增加了一个字段
// 相当于声明了一个变量id
"id":
{
"$convert": {
// 将本表的_id转为string
"input": "$_id",
"to": "string"
}
},
"noteCode": 1
}
}, {
"$lookup":
{
"from": "noteImage",
// 引用上面的id字段
"localField": "id",
"foreignField": "noteId",
"as": "image_docs"
}
}]
);
或,将关联ID类型转换为一致(string to objectId)
db.getCollection("noteImage").aggregate(
[{
"$project":
{
"nid":
{
"$convert": {
"input": "$noteId",
"to": "objectId"
}
},
"imgId": 1
}
}, {
"$lookup":
{
"from": "note",
"localField": "nid",
"foreignField": "_id",
"as": "noteDocs"
}
}]
);
简便写法
使用$toString
将ObjectId转为字符串
db.collection('article').aggregate([
{ "$addFields": { "article_id": { "$toString": "$_id" }}},
{ "$lookup": {
"from": "comments",
"localField": "article_id",
"foreignField": "articleId",
"as": "comments"
}}
])
或使用$toObjectId
将字符串转为objectId
db.collection('article').aggregate([
{ "$addFields": { "article_id_objectId": { "$toObjectId": "$article_id" }}},
])
array中的string和objectId互相转换
可以使用$map
将convert作用于array的每一个元素
db.collection.aggregate([
{
$addFields: {
objectIdArray: {
$map: {
input: "$stringArray",
as: "str",
in: {
$convert: {
input: "$$str",
to: "objectId",
onError: null
}
}
}
}
}
}
])
在这个聚合查询中,使用 $map 操作符将原始的字符串数组转换为 ObjectId 数组。在 $convert 操作符中,指定了 onError 参数为 null,表示如果转换失败,则返回 null 值。
如果想要在转换失败时返回一个默认值,可以将 onError 参数设置为一个默认值,例如:
db.collection.aggregate([
{
$addFields: {
objectIdArray: {
$map: {
input: "$stringArray",
as: "str",
in: {
$convert: {
input: "$$str",
to: "objectId",
onError: ObjectId("000000000000000000000000")
}
}
}
}
}
}
])
神奇的$运算符
$
$
在 update 中 加上关键字 就变成了修改器,其实 $
字符 独立出现也是有意义的 , 可以理解为一个代指符。
现在把 “score”: 100 的 test_list 里面的 2 改为 9,{$set :{"test_list.0" : 9}}
这样就是对应 Array 中的下标进行修改了 “test_list.下标”,如果是一个很长很长很长的 Array 怎么整呢?
$ 字符在语句中代表了下标,位置,使用 update的话, 满足条件的数据下标位置就会传递到 $ 字符中。
$exists
$exists是一个查询操作符,用于检查字段是否存在于文档中。它可以用于查找具有特定字段的文档或者查找没有特定字段的文档。
语法:
{ field: { $exists: <boolean> } }
用法:
存在db.person.find( { phone: { $exists: true } } )
注意:
-
$exists
操作符并不关心字段的实际值,只关心字段是否存在。如果指定的字段存在于文档中,不论其值是什么,都会被匹配到。 -
可以将$exists操作符与其他查询操作符组合使用,以构建更复杂的查询条件。
-
对于在嵌套文档或数组中的字段,
$exists
操作符只会检查字段是否存在于文档中,而不会检查字段的值是否为空或非空。如果需要同时检查字段是否存在和其值是否为空,可以结合使用$exists
和其他查询操作符,如$eq
、$ne
等。
$mod
$mod是一个查询操作符,用于匹配字段值与指定除法操作的余数条件,
语法:
{ field: { $mod: [ divisor, remainder ] } }
field是要匹配的字段名,divisor是除数,remainder是余数。
# 查询age字段的值除以2余0的文档
余数db.person.find( { age: { $mod: [ 2, 0 ] } } )
$mod
操作符只能用于整数字段,并且它只能匹配满足给定除法操作的整数余数条件的文档。
$unset
$unset是MongoDB的更新操作符之一,用于从文档中移除指定的字段。
语法:
{ $unset: { field1: "", field2: "", ... } }
field1、field2等是要移除的字段名,空字符串 “” 用于表示移除该字段。
# 删除某个字段
db.person.update( { _id: 1}, { $unset: { name:"" } })
-
$unset操作符只能用于更新操作(如updateOne、updateMany等),它不适用于查询操作。
-
如果指定的字段不存在于文档中,$unset操作符不会引发错误,它会忽略该字段。
$type
$type
是MongoDB的查询操作符之一,用于匹配指定字段的数据类型。
$type
的使用方式如下:
{ field: { $type: <type> } }
在上述示例中,field是要匹配的字段名,<type>
是一个数值或字符串,用于指定要匹配的数据类型。
# 使用$type操作符来查找"price"字段类型为double的文档:
db.products.find({ price: { $type: "double" } })
MongoDB支持的数据类型包括:
- double:双精度浮点数
- string:字符串
- object:嵌套文档
- array:数组
- binData:二进制数据
- objectId:对象ID
- bool:布尔值
- date:日期
- null:空值
- regex:正则表达式
- javascript:JavaScript代码
- int:整数
- timestamp:时间戳
- long:长整数
- decimal:高精度小数
- minKey:最小键
- maxKey:最大键
$size
$size
是MongoDB的查询操作符之一,用于匹配数组字段的大小(元素数量)。
{ field: { $size: <size> } }
field
是要匹配的数组字段名,<size>
是一个整数,用于指定要匹配的数组大小。
使用$size
操作符来查找"items"数组大小为3的文档:
db.orders.find({ items: { $size: 3 } })
-
$size操作符只能用于匹配数组字段的大小,不适用于其他数据类型。
-
另外,对于嵌套数组,$size操作符会匹配整个数组的大小,而不仅限于顶层数组。
$mul
$mul
是MongoDB的更新操作符之一,用于将指定字段的值乘以给定的因子。
{ $mul: { field: <factor> } }
field
是要更新的字段名,<factor>
是一个数字,表示要乘以的因子。
可以使用$mul操作符将"price"字段的值乘以2:
db.inventory.updateMany({}, { $mul: { price: 2 } })
$mul
操作符只能用于更新操作(如updateOne、updateMany等),它不适用于查询操作。
$rename
$rename
是MongoDB的更新操作符之一,用于重命名文档中的字段。
{ $rename: { oldField: newField } }
oldField
是要重命名的字段名,newField
是字段的新名称。
可以使用$rename
操作符将"name"字段重命名为"fullName":
db.users.updateMany({}, { $rename: { name: "fullName" } })
$rename
操作符只能用于更新操作(如updateOne、updateMany等),它不适用于查询操作。
另外,如果指定的旧字段不存在于文档中,$rename操作符不会引发错误,它会忽略该字段。
$elemMatch
$elemMatch
是MongoDB的查询操作符之一,用于在数组字段中匹配满足多个查询条件的元素。
{ field: { $elemMatch: { condition1, condition2, ... } } }
field
是要匹配的数组字段名,condition1、condition2等是一个或多个查询条件,用于指定要匹配的元素条件。
假设有一个名为"orders"的集合,其中的文档包含了"items"字段,它是一个数组,每个元素都是一个嵌套文档,包含了"product"和"quantity"字段。可以使用$elemMatch操作符来查找满足多个条件的"items"元素:
db.orders.find({ items: { $elemMatch: { product: "Apple", quantity: { $gte: 10 } } } })
上述示例将返回所有满足条件的文档,其中"items"数组至少包含一个元素,该元素的"product"字段为"Apple"且"quantity"字段大于等于10。
需要注意的是,$elemMatch
操作符可以在数组字段中匹配多个条件,并且它将仅返回满足所有条件的文档。如果不使用$elemMatch,数组字段中的每个条件将作为独立的匹配条件处理。
$push
$push
是MongoDB的更新操作符之一,用于向数组字段中添加一个或多个元素。
{ $push: { field: <value> } }
field
是要更新的数组字段名,<value>
是要添加到数组中的元素。
假设有一个名为"users"的集合,其中的文档包含了"favorites"字段,它是一个数组,用于存储用户的喜爱项。可以使用$push操作符将新的喜爱项添加到"favorites"数组中:
db.users.updateOne({ _id: ObjectId("...") }, { $push: { favorites: "Book" } })
-
如果指定的数组字段不存在,$push操作符将会自动创建该数组字段,并将元素添加到其中。
-
另外,
$push
操作符还可以添加多个元素到数组中,可以使用$each
修饰符和数组来指定要添加的多个元素。 -
不去重
$pull
$pull
是MongoDB的更新操作符之一,用于从数组字段中移除满足指定条件的元素。
{ $pull: { field: <condition> } }
field
是要更新的数组字段名,<condition>
是一个查询条件,用于指定要移除的元素条件。
假设有一个名为"users"的集合,其中的文档包含了"favorites"字段,它是一个数组,用于存储用户的喜爱项。可以使用$pull操作符移除"favorites"数组中的特定元素:
db.users.updateOne({ _id: ObjectId("...") }, { $pull: { favorites: "Book" } })
$pull
操作符可以移除满足指定条件的所有元素,而不仅仅是第一个匹配的元素。
$pop
$pop
是MongoDB的更新操作符之一,用于从数组字段中移除第一个或最后一个元素。
{ $pop: { field: <value> } }
field
是要更新的数组字段名,<value>
是一个数值,用于指定要移除的元素位置。若 <value>
为 1,则移除最后一个元素;若<value>
为 -1,则移除第一个元素。
假设有一个名为"users"的集合,其中的文档包含了"favorites"字段,它是一个数组,用于存储用户的喜爱项。可以使用$pop操作符从"favorites"数组中移除第一个或最后一个元素:
db.users.updateOne({ _id: ObjectId("...") }, { $pop: { favorites: 1 } })
-
使用$pop操作符时,若数组字段不存在或为空,不会引发错误,而是忽略操作。
-
另外,可以在同一个更新操作中多次使用$pop操作符来移除多个元素。
$all
$all
是MongoDB的查询操作符之一,用于匹配包含多个元素的数组字段。
{ field: { $all: [<value1>, <value2>, ...] } }
field
是要匹配的数组字段名,<value1>, <value2>, ...
是要匹配的多个元素。
假设有一个名为"products"的集合,其中的文档包含了"tags"字段,它是一个数组,用于标记产品的标签。可以使用$all操作符来查找包含指定标签的产品:
db.products.find({ tags: { $all: ["electronics", "smartphone"] } })
$addToSet
$addToSet
是MongoDB的更新操作符之一,用于向数组字段中添加一个元素,但仅在该元素不存在于数组中时才添加。
{ $addToSet: { field: <value> } }
field
是要更新的数组字段名,<value>
是要添加到数组中的元素。
假设有一个名为"users"的集合,其中的文档包含了"favorites"字段,它是一个数组,用于存储用户的喜爱项。可以使用$addToSet操作符将新的喜爱项添加到"favorites"数组中,但只有当该元素在数组中不存在时才会添加:
db.users.updateOne({ _id: ObjectId("...") }, { $addToSet: { favorites: "Book" } })
-
如果指定的数组字段不存在,$addToSet操作符将会自动创建该数组字段,并将元素添加到其中。
-
另外,
$addToSet
操作符还可以添加多个元素到数组中,可以使用$each
修饰符和数组来指定要添加的多个元素。
$inc
$inc
是MongoDB的更新运算符之一,用于对文档中的字段进行原子增减操作。它可以适用于数字类型的字段,如整数或浮点数。
{ $inc: { <field1>: <amount1>, <field2>: <amount2>, ... } }
- $inc是更新操作符,指示对字段进行增减操作。
<field1>
,<field2>
, … 是要进行增减操作的字段名。<amount1>
,<amount2>
, … 是增减的值。可以为正数、负数或零。
使用$inc时,MongoDB会在原始文档的字段上执行增减操作,并将更新后的文档存储回数据库。
假设有一个inventory集合,其中包含了产品的库存数量:
{
"_id": 1,
"product": "ABC",
"quantity": 10
}
db.inventory.updateOne(
{ "_id": 1 },
{ $inc: { "quantity": -2 } }
)
上述操作将使库存数量减少2
$text
$text
是MongoDB的查询运算符之一,用于在文本索引上执行全文搜索查询。它允许你在一个或多个字段中搜索包含指定关键词的文本。
$text
查询运算符需要与$search
运算符一起使用,以指定要搜索的关键词。
{ $text: { $search: <searchTerm> } }
- $text是查询操作符,指示执行全文搜索。
- $search是指定要搜索的关键词。
要使用$text进行全文搜索,需要满足以下条件:
在集合中创建一个文本索引,用于支持全文搜索。例如,对title和description字段创建一个名为textIndex的文本索引:
db.collection.createIndex({ title: "text", description: "text" })
在查询中使用$text运算符来指定搜索关键词:
db.collection.find({ $text: { $search: "keyword" } })
示例:
假设有一个名为products的集合,包含产品的标题和描述信息。要搜索包含关键词"apple"的产品,可以使用$text查询:
db.products.find({ $text: { $search: "apple" } })
上述查询将返回包含关键词"apple"的产品文档。
需要注意的是,使用$text进行全文搜索时,MongoDB会根据文本索引的设置计算匹配度,并根据匹配度对结果进行排序。默认情况下,MongoDB将返回按匹配度降序排列的结果。
可以结合$meta使用,做出更复杂的查询:
db.collection.find({ $text: { $search: "your_search_term" } }, { score: { $meta: "textScore" } }).sort({ score: { $meta: "textScore" } })
$meta
在 MongoDB 中,$meta
是一个投影操作符,用于检索文本搜索的元数据,主要用于获取文档的相关性分数(textScore)。以下是 $meta
的详细用法:
-
$meta
和文本搜索在执行文本搜索时,MongoDB会为匹配查询条件的文档分配一个相关性分数,表示文档与查询的匹配程度。你可以使用
$meta
来检索这个相关性分数。db.collection.find( { $text: { $search: "your_search_term" } }, { score: { $meta: "textScore" } } )
在这个示例中,
score
字段将包含每个文档的相关性分数。 -
$meta
在聚合管道中的应用$meta
也可以在聚合管道中使用,用于处理文本搜索的结果:db.collection.aggregate([ { $match: { $text: { $search: "your_search_term" } } }, { $project: { score: { $meta: "textScore" } } } ])
在这个例子中,首先使用
$match
阶段来过滤匹配查询条件的文档,然后使用$project
阶段来投影score
字段,并使用$meta
来获取相关性分数。 -
$meta
的其他用途除了在文本搜索中获取相关性分数之外,
$meta
也可以用于其他一些场景,比如:- 在地理空间查询中获取查询的距离。
- 在索引查询中获取索引的键值。
ObjectId
对于"_id" : ObjectId("5b151f8536409809ab2e6b26")
,其组成如下:
- “5b151f85” 代指的是时间戳,这条数据的产生时间(0-8字节)
- “364098” 代指某台机器的机器码,存储这条数据时的机器编号(9-14字节的机器标识符)
- “09ab” 代指进程ID,多进程存储数据的时候,非常有用的(15-18字节的进程id)
- “2e6b26” 代指计数器,这里要注意的是,计数器的数字可能会出现重复,不是唯一的(19-24字节是计数器)
- 只要是支持MongoDB的语言,都会有一个或多个方法,对ObjectID进行转换可以得到以上四种信息
- 这个类型是不可以被JSON序列化的
索引-Index
- 索引支持在MongoDB中高效地执行查询。如果没有索引,MongoDB必须执行全集合扫描,即扫描集合中的每个文档,以选择与查询语句匹配的文档。这种扫描全集合的查询效率是非常低的,特别在处理大量的数据时,查询可以要花费几十秒甚至几分钟,这对网站的性能是非常致命的。
- 如果查询存在适当的索引,MongoDB可以使用该索引限制必须检查的文档数。
- 索引是特殊的数据结构,它以易于遍历的形式存储集合数据集的一小部分。索引存储特定字段或一组字段的值,按字段值排序。索引项的排序支持有效的相等匹配和基于范围的查询操作。此外MongoDB还可以使用索引中的排序返回排序结果。
- 官网文档:https://docs.mongodb.com/manual/indexes/
- MongoDB索引使用B树数据结构(确切的说是B-Tree,MySQL是B+Tree)
索引的类型
单字段索引
MongoDB支持在文档的单个字段上创建用户定义的升序/降序索引,称为单字段索引(Single Field Index)。
对于单个字段索引和排序操作,索引键的排序顺序(即升序或降序)并不重要,因为MongoDB可以在任何方向上遍历索引。
复合索引
MongoDB还支持多个字段的用户定义索引,即复合索引(Compound Index)。
复合索引中列出的字段顺序具有重要意义。例如,如果复合索引由 { userid: 1, score: -1 } 组成,则索引首先按userid正序排序,然后在每个userid的值内,再在按score倒序排序。
其他索引
- 地理空间索引(Geospatial Index)、文本索引(Text Indexes)、哈希索引(Hashed Indexes)。
- 地理空间索引(Geospatial Index):为了支持对地理空间坐标数据的有效查询,MongoDB提供了两种特殊的索引:返回结果时使用平面几何的二维索引和返回结果时使用球面几何的二维球面索引。
- 文本索引(Text Indexes):MongoDB提供了一种文本索引类型,支持在集合中搜索字符串内容。这些文本索引不存储特定于语言的停止词(例如“the”、“a”、“or”),而将集合中的词作为词干,只存储根词。
- 哈希索引(Hashed Indexes):为了支持基于散列的分片,MongoDB提供了散列索引类型,它对字段值的散列进行索引。这些索引在其范围内的值分布更加随机,但只支持相等匹配,不支持基于范围的查询。
MongoDB支持多种类型的索引,每种索引类型都有不同的用途和性能特点。以下是MongoDB支持的主要索引类型以及如何设置它们:
-
单字段索引:
单字段索引是最常见的索引类型,用于对集合中的单个字段进行索引。可以通过在字段上调用.createIndex()
方法来创建单字段索引。db.collection.createIndex({ field_name: 1 })
这将在
field_name
字段上创建一个升序索引。将数字-1
传递给字段将创建一个降序索引。 -
复合索引:
复合索引允许您在多个字段上创建一个组合索引,以提高查询性能。复合索引可以覆盖多个查询条件,但需要权衡性能和索引大小。创建复合索引与创建单字段索引类似。db.collection.createIndex({ field1: 1, field2: -1 })
这将在
field1
字段上创建升序索引,然后在field2
字段上创建降序索引。 -
文本索引:
文本索引用于全文搜索,可以用于对文本字段执行文本搜索操作。文本索引不适用于所有数据,但对于需要执行全文搜索的场景很有用。db.collection.createIndex({ content: "text" })
-
地理空间索引:
地理空间索引用于存储和查询地理空间数据,如地理坐标和地理区域。这对于执行地理空间查询和位置相关操作很有用。db.collection.createIndex({ location: "2dsphere" })
-
哈希索引:
哈希索引用于在大数据集中进行均匀分布的索引,但不适合范围查询。它可以在哈希字段上执行相等性查询。db.collection.createIndex({ hash_field: "hashed" })
-
唯一索引:
唯一索引确保集合中的字段值是唯一的。在创建唯一索引时,任何两个文档不能具有相同的索引键值。db.collection.createIndex({ unique_field: 1 }, { unique: true })
以上示例中,collection
是集合名称,field_name
是字段名,field1
、field2
、content
等是字段名,location
是地理空间字段,hash_field
是用于哈希索引的字段,unique_field
是用于唯一索引的字段。
创建索引时,还可以通过传递额外的选项来定制索引,例如,设置索引的名称、部分索引、过期时间等。不过,需要注意的是,创建过多的索引可能会导致索引维护成本增加以及索引大小增大,因此在创建索引时要权衡索引的用途和性能影响。
全文索引text
全文索引是一种用于执行全文搜索的特殊类型索引,它允许您在文本字段上进行高效的文本搜索操作,而不仅仅是简单的相等性比较。MongoDB的全文索引支持称为文本索引。使用文本索引,您可以在文本字段中搜索关键字、短语和模糊匹配。
以下是如何在MongoDB中创建和使用文本索引:
-
创建文本索引:
使用.createIndex()
方法并指定字段及索引类型"text"
来创建文本索引。db.collection.createIndex({ content: "text" })
在上面的示例中,
collection
是集合名称,content
是要在其上创建文本索引的字段。 -
执行文本搜索:
创建文本索引后,您可以使用$text
操作符来执行文本搜索。db.collection.find({ $text: { $search: "keyword" } })
在这个查询中,
$search
指定了要搜索的关键字。MongoDB将返回匹配关键字的文档,按照匹配程度排序。 -
进一步定制文本搜索:
文本搜索还支持一些进一步的选项,例如:- 指定
"language"
参数以指定搜索语言。 - 使用
"caseSensitive"
参数来指定搜索是否区分大小写。 - 使用
"diacriticSensitive"
参数来指定搜索是否区分重音符号。
db.collection.find({ $text: { $search: "keyword", $language: "en", $caseSensitive: true, $diacriticSensitive: false } })
- 指定
全文索引是为了执行文本搜索而设计的,不适用于所有类型的数据和查询。它们在处理大量文本数据和需要复杂搜索功能的场景中非常有用。当创建全文索引时,MongoDB将会为文本字段构建一个特殊的数据结构,以便在搜索时能够更高效地匹配和排序文档。
MongoDB的文本索引提供了强大的全文搜索功能,可用于在文本字段中进行关键字、短语和模糊搜索,从而提高查询的灵活性和性能。
在 MongoDB 中,每个集合只能有一个文本索引。这是因为文本索引的设计目的是为了支持全文搜索,而全文搜索一般基于一个文本字段来执行。为了在一个集合中实现全文搜索,您可以在一个文本字段上创建一个文本索引,然后在该字段上执行 $text
查询操作。
如果需要在同一个集合中执行多种类型的搜索,包括全文搜索和其他类型的查询,您可以考虑创建其他类型的索引,如单字段索引、复合索引或地理空间索引,以满足不同查询模式的需求。但是,请注意,在同一个集合中只能有一个文本索引。
如果您需要在不同的字段上执行全文搜索,您可以选择在不同的集合上创建文本索引,然后执行跨集合的全文搜索查询。这样,每个集合都可以有其自己的文本索引,以支持不同的查询需求。
总之,虽然每个集合只能有一个文本索引,但您可以通过创建其他类型的索引,以及在不同的集合上创建文本索引,来满足不同类型的查询需求。
当您在 MongoDB 中使用文本索引执行全文搜索时,的确可能会出现一些与预期不符合的结果,例如查出一些无关的数据或者排序不够准确。这是因为文本搜索涉及到文本分析和相关度评分,而且默认情况下 MongoDB 使用了 TF-IDF(词频-逆文档频率)算法来评估文档与查询之间的相关度。
如果您想按照数据的相关度进行排序,可以考虑以下方法:
-
使用
$text
查询操作:
在执行$text
查询操作时,MongoDB会返回一个称为score
的字段,表示每个文档与查询之间的相关度分数。您可以使用.sort()
方法按照score
进行降序排序,以获得相关度最高的文档。db.collection.find( { $text: { $search: "your_search_query" } }, { score: { $meta: "textScore" } } ).sort({ score: { $meta: "textScore" } })
-
定制权重:
在创建文本索引时,您可以为不同的字段分配不同的权重,以便更准确地评估相关度。这样,某些字段的内容会被认为比其他字段更重要。(一个集合只能拥有 一个 文本检索索引,但是这个索引可以覆盖多个字段。)db.collection.createIndex( { field1: "text", field2: "text" }, { weights: { field1: 10, field2: 5 } } )
在上述示例中,
field1
的权重为 10,而field2
的权重为 5。 -
使用自定义评分算法:
MongoDB的文本索引支持自定义评分算法。您可以创建一个计算相关度分数的自定义函数,并将其用作$text
查询操作中的评分字段。db.collection.createIndex({ content: "text" }) db.collection.find( { $text: { $search: "your_search_query" } }, { score: { $meta: "textScore" } } ).sort({ score: your_custom_score_function })
这些方法可以帮助您根据相关度对文本搜索结果进行排序。请注意,MongoDB的文本搜索和排序涉及多个参数和算法,您可能需要根据您的数据和查询模式进行适当的调整,以获得最佳结果。
MongoDB的文本索引是一种支持全文搜索的特殊索引类型,它的原理基于文本分析和相关度评分。文本索引允许您在文本字段中执行关键字、短语和模糊搜索,以及评估文档与查询之间的相关度。以下是MongoDB文本索引的工作原理:
-
文本分析:
在创建文本索引时,MongoDB会对文本字段中的文本数据进行分析。分析包括将文本拆分成单词(或称为词汇单元),并将这些单词存储在索引数据结构中。分析还可能包括移除停用词(如 “a”、“an”、“the” 等)以及执行词干化操作(将单词转化为其基本形式)。 -
倒排索引:
文本索引采用了倒排索引的概念,这是一种用于加速文本搜索的数据结构。倒排索引记录了每个词汇单元(单词)出现在哪些文档中。这使得在查询时可以快速找到包含特定词汇单元的文档,从而实现更快的搜索操作。 -
词频-逆文档频率(TF-IDF)算法:
MongoDB的文本索引使用了 TF-IDF 算法来计算文档与查询之间的相关度。TF-IDF 结合了两个因素:- 词频(Term Frequency,TF):表示特定词汇单元在文档中的出现频率。
- 逆文档频率(Inverse Document Frequency,IDF):表示特定词汇单元在文档集合中的稀有性。稀有词汇单元的权重较高。
综合考虑词频和逆文档频率,TF-IDF 算法为每个文档计算一个相关度分数,表示该文档与查询的匹配程度。
-
评分和排序:
在执行文本搜索时,MongoDB返回一个称为score
的字段,表示每个文档与查询之间的相关度分数。您可以使用.sort()
方法按照score
进行降序排序,从而将最相关的文档排在前面。 -
自定义权重和评分:
MongoDB还允许您为不同字段分配不同的权重,以便在计算相关度分数时更准确地考虑字段的重要性。您还可以使用自定义评分函数,从而实现更精细的相关度评估。
总之,MongoDB的文本索引工作原理是基于文本分析、倒排索引和 TF-IDF 算法,它使得全文搜索能够高效地匹配和排序文档,以满足复杂的搜索需求。
索引的管理操作
索引的查看
db.collection.getIndexes()
查看comment集合中所有的索引情况:
> db.comment.getIndexes()
[
{
"v" : 2,
"key" : {
"_id" : 1
},
"name" : "_id_",
"ns" : "articledb.comment"
}
]
结果中显示的是默认 _id
索引:
- MongoDB在创建集合的过程中,在
_id
字段上创建一个唯一的索引,默认名字为_id
,该索引可防止客户端插入两个具有相同值的文档,您不能在_id
字段上删除此索引。 - 注意:该索引是唯一索引,因此值不能重复,即
_id
值不能重复的。在分片集群中,通常使用_id
作为片键。
索引的创建
db.collection.createIndex(keys, options)
参数:
options(更多选项)列表:
单字段索引示例:对 userid 字段建立索引:
> db.comment.createIndex({userid:1})
{
"createdCollectionAutomatically" : false,
"numIndexesBefore" : 1,
"numIndexesAfter" : 2,
"ok" : 1
}
复合索引:对 userid 和 nickname 同时建立复合(Compound)索引:
> db.comment.createIndex({userid:1,nickname:-1})
{
"createdCollectionAutomatically" : false,
"numIndexesBefore" : 2,
"numIndexesAfter" : 3,
"ok" : 1
}
索引的移除
db.collection.dropIndex(index)
参数:
删除 comment 集合中 userid 字段上的升序索引:
> db.comment.dropIndex({userid:1})
{ "nIndexesWas" : 3, "ok" : 1 }
所有索引的移除:
db.collection.dropIndexes()
提示: _id
的字段的索引是无法删除的,只能删除非 _id
字段的索引。
索引的使用
执行计划
分析查询性能(Analyze Query Performance)通常使用执行计划(解释计划、Explain Plan)来查看查询的情况,如查询耗费的时间、是否基于索引查询等。
那么,通常想知道,建立的索引是否有效,效果如何,都需要通过执行计划查看。
db.collection.find(query,options).explain(options)
查看根据userid查询数据的情况:
> db.comment.find({userid:"1003"}).explain()
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "articledb.comment",
"indexFilterSet" : false,
"parsedQuery" : {
"userid" : {
"$eq" : "1003"
}
},
"winningPlan" : {
"stage" : "COLLSCAN",
"filter" : {
"userid" : {
"$eq" : "1003"
}
},
"direction" : "forward"
},
"rejectedPlans" : [ ]
},
"serverInfo" : {
"host" : "9ef3740277ad",
"port" : 27017,
"version" : "4.0.10",
"gitVersion" : "c389e7f69f637f7a1ac3cc9fae843b635f20b766"
},
"ok" : 1
}
关键点看: “stage” : “COLLSCAN”, 表示全集合扫描,: “stage” : “IXSCAN” ,基于索引的扫描
涵盖的查询
当查询条件和查询的投影仅包含索引字段时,MongoDB直接从索引返回结果,而不扫描任何文档或将文档带入内存。 这些覆盖的查询可以非常有效。
mongo导出导入
导出mongoexport
mongoexport -h ip:port -u 用户名 -p 密码-d 数据库名 -c 集合名 -o 输出的文件路径以及文件名 --type json/csv -f 字段名
- 如果不指定type,默认为json
- type如果为csv,则需要指明字段:-f “name,id,…”
mongoexport
是MongoDB数据库的一个命令行工具,用于将数据库中的数据导出到JSON、CSV或其他格式的文件中。下面是mongoexport
工具的详细用法说明:
mongoexport -d <数据库名称> -c <集合名称> -o <输出文件路径> [选项]
选项说明:
-d
:指定要导出数据的数据库名称。-c
:指定要导出数据的集合名称。-o
:指定导出数据的输出文件路径。
可选选项(选项可根据需要进行设置):
-
输出格式选项:
--type <输出格式>
:指定输出格式,可以是json(默认)或csv。
-
查询条件选项:
--query <查询条件>
:指定要导出的数据的查询条件。查询条件应该是一个JSON文档,例如'{"field": "value"}'
。--fields <字段列表>
:指定要导出的字段列表,用逗号分隔。例如,--fields name,email
。
-
导出选项:
--limit <文档数量>
:指定要导出的文档数量的限制。--skip <文档数量>
:跳过指定数量的文档再导出。--sort <排序字段>
:指定导出的文档按照某个字段进行排序。例如,--sort age:1
表示按照age
字段升序排序。
-
其他选项:
--csv
:指定输出为CSV格式。--jsonArray
:以JSON数组的格式输出文档。--pretty
:以漂亮的格式输出JSON文档。--quiet
:以静默模式执行,不显示导出进度信息。
示例用法:
- 导出整个集合到JSON文件:
mongoexport -d mydb -c mycollection -o output.json
- 导出指定查询条件下的数据到CSV文件:
mongoexport -d mydb -c mycollection -o output.csv --type csv --query '{"field": "value"}' --fields name,email
- 导出并排序数据到JSON文件:
mongoexport -d mydb -c mycollection -o output.json --sort age:1
使用mongoexport
工具可以轻松地将MongoDB中的数据导出到所需的格式中,以便进一步处理或备份数据。
导入mongoimport
mongoimport -d "指定要被导入数据的数据库名" -c "指定表名" --file "指定被导入的文件名" --headline --type "指定类型" -f "指定字段"
- 如果导入的格式是csv,则可以使用第一行的标题作为导入的字段,–headline
- type默认为json
mongoimport
是MongoDB数据库的命令行工具,用于将数据从外部文件导入到MongoDB数据库中。以下是mongoimport
工具的详细用法说明:
mongoimport [选项] <文件路径>
选项说明:
-
连接选项:
--host <主机名>
:MongoDB服务器的主机名,默认为localhost。--port <端口号>
:MongoDB服务器的端口号,默认为27017。--username <用户名>
:登录数据库的用户名。--password <密码>
:登录数据库的密码。--authenticationDatabase <认证数据库>
:用于进行身份验证的数据库,默认为admin。
-
导入选项:
-d <数据库名称>
:要导入数据的目标数据库名称。-c <集合名称>
:要导入数据的目标集合名称。--type <输入格式>
:指定输入文件的格式,可以是json(默认)或csv。--headerline
:如果CSV文件包含标题行,则使用此选项。
-
其他选项:
--file <文件路径>
:指定要导入的文件路径。--drop
:在导入数据之前删除目标集合中的所有文档。--upsertFields <字段列表>
:如果存在匹配的记录,则更新现有文档,可以指定多个字段,用逗号分隔。--jsonArray
:要导入的JSON文件包含一个JSON数组。
示例用法:
- 导入JSON文件到指定数据库和集合:
mongoimport -d mydb -c mycollection --file data.json
- 导入CSV文件到指定数据库和集合:
mongoimport -d mydb -c mycollection --type csv --file data.csv --headerline
- 删除目标集合的文档并导入JSON文件:
mongoimport -d mydb -c mycollection --file data.json --drop
- 使用
upsert
功能,如果存在匹配的记录则更新:
mongoimport -d mydb -c mycollection --file data.json --upsertFields name
请根据您的需求和数据文件的格式来选择合适的选项和参数。使用mongoimport
工具可以方便地将外部数据导入到MongoDB数据库中,以进行分析、处理或存储。
备份mongodump
mongodump -h "数据库所在ip地址" -d "数据库名称" -o "保存文件的目录"
mongodump
是MongoDB的命令行工具,用于备份MongoDB数据库的数据。以下是mongodump
工具的详细用法说明:
mongodump [选项]
选项说明:
-
连接选项:
--host <主机名>
:MongoDB服务器的主机名,默认为localhost。--port <端口号>
:MongoDB服务器的端口号,默认为27017。--username <用户名>
:登录数据库的用户名。--password <密码>
:登录数据库的密码。--authenticationDatabase <认证数据库>
:用于进行身份验证的数据库,默认为admin。
-
备份选项:
-d <数据库名称>
:要备份的目标数据库名称。-c <集合名称>
:要备份的目标集合名称。-o <输出目录>
:指定备份文件的输出目录,默认为当前工作目录。
-
其他选项:
--gzip
:使用gzip对备份数据进行压缩。--archive=<文件名>
:将备份数据输出到一个归档文件,可以与--gzip
结合使用。
示例用法:
- 备份整个数据库到指定目录:
mongodump -d mydb -o /path/to/backup/directory
- 备份指定集合到指定目录:
mongodump -d mydb -c mycollection -o /path/to/backup/directory
- 使用gzip进行备份并压缩输出:
mongodump -d mydb -o /path/to/backup/directory --gzip
- 将备份数据输出到归档文件并使用gzip:
mongodump -d mydb --archive=/path/to/backup/archive.gz --gzip
mongodump
工具可用于创建MongoDB数据库的备份,以便将数据存储在文件中,以供以后恢复或迁移。根据需要,您可以备份整个数据库、特定集合或使用不同的选项来满足备份需求。
恢复mongorestore
mongorestore -h "数据库所在ip" -d "要保存数据的数据库名称" --dir "存放数据的目录"
mongorestore
是MongoDB的命令行工具,用于将mongodump
工具生成的备份文件还原到MongoDB数据库中。以下是mongorestore
工具的详细用法说明:
mongorestore [选项] <备份目录>
选项说明:
-
连接选项:
--host <主机名>
:MongoDB服务器的主机名,默认为localhost。--port <端口号>
:MongoDB服务器的端口号,默认为27017。--username <用户名>
:登录数据库的用户名。--password <密码>
:登录数据库的密码。--authenticationDatabase <认证数据库>
:用于进行身份验证的数据库,默认为admin。
-
还原选项:
-d <目标数据库名称>
:指定要还原到的目标数据库名称。-c <目标集合名称>
:指定要还原到的目标集合名称。--nsInclude <命名空间>
:只还原指定的命名空间(包括数据库和集合)。--nsFrom <源命名空间>
:指定从备份数据的源命名空间还原数据。--nsTo <目标命名空间>
:指定还原到的目标命名空间。--drop
:在还原数据之前删除目标集合中的所有文档。
-
其他选项:
--gzip
:如果备份数据已使用gzip压缩,则使用此选项进行解压。--archive=<文件名>
:从归档文件还原数据,可以与--gzip
结合使用。--dir=<备份目录>
:指定备份文件所在的目录。
示例用法:
- 从备份目录还原整个数据库:
mongorestore /path/to/backup/directory
- 从备份目录还原指定集合到指定数据库:
mongorestore -d mydb -c mycollection /path/to/backup/directory
- 从归档文件还原数据:
mongorestore --archive=/path/to/backup/archive.gz --gzip
- 还原数据并删除目标集合中的所有文档:
mongorestore -d mydb -c mycollection --drop /path/to/backup/directory
mongorestore
工具可用于将mongodump
工具生成的备份数据还原到MongoDB数据库中,以便进行数据恢复或数据迁移。根据需要,您可以还原整个数据库、特定集合或使用不同的选项来满足还原需求。
认证
- -u 用户名 -p pwd
- –authenticationDatabase admin
mongoimport导入json数据阻塞,导入不完全
mongoexport
导出json文件mongoimport
导入文件时 卡到某个进度不再动
解决方法:
方法一:pymongo
读取文件再利用pymongo写入操作,但此方法容易有编码,bson格式,时间格式等错误
方法二:numInsertionWorkers
这个参数设置worker的数量,可以理解为并发数,设置--numInsertionWorkers 100
或者更多,数据就可以完全导入,但最后还是会卡在100%
如果只加workers数量不行的话,可以加--batchSize 10000
步长试试。
GridFS
MongoDB GridFS
GridFS 用于存储和恢复那些超过16M(BSON文件限制)的文件(如:图片、音频、视频等)。
GridFS 也是文件存储的一种方式,但是它是存储在MonoDB的集合中。
GridFS 可以更好的存储大于16M的文件。
GridFS 会将大文件对象分割成多个小的chunk(文件片段),一般为256k/个,每个chunk将作为MongoDB的一个文档(document)被存储在chunks集合中。
GridFS 用两个集合来存储一个文件:fs.files与fs.chunks。
每个文件的实际内容被存在chunks(二进制数据)中,和文件有关的meta数据(filename,content_type,还有用户自定义的属性)将会被存在files集合中。
使用
调用 MongoDB 安装目录下bin的 mongofiles.exe工具
具体操作
-
存储文件:
mongofiles –d database put filename
-
获取文件:
mongofiles –d database -l new_filename get filename
-
删除文件:
mongofiles –d database delete filename
-
存储文件:
mongofiles –d database -l filename put new_filename
以这种命令上传文件,文件名为new_filename,不会携带路径 -
查看文件列表:
mongofiles -d database list
-
查找文件:
mongofiles -d database search test.txt
pymongo操作
from pymongo import MongoClient
from gridfs import GridFS
client = MongoClient()
database = client.get_database('files')
gf = GridFS(database=database)
res = gf.find_one()
print(res)
{'_GridOut__buffer': b'',
'_GridOut__chunk_iter': None,
'_GridOut__chunks': Collection(Database(MongoClient(host=['localhost:27017'], document_class=dict, tz_aware=False, connect=True), 'files'), 'fs.chunks'),
'_GridOut__file_id': None,
'_GridOut__files': Collection(Database(MongoClient(host=['localhost:27017'], document_class=dict, tz_aware=False, connect=True), 'files'), 'fs.files'),
'_GridOut__position': 0,
'_file': {'_id': ObjectId('6236e52955dd69298cc54a16'),
'chunkSize': 261120,
'filename': 'test.txt',
'length': 18,
'md5': '7552e7c2987420e42fa1b19a0988c7a1',
'uploadDate': datetime.datetime(2022, 3, 20, 8, 26, 17, 943000)},
'_session': None}
# 打印文件列表['filename1','filename2',...]
print(gf.list())
# 下载文件
with open('./gettest.txt','wb') as f:
for line in gf.get(res._id):
f.write(line)
# 上传文件
with open('./gettest.txt','rb') as f:
gf.put(f.read(),filename='gettest.txt')
原子操作&事务
原子操作
- 原子操作(atomic operation)指的是由多步操作组成的一个操作。如果该操作不能原子地执行,则要么执行完所有步骤,要么一步也不执行,不可能只执行所有步骤的一个子集。
- 不可中断的一个或者一系列操作, 也就是不会被线程调度机制打断的操作, 运行期间不会有任何的上下文切换(context switch).
事务
- 事务(Transaction)是访问并可能更新数据库中各项数据项的一个程序执行单元(unit)。 事务由事务开始(begin transaction)和事务结束(end transaction)之间执行的全体操作组成。
- 事务是一个不可分割的数据库操作序列,也是数据库并发控制的基本单位,其执行的结果必须使数据库从一种一致性状态变到另一种一致性状态。
- 事务结束有两种,事务中的步骤全部成功执行时,提交事务。如果其中一个失败,那么将会发生回滚操作,并且撤销之前的所有操作。也就是说,事务内的语句,要么全部执行成功,要么全部执行失败。
- 事务是恢复和并发控制的基本单位。
- 事务具有四个特征:原子性、一致性、隔离性和持久性。这四个特征通常称为ACID。
官方文档:
Method
|
Description
|
---|---|
StartTransaction()
|
在当前session(回话)根据配置文件开启新事务,如果会话中已经存在一个事务,则返回错误。更多信息, 参考
manual entry .
Parameter:
TransactionOptions
Return Type:
error
|
AbortTransaction()
|
|
CommitTransaction()
|
|
WithTransaction()
|
在此会话上启动一个事务,并运行
fn 回调函数。
Parameters:
Context ,
fn func(ctx SessionContext) ,
TransactionOptions
Return Type:
interface{} ,
error
|
EndSession()
|
终止所有现有的事务并关闭会话。
Parameter:
Context
Return Type: none
|
Example
以下示例展示了如何通过以下步骤创建会话、创建事务并提交多文档插入操作:
- 使用StartSession()方法从客户端创建会话。
- 使用WithTransaction()方法启动事务。
- 插入多个文档。WithTransaction()方法执行插入并提交事务。如果任何操作出现错误,WithTransaction()会处理事务的中止。
- 使用EndSession()方法关闭事务和会话。
wc := writeconcern.Majority()
txnOptions := options.Transaction().SetWriteConcern(wc)
session, err := client.StartSession()
if err != nil {
panic(err)
}
defer session.EndSession(context.TODO())
result, err := session.WithTransaction(context.TODO(), func(ctx mongo.SessionContext) (interface{}, error) {
result, err := coll.InsertMany(ctx, []interface{}{
bson.D{{"title", "The Bluest Eye"}, {"author", "Toni Morrison"}},
bson.D{{"title", "Sula"}, {"author", "Toni Morrison"}},
bson.D{{"title", "Song of Solomon"}, {"author", "Toni Morrison"}},
})
return result, err
}, txnOptions)
支持版本
- MongoDB从 3.0版本引入WiredTiger存储引擎之后开始支持事务。
- MongoDB 3.6之前的版本只能支持单文档的事务。
- MongoDB 4.0版本开始支持复制集部署模式下的事务。
- MongoDB 4.2版本开始支持分片集群中的事务。
要获取事务支持则需要安装对应的或高版本的mongodb
和驱动(pymongo(python)
,mongo-driver(go)
)
内置的一些原子操作
对于python
对于go
语言事务支持源码
根据阅读with系列方法,可以看出它已经封装了开启事务,提交事务,回滚事务的操作。
对于go
func (s *sessionImpl) WithTransaction(ctx context.Context, fn func(sessCtx SessionContext) (interface{}, error),
opts ...*options.TransactionOptions) (interface{}, error) {
// 超时时间为:120 * time.Second
timeout := time.NewTimer(withTransactionTimeout)
defer timeout.Stop()
var err error
for {
// 开启事务
err = s.StartTransaction(opts...)
if err != nil {
return nil, err
}
// 回调函数(会话上下文)
res, err := fn(NewSessionContext(ctx, s))
// 执行失败终止事务
if err != nil {
if s.clientSession.TransactionRunning() {
// 终止事务
_ = s.AbortTransaction(internal.NewBackgroundContext(ctx))
}
select {
case <-timeout.C:
return nil, err
default:
}
if errorHasLabel(err, driver.TransientTransactionError) {
continue
}
return res, err
}
// 判断在回调函数里面是否直接通过会话上下文终止事务了
err = s.clientSession.CheckAbortTransaction()
if err != nil {
return res, nil
}
if ctx.Err() != nil {
_ = s.AbortTransaction(internal.NewBackgroundContext(ctx))
return nil, ctx.Err()
}
// CommitLoop提交事务循环,还在上面那个for里面
CommitLoop:
for {
// 提交
err = s.CommitTransaction(ctx)
if err == nil {
// 返回成功结果 res, err := fn(NewSessionContext(ctx, s))
return res, nil
}
// 超时判断
select {
case <-timeout.C:
return res, err
default:
}
if cerr, ok := err.(CommandError); ok {
// UnknownTransactionCommitResult = "UnknownTransactionCommitResult"
if cerr.HasErrorLabel(driver.UnknownTransactionCommitResult) && !cerr.IsMaxTimeMSExpiredError() {
continue
}
// errorHasLabel:包含规定的错误信息,返回true
// TransientTransactionError = "TransientTransactionError"
if cerr.HasErrorLabel(driver.TransientTransactionError) {
break CommitLoop
}
}
return res, err
}
}
}
对于python
def with_transaction(
self,
callback: Callable[["ClientSession"], _T],
read_concern: Optional[ReadConcern] = None,
write_concern: Optional[WriteConcern] = None,
read_preference: Optional[_ServerMode] = None,
max_commit_time_ms: Optional[int] = None,
) -> _T:
start_time = time.monotonic()
while True:
self.start_transaction(read_concern, write_concern, read_preference, max_commit_time_ms)
try:
ret = callback(self)
except Exception as exc:
if self.in_transaction:
self.abort_transaction()
if (
isinstance(exc, PyMongoError)
and exc.has_error_label("TransientTransactionError")
and _within_time_limit(start_time)
):
# Retry the entire transaction.
continue
raise
if not self.in_transaction:
# Assume callback intentionally ended the transaction.
return ret
while True:
try:
self.commit_transaction()
except PyMongoError as exc:
if (
exc.has_error_label("UnknownTransactionCommitResult")
and _within_time_limit(start_time)
and not _max_time_expired_error(exc)
):
# Retry the commit.
continue
if exc.has_error_label("TransientTransactionError") and _within_time_limit(
start_time
):
# Retry the entire transaction.
break
raise
# Commit succeeded.
return ret
事务使用举例(go/py)
伪代码如下:
db.customers.drop()//假设有一个customers集合
db.createCollection('customers');
// 开始一个新的MongoDB事务
session = db.startSession()
session.startTransaction()
//在事务期间插入文档
db.customers.insert({
name: "john",
age: 20
});
db.customers.update({
name: "john"
},{
$set: {
age: 21
}
});
//提交事务
session.commitTransaction();
//结束会话
session.endSession();
也可以使用更加简便的with方法进行事务操作,参考Multi-Document ACID Transactions in MongoDB with Go
对于go
func main() {
clientOptions := options.Client().ApplyURI("mongodb://localhost:27017")
client, err := mongo.Connect(context.TODO(), clientOptions)
collection:=client.Database().Collection()
if err != nil {
log.Fatal(err)
}
wc := writeconcern.New(writeconcern.WMajority())
txnOptions := options.Transaction().SetWriteConcern(wc)
session,_:=client.StartSession()
result,_:=session.WithTransaction(context.TODO(), func(sessCtx mongo.SessionContext) (interface{}, error) {
result,err:=collection.Find(context.TODO(),bson.M{})
return result,err
},txnOptions)
cur,ok:=result.(*mongo.Cursor)
if ok{
fmt.Println("断言成功")
for cur.Next(context.TODO()){
m:=make(map[string]interface{})
cur.Decode(m)
fmt.Println(m)
}
}else{
fmt.Println("断言失败")
}
}
在开始 MongoDB 事务之前,需要执行几个步骤来设置事务的选项和会话。
-
首先,你需要创建一个
WriteConcern
对象来定义事务的写关注级别。在你的示例中,使用writeconcern.WMajority()
创建了一个写关注级别为 “majority” 的WriteConcern
对象。这意味着事务的提交需要大多数节点确认写操作。 -
然后,你需要创建一个事务选项对象,用于设置事务的选项。在你的示例中,使用
options.Transaction()
创建了一个事务选项对象,并通过SetWriteConcern
方法将之前创建的WriteConcern
对象设置为事务的写关注级别。 -
接下来,你需要使用
StartSession
方法来启动一个会话。会话对象是执行事务操作的上下文,并且可以在多个操作之间共享状态。在你的示例中,使用client.StartSession()
启动了一个会话,并将其赋值给session
变量。
完成了这些准备步骤后,你就可以开始执行 MongoDB 事务了。可以通过调用会话对象的 WithTransaction
方法,在一个事务中执行多个操作,并根据需要对事务进行提交、中止或回滚。
请注意,事务的使用需要满足一些前提条件,如使用复制集(replica set)或副本集群(sharded cluster),并且服务器版本较新(MongoDB 4.0+)。另外,事务操作还需要在同一会话中执行,因此需要确保会话对象在事务过程中保持活动状态。
python
import pymongo
client = pymongo.MongoClient()
collection = client.get_database().get_collection()
def call_back(ctx: pymongo.ContextManager):
print(collection.find_one())
with client.start_session() as session:
session.with_transaction(call_back)
开发中遇到的问题
1. 对于文本字段,普通索引和text索引的优劣对比
在MongoDB中,对一个普通的字符串字段添加正序或者倒序索引可以对查询性能产生显著的影响,具体取决于你的查询模式以及数据的访问模式。
-
正序索引:
- 正序索引(ascending index)是按照字典顺序存储数据,从小到大排列。在字符串字段上创建正序索引,可以加速按照该字段进行的升序排序查询以及范围查询(例如,大于、小于、介于等)。
- 示例:如果你经常对字符串字段进行按照字母顺序排序的查询,那么为该字段创建正序索引会提高查询性能。
-
倒序索引:
- 倒序索引(descending index)是按照字典逆序存储数据,从大到小排列。在字符串字段上创建倒序索引,可以加速按照该字段进行的降序排序查询以及范围查询。
- 示例:如果你经常对字符串字段进行按照字母逆序排序的查询,那么为该字段创建倒序索引会提高查询性能。
需要注意的是:
-
选择索引类型:应该根据实际查询需求来选择是正序索引还是倒序索引,以确保索引能够被有效利用。
-
多字段索引:如果你经常在查询中使用多个字段,可能需要考虑创建复合索引,这样可以更好地支持这些查询。
-
内存和磁盘:索引会占用额外的存储空间,并且会影响写操作的性能。在设计索引时,需要权衡查询性能和存储成本。
-
动态更新:如果字段的值会经常变动,那么索引的维护成本可能会变得很高。
总的来说,对于一个普通的字符串字段,根据实际需求和查询模式,添加正序或者倒序索引可以显著提升相应查询的性能。
当我们谈论“字母顺序排序”和“字典逆序”,我们实际上在讨论如何对字符串进行排序。
1. **字母顺序排序**:
- 字母顺序排序是按照字母表的顺序进行排序,从字母表的第一个字母开始,依次排列下去。对于英文字母,这通常就是按照 A、B、C...Z 的顺序排列。
- 举例:
如果有一组字符串 ["apple", "banana", "cherry"],按照字母顺序排序后的结果会是 ["apple", "banana", "cherry"]。
2. **字典逆序**:
- 字典逆序是按照字母表的反向顺序进行排序,从字母表的最后一个字母开始,逆序排列。对于英文字母,这就是按照 Z、Y、X...A 的顺序排列。
- 举例:
如果有一组字符串 ["apple", "banana", "cherry"],按照字典逆序排序后的结果会是 ["cherry", "banana", "apple"]。
这两种排序方式在处理字符串时可以产生不同的结果。具体选择哪种方式取决于你的需求以及应用场景。
举例:
假设有以下一组字符串:
["cat", "dog", "apple", "zebra", "banana"]
- 字母顺序排序后的结果:["apple", "banana", "cat", "dog", "zebra"]
- 字典逆序排序后的结果:["zebra", "dog", "cat", "banana", "apple"]
这些排序方式在不同情况下可能会产生不同的影响,例如在数据库中对字符串字段进行排序或者在编程中进行处理等场景。
在MongoDB中,选择使用text索引还是普通索引取决于你对该字段进行的操作以及查询需求:
-
使用Text索引的情况:
-
全文搜索:当你需要在文本字段中进行全文搜索,找到包含某个关键词的文档时,应该使用text索引。Text索引支持自然语言的搜索,可以进行模糊匹配、词干处理等操作。
-
处理自然语言文本:如果你处理的数据主要是自然语言文本(例如文章、评论等),并且需要进行文本搜索或者分析,那么text索引是一个好的选择。
-
不区分大小写和分词功能:Text索引会进行分词处理,允许你在搜索时忽略大小写。
-
支持文本相关的查询操作:例如
$text
操作符,它可以用于执行文本搜索。 -
支持文本相关的分值(score)计算:text索引可以为文档的匹配度打分,这对于排序返回结果非常有用。
-
-
使用普通索引的情况:
-
精确匹配:如果你需要对字段进行精确匹配的查询,例如根据用户名、产品编号等进行查询,普通索引是更适合的选择。
-
排序和范围查询:如果你需要对字段进行排序或者进行范围查询(例如大于、小于、介于等),普通索引通常更有效。
-
非文本类型的字段:对于非文本类型的字段(例如数字、日期等),text索引不适用,应该使用普通索引。
-
总的来说,如果你的应用需要进行全文搜索或者对自然语言文本进行处理,那么应该考虑使用text索引。如果你主要进行精确匹配、排序和范围查询等操作,那么普通索引会更适合。需要根据具体的业务需求和数据访问模式来选择合适的索引类型。
2. Mongodb 对于Sort排序能够支持的最大内存限制查看和修改
线上服务的MongoDB中有一个很大的表,查询时使用了sort()根据某个字段进行排序,结果报了下面这个错误:
[Error] Executor error during find command :: caused by :: Sort operation used more than the maximum 33554432 bytes of RAM. Add an index, or specify a smaller limit.
at line 0, column 0
这是个非常常见的MongoDB报错了。因为MongoDB处理排序时,如果排序的字段没有建立索引,会把全表都丢到内存中处理。
而内存的大小并不是无限使用的,MongoDB的默认设置是32MB。一旦数据量超过32MB,则会报错。
32MB这个限制是在参数internalQueryExecMaxBlockingSortBytes中控制。可以在MongoDB的客户端上直接查看这个参数的值,执行以下语句:
db.runCommand({
getParameter: 1,
"internalQueryExecMaxBlockingSortBytes": 1
})
返回如下结果:
{
"internalQueryExecMaxBlockingSortBytes": NumberInt("33554432"),
"ok": 1,
"operationTime": Timestamp(1651142670, 1),
"$clusterTime": {
"clusterTime": Timestamp(1651142670, 1),
"signature": {
"hash": BinData(0, "X09M2FBji5f+FOwaK/nLTv4+Ybs="),
"keyId": NumberLong("7080087363631710209")
}
}
}
所以解决排序时内存使用超过32MB的问题,有两个方法:
-
给排序的字段加索引。
-
修改internalQueryExecMaxBlockingSortBytes参数的大小,使用命令如下:
db.adminCommand({ setParameter: 1, internalQueryExecMaxBlockingSortBytes: 104857600 })
如果执行命令时报错:
MongoDB的官方网站上的两个相关JIRA:
第一个JIRA [SERVER-44053] Rename setParameter for maximum memory usage of blocking sort - MongoDB Jira里表示,在4.3.1版本时,因为参数命名描述不清楚,所以将参数internalQueryExecMaxBlockingSortBytes改为了internalQueryMaxBlockingSortMemoryUsageBytes。
第二个JIRA [SERVER-50767] internalQueryExecMaxBlockingSortBytes causing config exception on mongod load - Mongo中,Comments里提到了,新的internalQueryMaxBlockingSortMemoryUsageBytes参数,默认值从32MB改成了100MB。
所以在4.3以上的版本,执行以下命令:
db.runCommand({
getParameter: 1,
"internalQueryMaxBlockingSortMemoryUsageBytes": 1
})
可以看到查询结果:
{
"internalQueryMaxBlockingSortMemoryUsageBytes": NumberInt("104857600"),
"ok": 1
}
执行命令设置排序使用的内存大小,改为1G:
db.adminCommand({
setParameter:1,
"internalQueryMaxBlockingSortMemoryUsageBytes":1048576000
})
如果要建立排序字段索引也要注意以下几点:
- 查询时参与排序的多个字段的顺序,要和创建的索引每个字段的顺序保持一致。比如你创建的索引是:db.bigMongoTable.createIndex({“A”:1,“B”:1,“C”:1});那么你的排序语句也要按照顺序如下:sort({“A”:1,“B”:1,“C”:1})。如果你调换A和B的顺序,如下:sort({“B”:1,“A”:1,“C”:1}),则索引不会生效。
- 参与查询的字段少于索引的字段,则要保证符合前缀匹配。还是第一点里的索引,如果排序语句是这样:sort({“A”:1,“B”:1}),则索引继续生效。如果是这样:sort({“A”:1,“C”:1}),则无法生效。这个可以理解成和MySQL类似,索引都是按照最左匹配规则去触发的,一条索引的中间部分跳过了就无效了。
- 参与sort的字段的排序方式,要和创建索引时的排序方式保持完全一致,或者完全相反。对于第一点里的索引,如果查询sort({“A”:-1,“B”:1})或者sort({“A”:1,“B”:-1}),索引则不会生效。只有在查询sort({“A”:1,“B”:1})或者sort({“A”:-1,“B”:-1})时,索引才会生效。
3. 如何获取东八区时间
// 获取客户端本地时间
var localTime = new Date();
// 调整时区偏移量为东八区
var east8Time = new Date(localTime.getTime() + (8 * 60 * 60 * 1000));
// 输出东八区时间
// date类型
print(east8Time);
4. 点表示法(dot notation)
点表示法(dot notation)和$elemMatch
操作符在MongoDB中都用于查询嵌套数组中的元素,但它们在行为上有一些区别。
点表示法
用于直接访问嵌套文档或数组中的字段。当使用点表示法查询数组中的嵌套字段时,MongoDB会返回包含至少一个数组元素匹配指定查询条件的所有文档。
例如,考虑以下查询:
db.collection.find({"f1.f2.name": "xxx"})
这将返回所有在f1数组的任意元素中包含f2数组,且f2数组中至少有一个对象其name字段为xxx的文档。
$elemMatch操作符
用于在数组中匹配至少包含一个满足所有指定查询条件的元素的文档。当需要在同一个数组元素内部匹配多个条件时,$elemMatch
是必需的。
例如,如果想要查询f1数组中包含一个f2数组,且f2数组中有一个对象同时满足多个条件(比如name为xxx且age为20),可以使用$elemMatch:
db.collection.find({
"f1": {
"$elemMatch": {
"f2": {
"$elemMatch": {
"name": "xxx",
"age": 20
}
}
}
}
})
这将返回f1数组中至少有一个f2数组的元素,且这个f2数组中至少有一个对象同时满足name为xxx和age为20的条件。
总结:
- 点表示法用于访问嵌套文档或数组中的字段,适用于简单的查询,当只需要匹配数组中任意元素的单个字段时。
**$elemMatch**
用于在数组中匹配复杂条件,当需要在同一个数组元素中匹配多个字段时。
5. array_filters操作
arrayFilters
是 MongoDB 中的一项功能,它允许在更新嵌套数组文档时有选择地更新符合特定条件的数组元素。这在处理复杂的文档结构时非常有用,尤其是在只希望更新数组中部分元素而不是整个数组时。
基本概念
通常情况下,在 MongoDB 中更新数组时,我们可以使用位置操作符(如 $
)来匹配数组中的第一个符合条件的元素。然而,如果需要更新多个或特定条件的元素时,单纯依靠位置操作符可能不够灵活。arrayFilters
通过允许我们定义一个或多个过滤器来实现对数组元素的精确控制。
语法
db.collection.updateOne(
{ <query filter> },
{ <update operator>: { "<array>.$[<identifier>]": <value> }, ... },
{ arrayFilters: [ <filter condition>, ... ] }
)
关键组件
-
<query filter>
: 用于选择要更新的文档的查询条件。 -
<update operator>
: MongoDB 的更新操作符,如$set
,$unset
,$push
等,用于指定更新操作。 -
<array>.$[<identifier>]
:<array>
是要更新的数组字段名,$[<identifier>]
是表示匹配的占位符。 -
arrayFilters
: 一个数组,包含一个或多个条件,用于过滤数组中的元素。
示例
假设我们有如下文档结构:
{
"_id": 1,
"grades": [
{ "grade": 85, "mean": 75, "std": 8 },
{ "grade": 90, "mean": 75, "std": 8 },
{ "grade": 88, "mean": 75, "std": 8 }
]
}
我们希望将 grades
数组中 grade
大于 87
的元素的 std
字段更新为 10
。可以使用 arrayFilters
来实现:
db.collection.updateOne(
{ _id: 1 },
{ $set: { "grades.$[elem].std": 10 } },
{ arrayFilters: [ { "elem.grade": { $gt: 87 } } ] }
)
执行后,文档会更新为:
{
"_id": 1,
"grades": [
{ "grade": 85, "mean": 75, "std": 8 },
{ "grade": 90, "mean": 75, "std": 10 },
{ "grade": 88, "mean": 75, "std": 10 }
]
}
arrayFilters
的更多应用场景
-
更新多个匹配条件的数组元素: 通过定义多个过滤条件,更新多个不同条件的数组元素。
-
结合多个更新操作符: 可以在同一个更新操作中结合使用
$set
、$unset
、$inc
等,针对不同条件的数组元素应用不同的操作。 -
嵌套数组:
arrayFilters
也可以用于嵌套数组的更新,通过指定不同层次的过滤条件来实现。
注意事项
arrayFilters
是 MongoDB 3.6 及更高版本中引入的功能,确保使用的 MongoDB 版本支持该特性。- 过滤条件中的字段名必须与文档结构中的字段名精确匹配。
- 如果没有数组元素匹配
arrayFilters
的条件,更新操作将不会对文档进行任何更改。
通过 arrayFilters
,你可以在处理复杂文档时更灵活、更精确地控制更新操作,避免不必要的全局性修改。
更多文档
请参考: