1.MongoDB介绍
1.1 什么是MongoDB
MongoDB是一个文档数据库(以JSON 为数据模型),由C++语言编写,旨在为WEB应用提供可扩展的高性能数据存储解决方案。
文档来自于"JSON Document",并非我们一般理解的 PDF,WORD文档。
MongoDB是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的。它支持的数据结构非常松散,数据格式是BSON,一种类似ISON的二进制形式的存储格式,简称BinaryJSON,和JSON一样支持内嵌的文档对象和数组对象,因此可以存储比较复杂的数据类型。MongoDB最大的特点是它支持的查询语言非常强大,其语法有点类似于面向对象的查询语言,几乎可以实现类似关系数据库单表查询的绝大部分功能,而且还支持对数据建立索引。原则上 Oracle 和 MySQL 能做的事情,MongoDB 都能做(包括 ACID事务)。
MongoDB在数据库总排名第5,仅次于Oracle、MySQL等RDBMS,在NoSQL数据库排名首位。从诞生以来,其项目应用广度、社区活跃指数持续上升。
MongoDB概念与关系型数据库(RDBMS非常类似)
SQL概念 | MongoDB概念 |
---|---|
数据库(database) | 数据库(database) |
表(table) | 集合(collection) |
行(row) | 文档(document) |
列(column) | 字段(field) |
索引(index) | 索引(index) |
主键(primary key) | _id(字段) |
视图(view) | 视图(view) |
表连接(table joins) | 聚合操作($lookup) |
- 数据库(database):最外层的概念,可以理解为逻辑上的名称空间,一个数据库包含多个不同名称的集合。
- 集合(collection):相当于SQL中的表,一个集合可以存放多个不同的文档。
- 文档(document):一个文档相当于数据表中的一行,由多个不同的字段组成。
- 字段(field):文档中的一个属性,等同于列(column)。
- 索引(index):独立的检索式数据结构,与SQL概念一致。
- id:每个文档中都拥有一个唯一的id字段,相当于SQL中的主键(primary key)。
- 视图(view):可以看作一种虚拟的(非真实存在的)集合,与SQL中的视图类似。从MongoDB 3.4版本开始提供了视图功能,其通过聚合管道技术实现。
- 聚合操作($lookup):MongoDB用于实现 “类似” 表连接(tablejoin)的聚合操作符。
尽管这些概念大多与SQL标准定义类似,但MongoDB与传统RDBMS仍然存在不少差异,包括:
- 半结构化,在一个集合中,文档所拥有的字段并不需要是相同的,而且也不需要对所用的字段进行声明。因此,MongDB具有很明显的半结构化特点。除了松散的表结构,文档还可以支持多级的嵌套、数组等灵活的数据类型,非常契合面向对象的编程模型。
- 弱关系,MongoDB没有外键的约束,也没有非常强大的表连接能力。类似的功能需要使用聚合管道技术来弥补。
MongoDB | 关系型数据库 | |
---|---|---|
亿级以上数据量 | 轻松支持 | 要努力一下,分库分表 |
灵活表结构 | 轻松支持 | Entity Key/Value ,关联查询比较痛苦 |
高并发读 | 轻松支持 | 需要优化 |
高并发写 | 轻松支持 | 需要优化 |
跨地区集群 | 轻松支持 | 需要定制方案 |
分片集群 | 轻松支持 | 需要中间件 |
地理位置查询 | 比较完整的地理位置 | PG还可以,其他数据库略麻烦 |
聚合计算 | 功能很强大 | 使用Group By等,能力有限 |
异构数据 | 轻松支持 | 使用EKV属性表 |
大宽表 | 轻松支持 | 性能受限 |
1.2 MongoDB技术优势
MongoDB基于灵活的SON文栏模型,非常适合敏捷式的快速开发。与此同时,其与生俱来的高可用、高水平扩展能力使得它在处理海量、高并发的数据应用时颇具优势。
- JSON 结构和对象模型接近,开发代码量低
- JSON的动态模型意味着更容易响应新的业务需求
- 复制集提供99.999%高可用
- 分片架构支持海量数据和无缝扩容
简单直观: 从错综复杂的关系模型到一目了然的对象模型
快速: 最简单快速的开发方式
灵活: 快速响应业务变化
MongoDB优势:原生的高可用
MongoDB优势:横向扩展能力
1.3 MongoDB应用场景
从目前阿里云 MongoDB 云数据库上的用户看,MongoDB的应用已经渗透到各个领域:
-
游戏场景,使用 MongoDB 存储游戏用户信息,用户的装备、积分等直接以内嵌文档的形式存储,方便查询、更新;
-
物流场景,使用MongoDB 存储订单信息,订单状态在运送过程中会不断更新,以MongoDB 内嵌数组的形式来存储,一次查询就能将订单所有的变更读取出来;
-
社交场景,使用 MongoDB 存储存储用户信息,以及用户发表的朋友圈信息,通过地理位置索引实现附近的人、地点等功能;
-
物联网场景,使用MongoDB 存储所有接入的智能设备信息,以及设备汇报的日志信息,并对这些信息进行多维度的分析;
-
视频直播,使用 MongoDB 存储用户信息、礼物信息等;
-
大数据应用,使用云数据库MongoDB作为大数据的云存储系统,随时进行数据提取分析,掌握行业动态。
国内外知名互联网公司都在使用MongoDB:
如何考虑是否选择MongoDB?
没有某个业务场景必须要使用MongoDB才能解决,但使用MongoDB通常能让你以更低的成本解决问题。如果你不清楚当前业务是否适合使用MongoDB,可以通过做几道选择题来辅助决策。
应用特征 | Yes/No |
---|---|
应用不需要复杂/长事务及 join 支持 | 必须Yes |
新应用,需求会变,数据模型无法确定,想快速迭代开发 | ? |
应用需要2000-3000以上的读写QPS(更高也可以 | ? |
应用需要 TB 甚至 PB 级别数据存储 | ? |
应用发展迅速,需要能快速水平扩展 | ? |
应用要求存储的数据不丢失 | ? |
应用需要99.999%高可用 | ? |
应用需要大量的地理位置查询、文本查询 | ? |
只要有一项需求满足就可以考虑使用MongoDB,匹配越多,选择MongoDB越合适。
2.MongoDB快速开始
2.1 Linux安装MongoDB
环境准备
- Linux系统: centos7
- 安装MongoDB社区版
下载MongoDB Community Server
下载地址: https://www.mongodb.com/try/download/community
#下载MongoDB
wget https://fastdl.mongodb.org/osx/mongodb-macos-x86_64-4.4.28.tgz
tar -zxvf mongodb-macos-x86_64-4.4.28.tgz
vim ~/.zshrc# 内容BEGIN
export PATH=/Users/zhinian/Desktop/tools/mongodb/bin:$PATH
# 内容ENDsource ~/.zshrc
./mongod --helpOptions:--networkMessageCompressors arg (=snappy,zstd,zlib)Comma-separated list of compressors to use for network messagesGeneral options:-h [ --help ] Show this usage information--version Show version information-f [ --config ] arg Configuration file specifying additional options--configExpand arg Process expansion directives in config file (none, exec, rest)--port arg Specify port number - 27017 by default--ipv6 Enable IPv6 support (disabled by default)--listenBacklog arg (=128) Set socket listen backlog size--maxConns arg (=1000000) Max number of simultaneous connections--pidfilepath arg Full path to pidfile (if not set, no pidfile is created)--timeZoneInfo arg Full path to time zone info directory, e.g. /usr/share/zoneinfo--nounixsocket Disable listening on unix sockets--unixSocketPrefix arg Alternative directory for UNIX domain sockets (defaults to /tmp)--filePermissions arg Permissions to set on UNIX domain socket file - 0700 by default--fork Fork server process-v [ --verbose ] [=arg(=v)] Be more verbose (include multiple timesfor more verbosity e.g. -vvvvv)--quiet Quieter output--logpath arg Log file to send write to instead of stdout - has to be a file, not directory--syslog Log to system's syslog facility insteadof file or stdout--syslogFacility arg syslog facility used for mongodb syslogmessage--logappend Append to logpath instead of over-writing--logRotate arg Set the log rotation behavior (rename|reopen)--timeStampFormat arg Desired format for timestamps in log messages. One of iso8601-utc or iso8601-local--setParameter arg Set a configurable parameter--bind_ip arg Comma separated list of ip addresses tolisten on - localhost by default--bind_ip_all Bind to all ip addresses--noauth Run without security--transitionToAuth For rolling access control upgrade. Attempt to authenticate over outgoing connections and proceed regardless of success. Accept incoming connections with or without authentication.--slowms arg (=100) Value of slow for profile and console log--slowOpSampleRate arg (=1) Fraction of slow ops to include in the profile and console log--profileFilter arg Query predicate to control which operations are logged and profiled--auth Run with security--clusterIpSourceWhitelist arg Network CIDR specification of permittedorigin for `__system` access--profile arg 0=off 1=slow, 2=all--cpu Periodically show cpu and iowait utilization--sysinfo Print some diagnostic system information--noscripting Disable scripting engine--notablescan Do not allow table scans--keyFile arg Private key for cluster authentication--clusterAuthMode arg Authentication mode used for cluster authentication. Alternatives are (keyFile|sendKeyFile|sendX509|x509)Replication options:--oplogSize arg Size to use (in MB) for replication op log. default is 5% of disk space (i.e. large is good)Replica set options:--replSet arg arg is <setname>[/<optionalseedhostlist>]--enableMajorityReadConcern [=arg(=1)] (=1)Enables majority readConcernSharding options:--configsvr Declare this is a config db of a cluster; default port 27019; default dir /data/configdb--shardsvr Declare this is a shard db of a cluster; default port 27018Storage options:--storageEngine arg What storage engine to use - defaults to wiredTiger if no data files present--dbpath arg Directory for datafiles - defaults to /data/db--directoryperdb Each database will be stored in a separate directory--syncdelay arg (=60) Seconds between disk syncs--journalCommitInterval arg (=100) how often to group/batch commit (ms)--upgrade Upgrade db if needed--repair Run repair on all dbs--journal Enable journaling--nojournal Disable journaling (journaling is on bydefault for 64 bit)--oplogMinRetentionHours arg (=0) Minimum number of hours to preserve in the oplog. Default is 0 (turned off). Fractions are allowed (e.g. 1.5 hours)TLS Options:--tlsOnNormalPorts Use TLS on configured ports--tlsMode arg Set the TLS operation mode (disabled|allowTLS|preferTLS|requireTLS)--tlsCertificateKeyFile arg Certificate and key file for TLS--tlsCertificateKeyFilePassword arg Password to unlock key in the TLS certificate key file--tlsClusterFile arg Key file for internal TLS authentication--tlsClusterPassword arg Internal authentication key file password--tlsCAFile arg Certificate Authority file for TLS--tlsClusterCAFile arg CA used for verifying remotes during inbound connections--tlsCRLFile arg Certificate Revocation List file for TLS--tlsDisabledProtocols arg Comma separated list of TLS protocols to disable [TLS1_0,TLS1_1,TLS1_2]--tlsAllowConnectionsWithoutCertificates Allow client to connect without presenting a certificate--tlsAllowInvalidHostnames Allow server certificates to provide non-matching hostnames--tlsAllowInvalidCertificates Allow connections to servers with invalid certificates--tlsFIPSMode Activate FIPS 140-2 mode at startup--tlsCertificateSelector arg TLS Certificate in system store--tlsClusterCertificateSelector arg SSL/TLS Certificate in system store forinternal TLS authentication--tlsLogVersions arg Comma separated list of TLS protocols to log on connect [TLS1_0,TLS1_1,TLS1_2]AWS IAM Options:--awsIamSessionToken arg AWS Session Token for temporary credentialsWiredTiger options:--wiredTigerCacheSizeGB arg Maximum amount of memory to allocate for cache; Defaults to 1/2 of physical RAM--wiredTigerJournalCompressor arg (=snappy)Use a compressor for log records [none|snappy|zlib|zstd]--wiredTigerDirectoryForIndexes Put indexes and data in different directories--wiredTigerCollectionBlockCompressor arg (=snappy)Block compression algorithm for collection data [none|snappy|zlib|zstd]--wiredTigerIndexPrefixCompression arg (=1)Use prefix compression on row-store leaf pages
启动MongoDB Server
#创建dbpath和logpath
mkdir ‐p ~/Desktop/tools/mongodb-macos-x86_64-4.4.28/data ~/Desktop/tools/mongodb-macos-x86_64-4.4.28/log
#进入mongodb目录,启动mongodb服务
mongod --logpath="/Users/zhinian/Desktop/tools/mongodb/log/mongodb.log" --bind_ip="0.0.0.0" --dbpath="/Users/zhinian/Desktop/tools/mongodb/data" --port=27017 --fork# 以下为输出, 不是命令
about to fork child process, waiting until server is ready for connections.
forked process: 50386
child process started successfully, parent exiting
zhinian@192 ~ % mongo
MongoDB shell version v4.4.28
connecting to: mongodb://127.0.0.1:27017/?compressors=disabled&gssapiServiceName=mongodb
Implicit session: session { "id" : UUID("d68a44f7-6a0f-4b7e-9984-fe6954a37527") }
MongoDB server version: 4.4.28
Welcome to the MongoDB shell.
For interactive help, type "help".
For more comprehensive documentation, seehttps://docs.mongodb.com/
Questions? Try the MongoDB Developer Community Forumshttps://community.mongodb.com
---
The server generated these startup warnings when booting: 2024-01-27T14:32:34.658+08:00: Access control is not enabled for the database. Read and write access to data and configuration is unrestricted2024-01-27T14:32:34.659+08:00: Soft rlimits too low2024-01-27T14:32:34.659+08:00: currentValue: 25602024-01-27T14:32:34.659+08:00: recommendedMinimum: 64000
---
> show dbs
admin 0.000GB
config 0.000GB
local 0.000GB
–dbpath:指定数据文件存放目录
–logpath:指定日志文件,注意是指定文件不是目录
–logappend:使用追加的方式记录日志
–port:指定端口,默认为27017
–bind_ip:默认只监听localhost网卡
–fork:后台启动
–auth:开启认证模式
利用配置文件启动服务
编辑/mongodb/conf/mongo.conf文件,内容如下:
vim ~/Desktop/tools/mongodb/conf/mongo.conf
systemLog:destination: filepath: /Users/zhinian/Desktop/tools/mongodb/log/mongod.10g # log pathlogAppend: true
storage:dbPath: /Users/zhinian/Desktop/tools/mongodb/data # data directoryengine: wiredTiger #存储引擎journal: #是否启用journa1日志enabled: true
net:bindIp: 0.0.0.0port: 27017 # port
processManagement:fork: true # fork and run in backgroundshutdown: true # Add this line to shutdown the database
vim bins/mongodb
# 内容BEGIN
mongod -f /Users/zhinian/Desktop/tools/mongodb/conf/mongo.conf
# 内容END
chmod +x bins/mongodb
# 仅需要输入mongodb即可启动
zhinian@192 ~ % mongodb
about to fork child process, waiting until server is ready for connections.
forked process: 51981
child process started successfully, parent exiting
注意:一定要yaml格式
后动mongod
mongod -f/mongodb/conf/mongo.conf
关闭MongoDB服务
方式一
## 新版本已没有shutdown命令
mongod --port=27017 --dbpath=/Users/zhinian/Desktop/tools/mongodb/data --shutdownError parsing command line: unrecognised option '--shutdown'
try 'mongod --help' for more information
方式二
进入mongo shell
use admin
db.shutdownServer()
2.2 Mongo Shell 使用
mongo是MongoDB的交互式JavaScript Shell界面,它为系统管理员提供了强大的界面,并为开发人员提供了直接测试数据库查询和操作的方法。
mongo --port=27017
–port:指定端口,默认为27017
–host:连接的主机地址,默认127.0.0.1
Javascript支持
mongo shell是基于Javascript语法的,MongoDB使用了SpiderMonkey作为其内部的Javascript解释器引擎,这是由Mozilla官方提供的Javascript内核解释器,该解释器也被同样用于大名鼎鼎的Firefox浏览器产品之中。SpiderMonkey对ECMA Script标准兼容性非常好,以支持ECMA Script6。可以通过下面的命令检查JavaScript解释器的版本:
Mongo Shell常用命令
命令 | 说明 |
---|---|
show dbs| Show databases | 显示数据库列表 |
use 数据库名 | 切换数据库,如果不存在创建数据库 |
db.dropDatabase() | 删除数据库 |
show collections| show tables | 显示当前数据库的集合列表 |
db.集合名.stats() | 查看集合详情 |
db.集合名.drop() | 删除集合 |
show users | 显示当前数据库的用户列表 |
show roles | 显示当前数据库的角色列表 |
show profile | 显示最近发生的操作 |
load("xxx.js") | 执行一个JavaScript脚本文件 |
exit | quit() | 退出当前shell |
help | 查看mongodb支持哪些命令 |
db.help() | 查询当前数据库支持的方法 |
db.集合名.help() | 显示集合的帮助信息 |
db.version() | 查看数据库版本 |
数据库操作
# 查看所有库
show dbs
# 切换数据库,如果不存在创建数据库
use test
# 删除当前数据库
db.dropDatabase()
集合操作
# 查看集合
show collections
# 创建集合
db.createCollection("emp")
# 删除集合
db.emp.drop()
创建集合语法
db.createCollection(name, options)
options参数
字段 | 类型 | 描述 |
---|---|---|
capped | 布尔 | (可选)如果为true,则创建固定集合。固定集合是指有着固定大小的集合,当达到最大值时,它会自动覆盖最早的文档。 |
size | 数值 | (可选)为固定集合指定一个最大值(以字节计)。如果capped 为true,也需要指定该字段。 |
max | 数值 | (可选)指定固定集合中包含文档的最大数量。 |
注: 当集合不存在时,向集合中插入文档也会创建集合
2.3 安全认证
创建管理员账号
# 设置管理员用户名/密码需要切换到admin库
use admin
# 创建管理员
db.createUser({user: "fox", pwd: "fox", roles: ["root"]})
# 查看所有用户信息
show users
# 删除用户
db.dropUser("fox")
常用权限
权限名 | 描述 |
---|---|
read | 允许用户读取指定数据库 |
readWrite | 允许用户读写指定数据库 |
dbAdmin | 允许用户在指定数据库中执行管理函数,如索引创建、删除,查看统计或访问system.profile |
dbOwner | 允许用户在指定数据库中执行任意操作,增、删、改、查等 |
userAdmin | 允许用户向system.users集合写入,可以在指定数据库里创建、删除和管理用户 |
clusterAdmin | 只在admin数据库中可用,赋予用户所有分片和复制集相关函数的管理权限 |
readAnyDatabase | 只在admin数据库中可用,赋予用户所有数据库的读权限 |
readWriteAnyDatabase | 只在admin数据库中可用,赋予用户所有数据库的读写权限 |
userAdminAnyDatabase | 只在admin数据库中可用,赋予用户所有数据库的userAdmin权限 |
dbAdminAnyDatabase | 只在admin数据库中可用,赋予用户所有数据库的dbAdmin权限 |
root | 只在admin数据库中可用。超级账号,超级权限 |
用户认证,返回1表示认证成功
创建应用数据库用户
use appdb
db.createUser({user: "appdb", pwd: "fox", roles: ["dbOwner"]})
默认情况下,MongoDB不会启用鉴权,以鉴权模式启动MongoDB
mongod -f /Users/zhinian/Desktop/tools/mongodb/conf/mongo.conf --auth
启用鉴权之后,连接MongoDB的相关操作都需要提供身份认证
mongo 192.168.1.3:27017 -uappdb -pfox --authenticationDatabase=appdb
3. MongoDB文档操作
3.1 插入文档
3.2 版本之后新增了 db.collection.insertOne() 和 db.collection.insertMany()。
新增单个文档
- insertOne: 支持writeConcern
db.collection.insertOne(<document>,{writeConcern: <document>}
)
writeConcern 决定一个写操作落到多少个节点上才算成功。writeConcern 的取值包括:
0:发起写操作,不关心是否成功;
1-集群最大数据节点数:写操作需要被复制到指定节点数才算成功;
majority:写操作需要被复制到大多数节点上才算成功。
- insert:若插入的数据主键已经存在,则会抛 DuplicatekeyException 异常,提示主键重复,不保存当前数据。
- save:如果_jd 主键存在则更新数据,如果不存在就插入数据。
批量新增文档
- insertMany: 向指定集合中插入多条文档数据
db.collection.insertMany([ <document1>, <document2>, ... ],{writeConcern: <document>,ordered: <boolean>}
)
writeConcern: 写入策略,默认为1,即要求确认写操作,0是不要求。
ordered: 指定是否按顺序写入,默认true,按顺序写入。
- insert和save也可以实现批量插入
测试:批量插入50条随机数据
编辑脚本book.js
let tags = ["nosql", "mongodb", "document", "developer", "popular"];
let types = ["technology", "sociality", "travel", "novel", "literature"];
let books = [];
for(let i = 0; i < 50; i++) {let typeId = Math.floor(Math.random() * types.length);let tagId = Math.floor(Math.random() * tags.length);let favCount = Math.floor(Math.random() * 100);let book = {title: "book-"+i, type: types[typeId], tag: tags[tagId], favCount: favCount, author: "xxx"+i}books.push(book)
}
db.books.insertMany(books);
进入mongo shell,执行
load("book.js")
3.2 查询文档
find 查询集合中的若干文档。语法格式如下
db.collection.find(query, projection)
- query:可选,使用查询操作符指定查询条件
- projection:可选,使用投影操作符指定返回的键。查询时返回文档中所有键值,只需省略该参数即可(默认省略)。投影时,id为1的时候,其他字段必须是1;id是0的时候,其他字段可以是0;如果没有_id字段约束,多个其他字段必须同为0或同为1。
如果查询返回的条目数量较多,mongo shell则会自动实现分批显示。默认情况下每次只显示20条,可以输入it命令读取下一批。
findOne查询集合中的第一个文档。语法格式如下:
db.collection.findOne(query, projection)
条件查询
指定条件查询
# 查询带有nosql标签的book文档
db.books.find({tag: "nosql"})
# 按照id查询单个book文档
db.books.find({_id: ObjectId("65b4e219ebed3fc5e9d74276")})
# 查询分类为travel、收藏数超过60的book文档
db.books.find({type: "travel", favCount: {$gt: 60}})
查询条件对照表
SQL | MQL |
---|---|
a = 1 | ${a: 1} |
a <> 1 | {a: {$ne: 1}} |
a > 1 | {a: {$gt: 1}} |
a >= 1 | {a: {$ge: 1}} |
a < 1 | {a: {$lt: 1}} |
a <= 1 | {a: {$lte: 1}} |
查询逻辑对照表
SQL | MQL |
---|---|
a = 1 AND b = 1 | ${a: 1, b: 1}或{$and: [{a: 1}, {b: 1}]} |
a = 1 OR b = 1 | {$or: [{a: 1}, {b: 1}] |
a IS NULL | {a: {$exists: false}} |
a IN (1, 2, 3) | {a: {in: [1, 2, 3]}} |
查询逻辑运算符
-
$lt:存在并小于
-
$lte:存在并小于等于
-
$gt:存在并大于
-
$gte:存在并大于等于
-
$ne:不存在或存在但不等于
-
$in:存在并在指定数组中
-
$nin:不存在或不在指定数组中
-
$or:匹配两个或多个条件中的一个
-
$and:匹配全部条件
排序 & 分页
指定排序
在 MongoDB 中使用 sort() 方法对数据进行排序
# 指定按收藏数(favCount)降序返回
db.books.find({type: "travel"}).sort({favCount:-1})
分页查询
skip用于指定跳过记录数,limit则用于限定返回结果数量。可以在执行find命令的同时指定skip、limit参数,以此实现分页的功能。比如,假定每页大小为8条,查询第3页的book文档
db.books.find().skip(8).limit (4)
正则表达式匹配查询
MongoDB 使用$regex 操作符来设置匹配字符串的正则表达式。
-- 使用正则表达式查找type包含 so 字符串的book
db.books.find({type: {Sregex:"so"}})
-- 或者
db.books.find({type:/so/})
3.3 更新文档
可以用update命令对指定的数据进行更新,命令的格式如下:
db.collection.update(query, update, options)
- query:描述更新的查询条件;
- update:描述更新的动作及新的内容;
- options:描述更新的选项
- upsert: 可选,如果不存在update的记录,是否插入新的记录。默认false,不插入
- multi: 可选,是否按条件查询出的多条记录全部更新。默认false,只更新找到的第—条记录
- writeConcernk可选,决定一个写操作落到多少个节点上才算成功。
更新操作符
操作符 | 格式 | 描述 |
---|---|---|
$set | {$set:{field:value}} | 指定一个键并更新值,若键不存在则创建 |
$unset | {$unset: {field: 1}} | 删除一个键 |
$inc | {$inc: {field: value}} | 对数值类型进行增減 |
$rename | {$rename: {old_field_name: new_field_name}} | 修改字段名称 |
$push | {$push: {field: value}} | 将数值追加到数组中,若数组不存在则会进行初始化 |
$pushAll | {$pushAll: {field: value_array}} | 追加多个值到一个数组字段内 |
$pull | {$pull: {field: _value}} | 从数组中删除指定的元素 |
$addToSet | {$addToSet: {field: value}} | 添加元素到数组中,具有排重功能 |
$pop | {$pop: {field: 1}} | 删除数组的第一个或最后一个元素 |
更新单个文档
某个book文档被收藏了,则需要将该文档的favCount字段自增
db.books.update({_id: ObjectId("65b4e219ebed3fc5e9d74287")}, {$inc: {favCount: 1}})
更新多个文档
默认情况下,update命令只在更新第一个文档之后返回,如果需要更新多个文档,则可以使用multi选项。
将分类为 “novel” 的文档的增加发布时间(publishedDate)
db.books.update({type: "nosql"}, {$set: {publishedDate: new Date()}}, {"multi": true})
multi: 可选,mongodb 默认是 false,只更新找到的第一条记录,如果这个参数为 true,就把按条件查询出的多条记录全部更新
update命令的选项配置较多,为了简化使用还可以使用一些快捷命令:
- updateOne: 更新单个文档。
- updateMany: 更新多个文档。
- replaceOne: 替换单个文档。
使用upsert命令
upsert是一种特殊的更新,其表现为如果目标文档不存在,则执行插入命令。
db.books.update({title: "my book"},{$set: {tags: ["nosql", "mongodb"], type: "none", author: "fox"}},{upsert: true}
)
nMatched、nModified都为0,表示没有文档被匹配及更新,nUpserted=1提示执行了upsert操作
实现replace语义
update命令中的更新描述(update)通常由操作符描述,如果更新描述中不包含任何的操作符,那么MongoDB会实现文档的replace语义
db.books.update({title: "my book"},{justTitle: "my first book"}
)
findAndModify命令
findAndModify兼容了查询和修改指定文档的功能,
-- 将某个book文档的收藏数(favCount)加1
db.books.findAndModify({query: {_id: ObjectId("65b4e219ebed3fc5e9d74264")},update: {$inc: {favCount: 1}}
})
该操作会返回符合查询条件的文档数据,并完成对文档的修改。
默认情况下,findAndModify会返回修改前的"旧"数据。如果希望返回修改后的数据,则可以指定new选项
db.books.findAndModify({query: {_id: ObjectId("65b4e219ebed3fc5e9d74264")},update: {$inc: {favCount: 1}},new: true
})
与findAndModify语义相近的命令如下:
- findOneAndUpdate: 更新单个文档并返回更新前/或更新后的文档。
- findOneAndReplace: 替换单个文档并返回替换前/或替换后的文档。
3.4 删除文档
使用remove删除文档
- remove 命令需要配合查询条件使用;
- 匹配查询条件的文档会被删除;
- 指定一个空文档条件会删除所有文档;
示例:
db.user.remove({age: 28}) -- 删除 age 等于 28 的记录
db.user.remove({age: {$lt: 25}}) -- 删除 age 小于 25 的记录
db.user.remove({}) -- 删除所有记录
db.user.remove() -- 报错
remove命令会删除匹配条件的全部文档,如果希望明确限定只删除一个文档,则需要指定justOne参数,命令格式如下:
db.collection.remove(query, justone)
例如:删除满足type:novel条件的首条记录(加true删首条)
db.books.remove({type: "novel"}, true)
使用delete删除文档
官方推荐使用 deleteOne() 和 deleteMany() 方法删除文档,语法格式如下:
db.books.deleteMany({}) -- 删除集合下全部文档
db.books.deleteMany({type: "novel"}) -- 删除 type 等于 novel 的全部文档
db.books.deleteOne({type: "novel"}) -- 删除 type 等于 novel 的一个文档
注意:remove、deleteMany等命令需要对查询范围内的文档逐个删除,如果希望删除整个集合,则使用drop命令会更加高效
返回被删除文档
remove、deleteOne等命令在删除又档后只会返回确认性的信息,如果希望获得被删除的文档,则可以使用findOneAndDelete命令
db.books.findOneAndDelete({type: "novel"})
除了在结果中返回删除文档,findOneAndDelete命令还允许定义 “删除的顺序”,即按照指定顺序删除找到的第一个文档
db.books.findoneAndDelete({type: "nove1"}, {sort: {favcount:1}})
remove、deleteOne等命令能按默认顺序删除,利用这个特性,findOneAndDelete可以实现队列的先进先出。
4.MongoDB整合SpringBoot
4.1 环境准备
MongoDB Compass: https://www.mongodb.com/try/download/compass
1.引入依赖
<!-- Spring data mongodb -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
2.配置yml
spring:data:mongodb:uri: mongodb://fox:fox@192.168.1.3:27017/test?authSource=admin# uri等同于下面的配置# database: test# host: 192.168.1.3# port: 27017# username: fox# password: fox# authentication-database: admin
连接配置参考文档: https://docs.mongodb.com/manual/reference/connection-string/
3.使用时注入mongoTemplate
@Autowired
MongoTemplate mongoTemplate;
4.2 集合操作
/*** 创建集合*/
@Test
public void testCreateCollection() {final String collectionName = "emp";if (mongoTemplate.collectionExists(collectionName)) {mongoTemplate.dropCollection(collectionName);}mongoTemplate.createCollection(collectionName);
}
4.3 文档操作
相关注解
-
@Document
- 修饰范围: 用在类上
- 作用: 用来映射这个类的一个对象为mongo中一条文档数据。
- 属性: (value、collection)用来指定操作的集合名称
-
@ld
- 修饰范围: 用在成员变量、方法上
- 作用: 用来将成员变量的值映射为文档的_id的值
-
@Field
- 修饰范围: 用在成员变量、方法上
- 作用: 用来将成员变量及其值映射为文档中一个key:value对。
- 属性: (name,value)用来指定在文档中key的名称,默认为成员变量名
-
@Transient
-
修饰范围;用在成员变量、方法上
-
作用:用来指定此成员变量不参与文档的序列化
-
创建实体
@Data
@Document("emp") // 对应emp集合中的一个文档
@AllArgsConstructor
@NoArgsConstructor
public class Employee {@Id //映射文档中的_idprivate Long id;@Field("username")private String name;@Fieldprivate BigDecimal salary;@Fieldprivate Date birthday;
}
添加文档
insert方法返回值是新增的Document对象,里面包含了新增后id的值。如果集合不存在会自动创建集合。通过Spring Date MongoDB还会给集合中多加一个class的属性,存储新增时Document对应Java中类的全限定路径。这么做为了查询时能把Documnent转换力Java类型。
@Test
public void testInsert() {Employee emp = new Employee(1L, "小明", new BigDecimal("3010000.00"), new Date());// 添加文档// save: _id存在时更新数据// mongoTemplate.save(emp);// insert: _id存在时抛出异常, 支持批量操作mongoTemplate.insert(emp);List<Employee> list = Arrays.asList(new Employee(2L, "张三", new BigDecimal("2150000.00"), new Date()),new Employee(3L, "李四", new BigDecimal("2680000.00"), new Date()),new Employee(4L, "王五", new BigDecimal("2280000.00"), new Date()),new Employee(5L, "张龙", new BigDecimal("2860000.00"), new Date()),new Employee(6L, "赵虎", new BigDecimal("2470000.00"), new Date()),new Employee(7L, "赵六", new BigDecimal("2812000.00"), new Date()));// 插入多条数据mongoTemplate.insert(list, Employee.class);
}
查询文档
Criteria是标准查询的接口,可以引用静态的Criteria.where的把多个条件组合在一起,就可以轻松地将多个方法标准和查询连接起来,方便我们操作查询语句。
Criteria | Mongodb | 说明 |
---|---|---|
Criteria and (String key) | $and | 井且 |
Criteria andOperator (Criteria... criteria) | $and | 井且 |
Criteria orOperator (Criteria.. criteria) | $or | 或者 |
Criteria gt (Object o) | $gt | 大于 |
Criteria gte (Object o) | $gte | 大于等于 |
Criteria in (Object... o) | $in | 包含 |
Criteria is (Object o) | $is | 等于 |
Criteria lt (Object o) | $lt | 小于 |
Criteria lte (Object o) | $lte | 小于等于 |
Criteria nin (Object... o) | $nin | 不包含 |
@Test
public void testFind() {System.out.println("=============查询所有文档=============");// 查看所有文档List<Employee> list = mongoTemplate.findAll(Employee.class);list.forEach(System.out::println);System.out.println("==============根据id查询==============");// 根据id查询Employee emp = mongoTemplate.findById(1L, Employee.class);System.out.println(emp);System.out.println("==========findOne返回第一个文档==========");// 如果查询结果是多个, 返回其中第一个文档对象Employee one = mongoTemplate.findOne(new Query(), Employee.class);System.out.println(one);System.out.println("================条件查询================");// new Query() 表示没有条件// 查询薪资大于等于8000的员工// Query query = new Query(Criteria.where("salary").gte(8000.00));// 查询薪资大于4000小于10000的员工// Query query = new Query(Criteria.where("salary").gt(4000.00).lt(10000.00));// 正则查询(模糊查询) java中正则不需要有//// Query query = new Query(Criteria.where("name").regex("张"));// and or 多条件查询Criteria criteria = new Criteria();// and 查询年龄大于25&薪资大于8000的员工// criteria.andOperator(Criteria.where("age").gt(25), Criteria.where("salary").gt(8000.00));// or 查询姓名是张三或者薪资大于8000的员工criteria.orOperator(Criteria.where("name").is("张三"), Criteria.where("salary").gt(8000.00));Query query = new Query(criteria);// sort排序// query.with(Sort.by(Sort.Order.desc("salary")));// skip limit 分页 skip用于指定跳过记录数, limit 则用于限定返回结果数量。query.with(Sort.by(Sort.Order.desc("salary"))).skip(0) // 指定跳过记录数.limit(4); // 每页显示记录数// 查询结果List<Employee> employees = mongoTemplate.find(query, Employee.class);employees.forEach(System.out::println);
}
@Test
public void testFindByJSON() {// 使用JSON字符串方式查询// 等值查询// String json = "{name: '张三'}";// 多条件查询String json = "{$or: [{age: {$gt: 25}}, {salary: {$gte: 8000}}]}";Query query = new BasicQuery(json);// 查询结果List<Employee> employees = mongoTemplate.find(query, Employee.class);employees.forEach(System.out::println);
}
更新文档
在Mongodb中无论是使用客户端APi还是使用Spring Data,更新返回结果一定是受行数影响。如果更新后的结果和更新前的结果是相同,返旦0。
- updateFirst() 只更新满足条件的第一条记录
- updateMulti() 更新所有满足条件的记录
- upsert() 没有符合条件的记录则插入数据
@Test
public void testUpdate() {// query设置查询条件Query query = new Query(Criteria.where("salary").gte(10000));System.out.println("==========更新前===========");List<Employee> employees = mongoTemplate.find(query, Employee.class);employees.forEach(System.out::println);Update update = new Update();// 设置更新属性update.set("salary", 14000);// updateFirst() 只更新满足条件的第一条记录// UpdateResult updateResult = mongoTemplate.updateFirst(query, update, Employee.class);// updateMulti() 更新所有满足条件的记录// UpdateResult updateResult = mongoTemplate.updateMulti(query, update, Employee.class);// upsert() 没有符合条件的记录则插入数据update.setOnInsert("id", 11); // 指定_idUpdateResult updateResult = mongoTemplate.upsert(query, update, Employee.class);// 返口修改的记录数System.out.println(updateResult.getModifiedCount());System.out.println("===============更新后===============");employees = mongoTemplate.find(query, Employee.class);employees.forEach(System.out::println);
}
删除文档
@Test
public void testDelete() {// 删除所有文档// mongoTemplate.remove(new Query(), Employee.class);// 条件删除Query query = new Query(Criteria.where("salary").gte(10000));mongoTemplate.remove(query, Employee.class);
}
5.聚合操作
聚合操作处理数据记录并返回计算结果(诸如统计平均值,求和等)。聚合操作组值来自多个文档,可以对分组数据执行各种操作以返回单个结果。聚合操作包含三类:单一作用聚合、聚合管道、MapReduce。
- 单一作用聚合:提供了对常见聚合过程的简单访问,操作都从单个集合聚合文档。
- 聚合管道是一个数据聚合的框架,模型基于数据处理流水线的概念。文档进入多级管道,将文档转换为聚合结果。
- MapReduce操作具有两个阶段:处理每个文档并向每个输入文档发射一个或多个对象的map阶段,以及reduce组合map操作的输出阶段。
5.1 单一作用聚合
MongoDB提供 db.collection.estimatedDocumentCount(), db.collection.count(), db.collection.distinct()这类单一作用的聚合函数。所有这些操作都聚合来自单个集合的文档。虽然这些操作提供了对公共聚合过程的简单访问,但它们缺乏聚合管道和map-Reduce的灵活性和功能。
函数 | 描述 |
---|---|
db.collection.estimatedDocumentCount() | 忽略查询条件,返回集合或视图中所有文档的计数 |
db.collection.count() | 返回与find()集合或视图的查询匹配的文档计数。等同于db.collection.find(query).count() |
db.collection.distinct() | 在单个集合或视图中查找指定字段的不同值,并在数组中返回结果 |
# 检索books集合中所有文档的计数
db.books.estimatedDocumentCount()
# 计算与查询匹配的所有文档
db.books.count({favCount: {$gt: 50}})
# 返回不同的type的数组
db.books.distinct("type")
# 返回收藏数大于90的文档不同type的数组
db.collection.distinct("type", {favCount: {$gt: 90}})
注意:在分片集群上,如果存在孤立文档或正在进行块迁移,则 db.collection.count()
没有查询谓词可能导致计数不准确。要避免这些情况,请在分片集群上使用 db.collection.aggregate()
方法。
5.2 聚合管道
什么是MongoDB聚合框架
MongoDB 聚合框架(Aggregation Framework)是一个计算框架,它可以:
- 作用在一个或几个集合上;
- 对集合中的数据进行的一系列运算;
- 将这些数据转化为期望的形式;
从效果而言,聚合框架相当于 SQL 查询中的GROUP BY、LEFT OUTER JOIN、AS等。
管道(Pipeline)和阶段(Stage)
整个聚合运算过程称为管道(Pipeline),它是由多个阶段(Stage)组成的,每个管道:
- 接受一系列文档(原始数据);
- 每个阶段对这些文档进行一系列运算;
- 结果文档输出给下一个阶段;
聚合管道操作语法
pipeline = [$stage1, $stage2, ...$stageN];
db.collection.aggregate(pipeline, {options})
- pipelines 一组数据聚合阶段。除 o u t 、 out、 out、Merge和$geonear阶段之外,每个阶段都可以在管道中出现多次。
- options 可选,聚合操作的其他参数。包含:查询计划、是否使用临时文件、 游标、最大操作时间、读写策略、强制索引等等
常用的管道聚合阶段
聚合管道包含非常丰富的聚合阶段,下面是最常用的聚合阶段
阶段 | 描述 | SQL等价运算符 |
---|---|---|
$match | 筛选条件 | WHERE |
$project | 投影 | AS |
$lookup | 左外连接 | LEFT OUTER JOIN |
$sort | 排序 | ORDER BY |
$group | 分组 | ORDER BY |
$skip/$limit | 分页 | |
$unwind | 展开数组 | |
$graphLookup | 图搜索 | |
$facet/$bucket | 分面搜索 |
文档:Aggregation Pipeline Stages — MongoDB Manual
数据准备
准备数据集,执行脚本
let tags = ["nosql", "mongodb", "document", "developer", "popular"];
let types = ["technology", "sociality", "travel", "novel", "literature"];
let books=[];
for(let i=0; i<50; i++){let typeIdx = Math.floor(Math.random() * types.length);let tagIdx = Math.floor(Math.random() * tags.length);let tagIdx2 = Math.floor(Math.random() * tags.length);let favCount = Math.floor(Math.random() * 100);let username = "xx00" + Math.floor(Math.random() * 10);let age = 20 + Math.floor(Math.random() * 15);let book = {title: "book-" + i,type: types[typeIdx],tag: [tags[tagIdx], tags[tagIdx2]],favCount: favCount,author: {name:username, age:age}};books.push(book)
}
db.books.insertMany(books);
$project
投影操作,将原始字段投影成指定名称,如将集合中的 title 投影成 name
db.books.aggregate([{$project:{name:"$title"}}])
$project 可以灵活控制输出文档的格式,也可以剔除不需要的字段
db.books.aggregate([{$project:{name:"$title",_id:0,type:1,author:1}}])
从嵌套文档中排除字段
db.books.aggregate([{$project:{name:"$title",_id:0,type:1,"author.name":1}}
])
或者
db.books.aggregate([{$project:{name:"$title",_id:0,type:1,author:{name:1}}}
])
$match
$match用于对文档进行筛选,之后可以在得到的文档子集上做聚合,$match可以使用除了地理空间之外的所有常规查询操作符,在实际应用中尽可能将$match放在管道的前面位置。这样有两个好处:一是可以快速将不需要的文档过滤掉,以减少管道的工作量;二是如果再投射和分组之前执行$match,查询可以使用索引。
db.books.aggregate([{$match:{type:"technology"}}])
筛选管道操作和其他管道操作配合时候时,尽量放到开始阶段,这样可以减少后续管道操作符要操作的文档数,提升效率
db.books.aggregate([{$match:{type:"technology"}},{$project:{name:"$title",_id:0,type:1,author:{name:1}}}
])
$count
计数并返回与查询匹配的结果数
db.books.aggregate([{$match:{type:"technology"}},{$count: "type_count"}
])
$match阶段筛选出type匹配technology的文档,并传到下一阶段;
$count阶段返回聚合管道中剩余文档的计数,并将该值分配给type_count
$group
按指定的表达式对文档进行分组,并将每个不同分组的文档输出到下一个阶段。输出文档包含一个_id字段,该字段按键包含不同的组。输出文档还可以包含计算字段,该字段保存由$group的_id字段分组的一些accumulator表达式的值。 $group不会输出具体的文档而只是统计信息。
{ $group: { _id: <expression>, <field1>: { <accumulator1> : <expression1> }, ... } }
- _id字段是必填的;但是,可以指定_id值为null来为整个输入文档计算累计值。
- 剩余的计算字段是可选的,并使用<accumulator>运算符进行计算。
- _id和<accumulator>表达式可以接受任何有效的表达式。
accumulator操作符
名称 | 描述 | 类比sql |
---|---|---|
$avg | 计算均值 | avg |
$first | 返回每组第一个文档,如果有排序,按照排序,如果没有按照默认的存储的顺序的第一个文档。 | limit 0,1 |
$last | 返回每组最后一个文档,如果有排序,按照排序,如果没有按照默认的存储的顺序的最后个文档。 | - |
$max | 根据分组,获取集合中所有文档对应值得最大值。 | max |
$min | 根据分组,获取集合中所有文档对应值得最小值。 | min |
$push | 将指定的表达式的值添加到一个数组中。 | - |
$addToSet | 将表达式的值添加到一个集合中(无重复值,无序)。 | - |
$sum | 计算总和 | sum |
$stdDevPop | 返回输入值的总体标准偏差(population standard deviation) | - |
$stdDevSamp | 返回输入值的样本标准偏差(the sample standard deviation) | - |
$group
阶段的内存限制为100M。默认情况下,如果stage超过此限制,$group
将产生错误。但是,要允许处理大型数据集,请将allowDiskUse
选项设置为true以启用 $group
操作以写入临时文件。
book的数量,收藏总数和平均值
db.books.aggregate([{$group: {_id: null, count: {$sum: 1}, pop: {$sum: "$favCount"}, avg: {$avg: "$favCount"}}}
])
统计每个作者的book收藏总数
db.books.aggregate([{$group: {_id: "$author.name", pop: {$sum: "$favCount"}}}
])
统计每个作者的每本book的收藏数
db.books.aggregate([{$group: {_id: {name: "$author.name", title: "$title"}, pop: {$sum: "$favCount"}}}
])
每个作者的book的type合集
db.books.aggregate([{$group: {_id: "$author.name", types: {$addToSet: "$type"}}}
])
$unwind
可以将数组拆分为单独的文档
v3.2+支持如下语法:
{$unwind:{# 要指定字段路径, 在字段名称前加上$符并用引号括起来。path: <field path>,# 可选, 一个新字段的名称用于存放元素的数组索引。该名称不能以$开头。includeArrayIndex: <string>, # 可选, default: false, 若为true, 如果路径为空, 缺少或为空数组, 则$unwind输出文档preserveNullAndEmptyArrays: <boolean> }
}
姓名为xx006的作者的book的tag数组拆分为多个文档
db.books.aggregate([{$match: {"author.name": "xx006"}},{$unwind: "$tag"}
])db.books.aggregate([{$match: {"author.name": "xx006"}}
])
每个作者的book的tag合集
db.books.aggregate([{$unwind: "$tag"},{$group: {_id: "$author.name", types: {$addToSet: "$tag"}}}
])
案例
示例数据
db.books.insert([
{"title": "book-51","type": "technology","favCount": 11,"tag": [],"author": {"name": "fox","age": 28}
},{"title": "book-52","type": "technology","favCount": 15,"author": {"name": "fox","age": 28}
},{"title": "book-53","type": "technology","tag": ["nosql","document"],"favCount": 20,"author": {"name": "fox","age": 28}
}])
测试
# 使用includeArrayIndex选项来输出数组元素的数组索引
db.books.aggregate([{$match:{"author.name":"fox"}},{$unwind:{path:"$tag", includeArrayIndex: "arrayIndex"}}
])
# 使用preserveNullAndEmptyArrays选项在输出中包含缺少size字段,null或空数组的文档
db.books.aggregate([{$match:{"author.name":"fox"}},{$unwind:{path:"$tag", preserveNullAndEmptyArrays: true}}
])
$limit
限制传递到管道中下一阶段的文档数
db.books.aggregate([{$limit: 5 }
])
此操作仅返回管道传递给它的前5个文档。 $limit对其传递的文档内容没有影响。
注意:当 $sort
在管道中的 $limit
之前立即出现时,$sort
操作只会在过程中维持前n个结果,其中n是指定的限制,而MongoDB只需要将n个项存储在内存中。
$skip
跳过进入stage的指定数量的文档,并将其余文档传递到管道中的下一个阶段
db.books.aggregate([{$skip: 5 }
])
此操作将跳过管道传递给它的前5个文档。 $skip对沿着管道传递的文档的内容没有影响。
$sort
对所有输入文档进行排序,并按排序顺序将它们返回到管道。
语法:
{ $sort: { <field1>: <sort order>, <field2>: <sort order> ... } }
要对字段进行排序,请将排序顺序设置为1或-1,以分别指定升序(1)或降序(-1)排序,如下例所示:
db.books.aggregate([{$sort: {favCount: -1, title: 1}}
])
$lookup
Mongodb 3.2版本新增,主要用来实现多表关联查询, 相当关系型数据库中多表关联查询。每个输入待处理的文档,经过$lookup 阶段的处理,输出的新文档中会包含一个新生成的数组(可根据需要命名新key )。数组列存放的数据是来自被Join集合的适配文档,如果没有,集合为空(即 为[ ])
语法:
db.collection.aggregate([{$lookup: {from: "<collection to join>",localField: "<field from the input documents>",foreignField: "<field from the documents of the from collection>",as: "<output array field>"}
})
属性 | 作用 |
---|---|
from | 同一个数据库下等待被Join的集合。 |
localField | 源集合中的match值,如果输入的集合中,某文档没有 localField 这个Key(Field),在处理的过程中,会默认为此文档含有 localField:null的键值对。 |
foreignField | 待Join的集合的match值,如果待Join的集合中,文档没有foreignField 值,在处理的过程中,会默认为此文档含有 foreignField:null的键值对。 |
as | 为输出文档的新增值命名。如果输入的集合中已存在该值,则会覆盖掉 |
注意:null = null 此为真
其语法功能类似于下面的伪SQL语句:
SELECT *, <output array field>
FROM collection
WHERE <output array field> IN (SELECT *FROM <collection to join>WHERE <foreignField>= <collection.localField>);
案例
数据准备
db.customer.insert({customerCode: 1, name: "customer1", phone: "13112345678", address: "test1"})
db.customer.insert({customerCode: 2, name: "customer2", phone: "13112345679", address: "test2"})db.order.insert({orderId: 1, orderCode: "order001", customerCode: 1,price: 200})
db.order.insert({orderId: 2, orderCode: "order002", customerCode: 2,price: 400})db.orderItem.insert({itemId: 1, productName: "apples", qutity: 2, orderId: 1})
db.orderItem.insert({itemId: 2, productName: "oranges", qutity: 2, orderId: 1})
db.orderItem.insert({itemId: 3, productName: "mangoes", qutity: 2, orderId: 1})
db.orderItem.insert({itemId: 4, productName: "apples", qutity: 2, orderId: 2})
db.orderItem.insert({itemId: 5, productName: "oranges", qutity: 2, orderId: 2})
db.orderItem.insert({itemId: 6, productName: "mangoes", qutity: 2, orderId: 2})
关联查询
db.customer.aggregate([ {$lookup: {from: "order",localField: "customerCode",foreignField: "customerCode",as: "customerOrder"}}
])db.order.aggregate([{$lookup: {from: "customer",localField: "customerCode",foreignField: "customerCode",as: "curstomer"}},{$lookup: {from: "orderItem",localField: "orderId",foreignField: "orderId",as: "orderItem"}}
])
聚合操作示例1
统计每个分类的book文档数量
db.books.aggregate([{$group: {_id: "$type", total: {$sum: 1}}},{$sort: {total: -1}}
])
标签的热度排行,标签的热度则按其关联book文档的收藏数(favCount)来计算
db.books.aggregate([{$match: {favCount: {$gt: 0}}},{$unwind: "$tag"},{$group: {_id: "$tag", total: {$sum: "$favCount"}}},{$sort: {total: -1}}
])
1.$match阶段:用于过滤favCount=0的文档。
2.$unwind阶段:用于将标签数组进行展开,这样一个包含3个标签的文档会被拆解为3个条目。
3.$group阶段:对拆解后的文档进行分组计算,$sum:“$favCount” 表示按 favCount 字段进行累加。
4.$sort阶段:接收分组计算的输出,按total得分进行排序。
统计book文档收藏数[0,10),[10,60),[60,80),[80,100),[100,+∞)
https://docs.mongodb.com/manual/reference/operator/aggregation/bucket/
db.books.aggregate([{$bucket: {groupBy: "$favCount",boundaries: [0, 10, 60, 80, 100],default: "other",output: {"count": {$sum: 1}}}
}])
聚合操作示例2
导入邮政编码数据集:https://media.mongodb.org/zips.json
使用mongoimport工具导入数据 (https://www.mongodb.com/try/download/database-tools)
mongoimport -h 192.168.1.3 -d appdb -u fox -p fox --authenticationDatabase=admin -c zips --file ~/Downloads/zips.json
-h,–host :代表远程连接的数据库地址,默认连接本地Mongo数据库;
–port:代表远程连接的数据库的端口,默认连接的远程端口27017;
-u,–username:代表连接远程数据库的账号,如果设置数据库的认证,需要指定用户账号;
-p,–password:代表连接数据库的账号对应的密码;
-d,–db:代表连接的数据库;
-c,–collection:代表连接数据库中的集合;
-f, --fields:代表导入集合中的字段;
–type:代表导入的文件类型,包括csv和json,tsv文件,默认json格式;
–file:导入的文件名称
–headerline:导入csv文件时,指明第一行是列名,不需要导入;
返回人口超过1000万的州
db.zips.aggregate( [{ $group: { _id: "$state", totalPop: { $sum: "$pop" } } },{ $match: { totalPop: { $gte: 10*1000*1000 } } }
])
这个聚合操作的等价SQL是:
SELECT state, SUM(pop) AS totalPop
FROM zips
GROUP BY state
HAVING totalPop >= (10*1000*1000)
返回各州平均城市人口
db.zips.aggregate( [{ $group: { _id: { state: "$state", city: "$city" }, cityPop: { $sum: "$pop" } } },{ $group: { _id: "$_id.state", avgCityPop: { $avg: "$cityPop" } } }
] )
按州返回最大和最小的城市
db.zips.aggregate( [{ $group:{_id: { state: "$state", city: "$city" },pop: { $sum: "$pop" }}},{ $sort: { pop: 1 } },{ $group:{_id : "$_id.state",biggestCity: { $last: "$_id.city" },biggestPop: { $last: "$pop" },smallestCity: { $first: "$_id.city" },smallestPop: { $first: "$pop" }}},{ $project:{ _id: 0,state: "$_id",biggestCity: { name: "$biggestCity", pop: "$biggestPop" },smallestCity: { name: "$smallestCity", pop: "$smallestPop" }}}
] )
5.3 MapReduce
MapReduce操作将大量的数据处理工作拆分成多个线程并行处理,然后将结果合并在一起。MongoDB提供的Map-Reduce非常灵活,对于大规模数据分析也相当实用。
MapReduce具有两个阶段:
- 将具有相同Key的文档数据整合在一起的map阶段
- 组合map操作的结果进行统计输出的reduce阶段
MapReduce的基本语法
db.collection.mapReduce(function() {emit(key,value);}, -- map 函数function(key,values) {return reduceFunction}, -- reduce 函数{out: <collection>,query: <document>,sort: <document>,limit: <number>,finalize: <function>, scope: <document>,jsMode: <boolean>,verbose: <boolean>,bypassDocumentValidation: <boolean>}
)
- map,将数据拆分成键值对,交给reduce函数
- reduce,根据键将值做统计运算
- out,可选,将结果汇入指定表
- quey,可选筛选数据的条件,筛选的数据送入map
- sort,排序完后,送入map
- limit,限制送入map的文档数
- finalize,可选,修改reduce的结果后进行输出
- scope,可选,指定map、reduce、finalize的全局变量
- jsMode,可选,默认false。在mapreduce过程中是否将数 据转换成bson格式。
- verbose,可选,是否在结果中显示时间,默认false
- bypassDocmentValidation,可选,是否略过数据校验
统计type为travel的不同作者的book文档收藏数
db.books.mapReduce(function(){emit(this.author.name, this.favCount)},function(key,values){return Array.sum(values)},{query: {type: "travel"},out: "books_favCount"})
从MongoDB 5.0开始,map-reduce操作已被弃用。聚合管道比映射-reduce操作提供更好的性能和可用性。Map-reduce操作可以使用聚合管道操作符重写,例如 $group
、$merge
等。
5.4 SpringBoot整合MongoDB聚合操作
MongoTemplate提供了aggregate方法来实现对数据的聚合操作。
基于聚合管道mongodb提供的可操作的内容:
支持的操作 | Java接口 | 说明 |
---|---|---|
$project | Aggregation.project | 修改输入文档的结构 |
$match | Aggregation.match | 用于过滤数据 |
$limit | Aggregation.limit | 用于限制MongoDB聚合管道返回的文档数 |
$skip | Aggregation.skip | 在聚合管道中跳过指定数量的文档 |
$unwind | Aggregation.unwind | 在文档中的某一个数组类型字段拆分成多条 |
$group | Aggregation.group | 在集合中的文档分组,可用于统计结果 |
$sort | Aggregation.sort | 将输入文档排序后输出 |
$geoNear | Aggregation.geoNear | 输出接近某一地理位置的有序文档 |
基于聚合操作Aggregation.group,mongodb提供可选的表达式
聚合表达式 | Java接口 | 说明 |
---|---|---|
$sum | Aggregation.group().sum("field").as("sum") | 求和 |
$avg | Aggregation.group().avg("field").as("avg") | 求平均 |
$min | Aggregation.group().min("field").as("min") | 获取集合中所有文档对应值的最小值 |
$max | Aggregation.group().max("field").as("max") | 获取集合中所有文档对应值的最大值 |
$push | Aggregation.group().push("field").as("push") | 在结果文档中插入值到一个数组中 |
$addToSet | Aggregation.group().addToSet("field").as("addToSet") | 在结果文档中插入值到一个数组中,但不创建副本 |
$first | Aggregation.group().first("field").as("first") | 根据资源文档的排序获取第一个文档数据 |
$last | Aggregation.group().last("field").as("last") | 根据资源文档的排序获取最后一个文档数据 |
示例:以聚合管道示例2为例
实体结构
@Data
@Document("zips")
@AllArgsConstructor
@NoArgsConstructor
public class Zips {@Id //映射文档中的_idprivate String id;@Fieldprivate String city;@Fieldprivate Double[] loc;@Fieldprivate Integer pop;@Fieldprivate String state;
}
返回人口超过1000万的州
db.zips.aggregate( [{ $group: { _id: "$state", totalPop: { $sum: "$pop" } } },{ $match: { totalPop: { $gt: 10*1000*1000 } } }
])
Java实现
@Test
public void test() {// $groupGroupOperation groupOperation = Aggregation.group("state").sum("pop").as("totalPop");// $matchMatchOperation matchOperation = Aggregation.match(Criteria.where("totalPop").gt(10 * 1000 * 1000));// $sortSortOperation sortOperation = Aggregation.sort(Sort.Direction.DESC, "totalPop");// 按顺序组合每一个聚合步骤TypedAggregation<Zips> typedAggregation = Aggregation.newAggregation(Zips.class,groupOperation, matchOperation, sortOperation);// 调对应的api获取结果// 执行聚合操作,如果不使用 Map,也可以使用自定义的实体类来接收数据AggregationResults<TotalPopDO> aggregationResults = mongoTemplate.aggregate(typedAggregation, TotalPopDO.class);// 取出最终结果List<TotalPopDO> mappedResults = aggregationResults.getMappedResults();for (TotalPopDO totalPopDO : mappedResults) {System.out.println(totalPopDO);}
}
返回各州平均城市人口
db.zips.aggregate( [{ $group: { _id: { state: "$state", city: "$city" }, cityPop: { $sum: "$pop" } } },{ $group: { _id: "$_id.state", avgCityPop: { $avg: "$cityPop" } } },{ $sort:{avgCityPop:-1}}
])
Java实现
@Test
public void test2() {// $groupGroupOperation groupOperation = Aggregation.group("state", "city").sum("pop").as("cityPop");// $groupGroupOperation groupOperation2 = Aggregation.group("_id.state").avg("cityPop").as("avgCityPop");// $sortSortOperation sortOperation = Aggregation.sort(Sort.Direction.DESC, "avgCityPop");// 按顺序组合每一个聚合步骤TypedAggregation<Zips> typedAggregation = Aggregation.newAggregation(Zips.class,groupOperation, groupOperation2, sortOperation);// 执行聚合操作,如果不使用 Map,也可以使用自定义的实体类来接收数据AggregationResults<AvgCityPopDO> aggregationResults = mongoTemplate.aggregate(typedAggregation, AvgCityPopDO.class);// 取出最终结果List<AvgCityPopDO> mappedResults = aggregationResults.getMappedResults();for (AvgCityPopDO avgCityPopDO : mappedResults) {System.out.println(avgCityPopDO);}
}
按州返回最大和最小的城市
db.zips.aggregate( [{ $group:{_id: { state: "$state", city: "$city" },pop: { $sum: "$pop" }}},{ $sort: { pop: 1 } },{ $group:{_id : "$_id.state",biggestCity: { $last: "$_id.city" },biggestPop: { $last: "$pop" },smallestCity: { $first: "$_id.city" },smallestPop: { $first: "$pop" }}},{ $project:{ _id: 0,state: "$_id",biggestCity: { name: "$biggestCity", pop: "$biggestPop" },smallestCity: { name: "$smallestCity", pop: "$smallestPop" }}},{ $sort: { state: 1 } }
])
Java实现
@Test
public void test3() {//$groupGroupOperation groupOperation = Aggregation.group("state", "city").sum("pop").as("pop");//$sortSortOperation sortOperation = Aggregation.sort(Sort.Direction.ASC, "pop");//$groupGroupOperation groupOperation2 = Aggregation.group("_id.state").last("_id.city").as("biggestCity").last("pop").as("biggestPop").first("_id.city").as("smallestCity").first("pop").as("smallestPop");//$projectProjectionOperation projectionOperation = Aggregation.project("biggestCity", "smallestCity", "state").andExclude("_id").andExpression(" { name: \"$biggestCity\", pop: \"$biggestPop\" }").as("biggestCity").andExpression("{ name: \"$smallestCity\", pop: \"$smallestPop\" }").as("smallestCity").and("_id").as("state");// $sortSortOperation sortOperation2 = Aggregation.sort(Sort.Direction.ASC, "state");// 按顺序组合每一个聚合步骤TypedAggregation<Zips> typedAggregation = Aggregation.newAggregation(Zips.class,groupOperation, sortOperation, groupOperation2, projectionOperation,sortOperation2);// 执行聚合操作,如果不使用 Map,也可以使用自定义的实体类来接收数据AggregationResults<SmallBigCityPopDO> aggregationResults = mongoTemplate.aggregate(typedAggregation, SmallBigCityPopDO.class);// 取出最终结果List<SmallBigCityPopDO> mappedResults = aggregationResults.getMappedResults();for (SmallBigCityPopDO smallBigCityPopDO : mappedResults) {System.out.println(smallBigCityPopDO);}
}
6.视图
MongoDB视图是一个可查询的对象,它的内容由其他集合或视图上的聚合管道定义。MongoDB不会将视图内容持久化到磁盘。当客户端查询视图时,视图的内容按需计算。MongoDB可以要求客户端具有查询视图的权限。MongoDB不支持对视图进行写操作。
作用
- 数据抽象
- 保护敏感数据的一种方法
- 将敏感数据投影到视图之外
- 只读
- 结合基于角色的授权,可按角色访问信息
数据准备
let orders = new Array();
let shipping = new Array();
let addresses = ["广西省玉林市", "湖南省岳阳市", "湖北省荆州市", "甘肃省兰州市", "吉林省松 原市", "江西省景德镇", "辽宁省沈阳市", "福建省厦门市", "广东省广州市", "北京市朝阳区"];
for (let i = 10000; i < 20000; i++) {let orderNo = i + Math.random().toString().substr(2, 5);orders[i] = {orderNo: orderNo, userId: i, price: Math.round(Math.random() * 10000) / 100, qty: Math.floor(Math.random() * 10) + 1, orderTime: new Date(new Date().setSeconds(Math.floor(Math.random() * 10000))) };let address = addresses[Math.floor(Math.random() * 10)];shipping[i] = { orderNo: orderNo, address: address, recipienter: "Wilson",province: address.substr(0, 3), city: address.substr(3, 3) }
}
db.order.insert(orders);
db.shipping.insert(shipping);
6.1 创建视图
基本语法格式
db.createView("<viewName>","<source>",[<pipeline>],{"collation" : { <collation> }}
)
- viewName : 必须,视图名称
- source : 必须,数据源,集合/视图
- [] : 可选,一组管道
- collation 可选,排序规则
单个集合创建视图
假设现在查看当天最高的10笔订单视图,例如需要实时显示金额最高的订单
db.orderInfo.drop()
db.createView("orderInfo", // 视图名称"order", // 数据源[// 筛选符合条件的订单,大于当天,这里要注意时区{ $match: { "orderTime": { $gte: ISODate("2022-01-26T00:00:00.000Z") } } },// 按金额倒序{ $sort: { "price": -1 } },// 限制10个文档{ $limit: 10 },// 选择要显示的字段// 0: 排除字段,若字段上使用(_id除外),就不能有其他包含字段// 1: 包含字段{ $project: { _id: 0, orderNo: 1, price: 1, orderTime: 1 } }]
)
视图创建成功后可以直接使用视图查询数据 db.orderInfo.find()
多个集合创建视图
跟单个是集合是一样,只是多了$lookup连接操作符,视图根据管道最终结果显示,所以可以关联多个 集合
db.orderDetail.drop()
db.createView("orderDetail","order",[{ $lookup: { from: "shipping", localField: "orderNo", foreignField: "orderNo", as: "shipping" } },{ $project: { "orderNo": 1, "price": 1, "shipping.address": 1 } }]
)
db.orderDetail.find()
6.2 修改视图
db.runCommand({collMod: "orderInfo",viewOn: "order",pipeline: [{ $match: { "orderTime": { $gte: ISODate("2020-04-13T16:00:00.000Z") } } },{ $limit: 10 },{ $sort: { "price": -1 } },// 增加qty{ $project: { _id: 0, orderNo: 1, price: 1, qty: 1, orderTime: 1 } }]
})
db.orderInfo.find()
6.3 删除视图
db.orderInfo.drop();
7.MongoDB索引
7.1 索引介绍
索引是一种用来快速查询数据的数据结构。B+Tree就是一种常用的数据库索引数据结构,MongoDB 采用B+Tree 做索引,索引创建在colletions上。MongoDB不使用索引的查询,先扫描所有的文档,再匹配符合条件的文档。 使用索引的查询,通过索引找到文档,使用索引能够极大的提升查询效率。
MongoDB索引数据结构
思考: MongoDB索引数据结构是B-Tree还是B+Tree?
B-Tree说法来源于官方文档,然后就导致了分歧: 有人说MongoDB索引数据结构使用的是B-Tree,有的 人又说是B+Tree。
MongoDB官方文档:https://docs.mongodb.com/manual/indexes/
MongoDB indexes use a B-tree data structure.
注: MongoDB具体使用的是B+Tree,因为B+Tree是B-Tree的子集,所以叫B-Tree也对,但容易产生误导。
WiredTiger官方文档: https://source.wiredtiger.com/3.0.0/tune_page_size_and_comp.html
WiredTiger maintains a table’s data in memory using a data structure called a B-Tree ( B+ Tree to be specific), referring to the nodes of a B-Tree as pages. Internal pages carry only keys. The leaf pages store both keys and values.
参考数据结构网站: https://www.cs.usfca.edu/~galles/visualization/Algorithms.html
WiredTiger数据文件在磁盘的存储结构
B+ Tree中的leaf page包含一个页头(page header)、块头(block header)和真正的数据(key/value),其中页头定义了页的类型、页中实际载荷数据的大小、页中记录条数等信息;块头定义了此页的checksum、块在磁盘上的寻址位置等信息。
WiredTiger有一个块设备管理的模块,用来为page分配block。如果要定位某一行数据(key/value)的位置,可以先通过block的位置找到此page(相对于文件起始位置的偏移量),再通过page找到行数据的相对位置,最后可以得到行数据相对于文件起始位置的偏移量offsets。
索引的分类
- 按照索引包含的字段数量,可以分为单键索引和组合索引(或复合索引)。
- 按照索引字段的类型,可以分为主键索引和非主键索引。
- 按照索引节点与物理记录的对应方式来分,可以分为聚簇索引和非聚簇索引,其中聚簇索引是指索引节点上直接包含了数据记录,而后者则仅仅包含一个指向数据记录的指针。
- 按照索引的特性不同,又可以分为唯一索引、稀疏索引、文本索引、地理空间索引等
与大多数数据库一样,MongoDB支持各种丰富的索引类型,包括单键索引、复合索引,唯一索引等一些常用的结构。由于采用了灵活可变的文档类型,因此它也同样支持对嵌套字段、数组进行索引。通过建立合适的索引,我们可以极大地提升数据的检索速度。在一些特殊应用场景,MongoDB还支持地理空间索引、文本检索索引、TTL索引等不同的特性。
索引的设计原则
- 每个查询原则上都需要创建对应索引
- 单个索引设计应考虑满足尽量多的查询
- 索引字段选择及顺序需要考虑查询覆盖率及选择性
- 对于更新及其频繁的字段上创建索引需慎重
- 对于数组索引需要慎重考虑未来元素个数
- 对于超长字符串类型字段上慎用索引
- 并发更新较高的单个集合上不宜创建过多索引
7.2 索引操作
创建索引
创建索引语法格式
db.collection.createIndex(keys, options)
- Key 值为你要创建的索引字段,1 按升序创建索引, -1 按降序创建索引
- 可选参数列表如下
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 |
注意:3.0.0 版本前创建索引方法为 db.collection.ensureIndex()
-- 创建索引后台执行
db.values.createIndex({open: 1, close: 1}, {background: true})
-- 创建唯一索引
db.values.createIndex({title:1},{unique:true})
查看索引
# 查看索引信息
db.books.getIndexes()
# 查看索引键
db.books.getIndexKeys()
查看索引占用空间
db.collection.totalIndexSize([is_detail])
- is_detail:可选参数,传入除0或false外的任意数据,都会显示该集合中每个索引的大小及总大 小。如果传入0或false则只显示该集合中所有索引的总大小。默认值为false。
删除索引
# 删除集合指定索引
db.books.dropIndex("索引名称")
# 删除集合所有索引
db.books.dropIndexes()
7.3 索引类型
单键索引(Single Field Indexes)
在某一个特定的字段上建立索引 mongoDB在ID上建立了唯一的单键索引,所以经常会使用id来进行查 询; 在索引字段上进行精确匹配、排序以及范围查找都会使用此索引
db.books.createIndex({title:1})
对内嵌文档字段创建索引:
db.books.createIndex({"author.name":1})
复合索引(Compound Index)
复合索引是多个字段组合而成的索引,其性质和单字段索引类似。但不同的是,复合索引中字段的顺序、字段的升降序对查询性能有直接的影响,因此在设计复合索引时则需要考虑不同的查询场景。
db.books.createIndex({type: 1, favCount: 1})
多键索引(Multikey Index)
在数组的属性上建立索引。针对这个数组的任意值的查询都会定位到这个文档,既多个索引入口或者键值引用同一个文档
准备inventory集合:
db.inventory.insertMany([{ _id: 5, type: "food", item: "aaa", ratings: [ 5, 8, 9 ] },{ _id: 6, type: "food", item: "bbb", ratings: [ 5, 9 ] },{ _id: 7, type: "food", item: "ccc", ratings: [ 9, 5, 8 ] },{ _id: 8, type: "food", item: "ddd", ratings: [ 9, 5 ] },{ _id: 9, type: "food", item: "eee", ratings: [ 5, 9, 5 ] }
])
创建多键索引
db.inventory.createIndex( { ratings: 1 } )
多键索引很容易与复合索引产生混淆,复合索引是多个字段的组合,而多键索引则仅仅是在一个字段上 出现了多键(multi key)。而实质上,多键索引也可以出现在复合字段上
# 创建复合多键索引
db.inventory.createIndex( { item:1,ratings: 1} )
注意: MongoDB并不支持一个复合索引中同时出现多个数组字段
嵌入文档的索引数组
db.inventory.insertMany([{_id: 1,item: "abc",stock: [{ size: "S", color: "red", quantity: 25 },{ size: "S", color: "blue", quantity: 10 },{ size: "M", color: "blue", quantity: 50 }
] },
{
_id: 2,item: "def",stock: [{ size: "S", color: "blue", quantity: 20 },{ size: "M", color: "blue", quantity: 5 },{ size: "M", color: "black", quantity: 10 },{ size: "L", color: "red", quantity: 2 }
] },
{
_id: 3,item: "ijk",stock: [{ size: "M", color: "blue", quantity: 15 },{ size: "L", color: "blue", quantity: 100 },{ size: "L", color: "red", quantity: 25 }
] }
])
在包含嵌套对象的数组字段上创建多级索引
db.inventory.createIndex( { "stock.size": 1, "stock.quantity": 1 } )
db.inventory.find( { "stock.size": "S", "stock.quantity": { $gt: 20 } } )
地理空间索引(Geospatial Index)
在移动互联网时代,基于地理位置的检索(LBS)功能几乎是所有应用系统的标配。MongoDB为地理空间检索提供了非常方便的功能。地理空间索引(2dsphereindex)就是专门用于实现位置检索的一种特殊索引。
案例:MongoDB如何实现 “查询附近商家”?
假设商家的数据模型如下:
db.restaurant.insert({restaurantId: 0, restaurantName:"兰州牛肉面", location: {type: "Point",coordinates: [ -73.97, 40.77 ]}
})
创建一个2dsphere索引
db.restaurant.createIndex({location : "2dsphere"})
查询附近10000米商家信息
db.restaurant.find( {location:{$near :{$geometry :{type: "Point",coordinates: [ -73.88, 40.78 ]},$maxDistance: 10000}}
})
- $near查询操作符,用于实现附近商家的检索,返回数据结果会按距离排序。
- $geometry操作符用于指定一个GeoJSON格式的地理空间对象,type=Point表示地理坐标点,coordinates则是用户当前所在的经纬度位置;$maxDistance限定了最大距离,单位是米。
全文索引(Text Indexes)
MongoDB支持全文检索功能,可通过建立文本索引来实现简易的分词检索。
db.reviews.createIndex( { comments: "text" } )
$text操作符可以在有text index的集合上执行文本检索。$text将会使用空格和标点符号作为分隔符对检 索字符串进行分词, 并且对检索字符串中所有的分词结果进行一个逻辑上的 OR 操作。
全文索引能解决快速文本查找的需求,比如有一个博客文章集合,需要根据博客的内容来快速查找,则可以针对博客内容建立文本索引。
案例
数据准备
db.stores.insert([{ _id: 1, name: "Java Hut", description: "Coffee and cakes" },{ _id: 2, name: "Burger Buns", description: "Gourmet hamburgers" },{ _id: 3, name: "Coffee Shop", description: "Just coffee" },{ _id: 4, name: "Clothes Clothes Clothes", description: "Discount clothing" },{ _id: 5, name: "Java Shopping", description: "Indonesian goods" }
] )
创建name和description的全文索引
db.stores.createIndex({name: "text", description: "text"})
测试
通过$text操作符来查寻数据中所有包含 “coffee”、“shop”、“java” 列表中任何词语的商店
db.stores.find({$text: {$search: "java coffee shop"}})
MongoDB的文本索引功能存在诸多限制,而官方并未提供中文分词的功能,这使得该功能的应用场景十分受限。
Hash索引(Hashed Indexes)
不同于传统的B-Tree索引,哈希索引使用hash函数来创建索引。在索引字段上进行精确匹配,但不支持范围查询,不支持多键hash;Hash索引上的入口是均匀分布的,在分片集合中非常有用;
db.users.createIndex({username: 'hashed'})
通配符索引(Wildcard Indexes)
MongoDB的文档模式是动态变化的,而通配符索引可以建立在一些不可预知的字段上,以此实现查询的加速。MongoDB 4.2 引入了通配符索引来支持对未知或任意字段的查询。
案例
准备商品数据,不同商品属性不一样
db.products.insert([{"product_name": "Spy Coat","product_attributes": {"material": [ "Tweed", "Wool", "Leather" ],"size": {"length": 72,"units": "inches"}}
}, {"product_name": "Spy Pen","product_attributes": {"colors": [ "Blue", "Black" ],"secret_feature": {"name": "laser","power": "1000","units": "watts",}}
}, {"product_name": "Spy Book"}
])
创建通配符索引
db.products.createIndex( { "product_attributes.$**": 1 } )
测试
通配符索引可以支持任意单字段查询 product_attributes或其嵌入字段:
db.products.find( { "product_attributes.size.length": { $gt: 60 } } )
db.products.find( { "product_attributes.material": "Leather" } )
db.products.find( { "product_attributes.secret_feature.name": "laser" } )
注意事项
-
通配符索引不兼容的索引类型或属性
db.products.createIndex( { "product_attributes.$**": 1, "product_name": 1 } )
-
通配符索引是稀疏的,不索引空字段。因此,通配符索引不能支持查询字段不存在的文档。
-- 通配符索引不能支持以下查询 db.products.find( {"product_attributes": { $exists: false } } ) db.products.aggregate([{ $match: { "product_attributes": { $exists: false } } } ])
-
通配符索引为文档或数组的内容生成条目,而不是文档/数组本身。因此通配符索引不能支持精确的文档/数组相等匹配。通配符索引可以支持查询字段等于空文档{}的情况。
-- 通配符索引不能支持以下查询: db.products.find({ "product_attributes.colors": [ "Blue", "Black" ] } ) db.products.aggregate([{$match: { "product_attributes.colors": [ "Blue", "Black" ] } }])
7.4 索引属性
唯一索引(Unique Indexes)
在现实场景中,唯一性是很常见的一种索引约束需求,重复的数据记录会带来许多处理上的麻烦,比如订单的编号、用户的登录名等。通过建立唯一性索引,可以保证集合中文档的指定字段拥有唯一值。
# 创建唯一索引
db.values.createIndex({title:1},{unique:true})
# 复合索引支持唯一性约束
db.values.createIndex({title:1,type:1},{unique:true})
# 多键索引支持唯一性约束
db.inventory.createIndex( { ratings: 1 },{unique:true} )
- 唯一性索引对于文档中缺失的字段,会使用null值代替,因此不允许存在多个文档缺失索引字段的情况。
- 对于分片的集合,唯一性约束必须匹配分片规则。换句话说,为了保证全局的唯一性,分片键必须作为唯一性索引的前缀字段。
部分索引(Partial Indexes)
部分索引仅对满足指定过滤器表达式的文档进行索引。通过在一个集合中为文档的一个子集建立索引,部分索引具有更低的存储需求和更低的索引创建和维护的性能成本。3.2新版功能。
部分索引提供了稀疏索引功能的超集,应该优先于稀疏索引。
db.restaurants.createIndex({ cuisine: 1, name: 1 },{ partialFilterExpression: { rating: { $gt: 5 } } }
)
partialFilterExpression选项接受指定过滤条件的文档:
- 等式表达式(例如:field: value或使用$eq操作符)
- $exists: true
- $gt, $gte, $lt, $lte
- $type
- 顶层的$and
-- 符合条件,使用索引
db.restaurants.find( { cuisine: "Italian", rating: { $gte: 8 } } )
-- 不符合条件,不能使用索引
db.restaurants.find( { cuisine: "Italian" } )
案例1
restaurants集合数据
db.restaurants.insert({"_id": ObjectId("5641f6a7522545bc535b5dc9"),"address": {"building": "1007","coord": [-73.856077,40.848447 ],"street": "Morris Park Ave","zipcode": "10462"},"borough": "Bronx","cuisine": "Bakery","rating": { "date": ISODate("2014-03-03T00:00:00Z"),"grade": "A","score": 2},"name": "Morris Park Bake Shop","restaurant_id": "30075445"
})
创建索引
db.restaurants.createIndex({ borough: 1, cuisine: 1 },{ partialFilterExpression: { 'rating.grade': { $eq: "A" } } }
)
测试
db.restaurants.find( { borough: "Bronx", 'rating.grade': "A" } )
db.restaurants.find( { borough: "Bronx", cuisine: "Bakery" } )
唯一约束结合部分索引使用导致唯一约束失效的问题
注意: 如果同时指定了partialFilterExpression和唯一约束,那么唯一约束只适用于满足筛选器表达式的 文档。如果文档不满足筛选条件,那么带有惟一约束的部分索引不会阻止插入不满足惟一约束的文档。
案例2
users集合数据准备
db.users.insertMany( [{ username: "david", age: 29 },{ username: "amanda", age: 35 },{ username: "rajiv", age: 57 }
])
创建索引,指定username字段和部分过滤器表达式age: {$gte: 21}的唯一约束。
db.users.createIndex({ username: 1 },{ unique: true, partialFilterExpression: { age: { $gte: 21 } } }
)
测试
索引防止了以下文档的插入,因为文档已经存在,且指定的用户名和年龄字段大于21:
db.users.insertMany( [{ username: "david", age: 27 },{ username: "amanda", age: 25 },{ username: "rajiv", age: 32 }
])
但是,以下具有重复用户名的文档是允许的,因为唯一约束只适用于年龄大于或等于21岁的文档。
db.users.insertMany( [{ username: "david", age: 20 },{ username: "amanda" },{ username: "rajiv", age: null }
])
稀疏索引(Sparse Indexes)
索引的稀疏属性确保索引只包含具有索引字段的文档的条目,索引将跳过没有索引字段的文档。
特性: 只对存在字段的文档进行索引(包括字段值为null的文档)
# 不索引不包含xmpp_id字段的文档
db.addresses.createIndex( { "xmpp_id": 1 }, { sparse: true } )
如果稀疏索引会导致查询和排序操作的结果集不完整,MongoDB将不会使用该索引,除非hint()明确指 定索引。
案例1
数据准备
db.scores.insertMany([{"userid" : "newbie"},{"userid" : "abby", "score" : 82},{"userid" : "nina", "score" : 90}
])
创建稀疏索引
db.scores.createIndex( { score: 1 } , { sparse: true } )
测试
-- 使用稀疏索引
db.scores.find( { score: { $lt: 90 } } )-- 即使排序是通过索引字段,MongoDB也不会选择稀疏索引来完成查询,以返回完整的结果
db.scores.find().sort( { score: -1 } )-- 要使用稀疏索引,使用hint()显式指定索引
db.scores.find().sort( { score: -1 } ).hint( { score: 1 } )
同时具有稀疏性和唯一性的索引可以防止集合中存在字段值重复的文档,但允许不包含此索引字段的文档插入。
案例2
-- 创建具有唯一约束的稀疏索引
db.scores.createIndex( { score: 1 } , { sparse: true, unique: true } )
测试
这个索引将允许插入具有唯一的分数字段值或不包含分数字段的文档。因此,给定scores集合中的现有文档,索引允许以下插入操作:
db.scores.insertMany( [{ "userid": "AAAAAAA", "score": 43 },{ "userid": "BBBBBBB", "score": 34 },{ "userid": "CCCCCCC" },{ "userid": "CCCCCCC" }
])
索引不允许添加下列文件,因为已经存在评分为82和90的文件:
db.scores.insertMany( [{ "userid": "AAAAAAA", "score": 82 },{ "userid": "BBBBBBB", "score": 90 }
])
TTL索引(TTL Indexes)
在一般的应用系统中,并非所有的数据都需要永久存储。例如一些系统事件、用户消息等,这些数据随着时间的推移,其重要程度逐渐降低。更重要的是,存储这些大量的历史数据需要花费较高的成本,因此项目中通常会对过期且不再使用的数据进行老化处理。
通常的做法如下:
方案一: 为每个数据记录一个时间戳,应用侧开启一个定时器,按时间戳定期删除过期的数据。
方案二: 数据按日期进行分表,同一天的数据归档到同一张表,同样使用定时器删除过期的表。
对于数据老化,MongoDB提供了一种更加便捷的做法:TTL(Time To Live)索引。TTL索引需要声明 在一个日期类型的字段中,TTL 索引是特殊的单字段索引,MongoDB 可以使用它在一定时间或特定时 钟时间后自动从集合中删除文档。
-- 创建 TTL 索引,TTL 值为3600秒
db.eventlog.createIndex( { "lastModifiedDate": 1 }, { expireAfterSeconds: 3600 } )
对集合创建TTL索引之后,MongoDB会在周期性运行的后台线程中对该集合进行检查及数据清理工作。 除了数据老化功能,TTL索引具有普通索引的功能,同样可以用于加速数据的查询。
TTL 索引不保证过期数据会在过期后立即被删除。文档过期和 MongoDB 从数据库中删除文档的时间之 间可能存在延迟。删除过期文档的后台任务每 60 秒运行一次。因此,在文档到期和后台任务运行之间 的时间段内,文档可能会保留在集合中。
案例
数据准备
db.log_events.insertOne( {"createdAt": new Date(),"logEvent": 2,"logMessage": "Success!"
})
创建TTL索引
db.log_events.createIndex( { "createdAt": 1 }, { expireAfterSeconds: 20 } )
可变的过期时间
TTL索引在创建之后,仍然可以对过期时间进行修改。这需要使用collMod命令对索引的定义进行变更
db.runCommand({collMod:"log_events",index:{keyPattern: {createdAt:1},expireAfterSeconds:600}})
db.log_events.getIndexes()
使用约束
TTL索引的确可以减少开发的工作量,而且通过数据库自动清理的方式会更加高效、可靠,但是在使用 TTL索引时需要注意以下的限制:
- TTL索引只能支持单个字段,并且必须是非_id字段。
- TTL索引不能用于固定集合。
- TTL索引无法保证及时的数据老化,MongoDB会通过后台的TTLMonitor定时器来清理老化数据, 默认的间隔时间是1分钟。当然如果在数据库负载过高的情况下,TTL的行为则会进一步受到影响。
- TTL索引对于数据的清理仅仅使用了remove命令,这种方式并不是很高效。因此TTL Monitor在运 行期间对系统CPU、磁盘都会造成一定的压力。相比之下,按日期分表的方式操作会更加高效。
隐藏索引(Hidden Indexes)
隐藏索引对查询规划器不可见,不能用于支持查询。通过对规划器隐藏索引,用户可以在不实际删除索 引的情况下评估删除索引的潜在影响。如果影响是负面的,用户可以取消隐藏索引,而不必重新创建已 删除的索引。4.4新版功能。
-- 创建隐藏索引
db.restaurants.createIndex({ borough: 1 },{ hidden: true });
-- 隐藏现有索引
db.restaurants.hideIndex( { borough: 1} );
db.restaurants.hideIndex( "索引名称" );
# 取消隐藏索引
db.restaurants.unhideIndex( { borough: 1} );
db.restaurants.unhideIndex( "索引名称" );
案例
db.scores.insertMany([{"userid" : "newbie"},{"userid" : "abby", "score" : 82},{"userid" : "nina", "score" : 90}
])
创建隐藏索引
db.scores.createIndex({ userid: 1 },{ hidden: true }
);
查看索引信息
db.scores.getIndexes()
索引属性hidden只在值为true时返回
测试
-- 不使用索引
db.scores.find({userid:"abby"}).explain()
-- 取消隐藏索引
db.scores.unhideIndex( { userid: 1} );
-- 使用索引
db.scores.find({userid:"abby"}).explain()
7.5 索引使用建议
1.为每一个查询建立合适的索引
这个是针对于数据量较大比如说超过几十上百万(文档数目)数量级的集合。如果没有索引MongoDB 需要把所有的Document从盘上读到内存,这会对MongoDB服务器造成较大的压力并影响到其他请求的 执行。
2.创建合适的复合索引,不要依赖于交叉索引
如果你的查询会使用到多个字段,MongoDB有两个索引技术可以使用:交叉索引和复合索引。交叉索 引就是针对每个字段单独建立一个单字段索引,然后在查询执行时候使用相应的单字段索引进行索引交 叉而得到查询结果。交叉索引目前触发率较低,所以如果你有一个多字段查询的时候,建议使用复合索 引能够保证索引正常的使用。
-- 查找所有年龄小于30岁的深圳市马拉松运动员
db.athelets.find({sport: "marathon", location: "sz", age: {$lt: 30}}})
-- 创建复合索引
db.athelets.createIndex({sport:1, location:1, age:1})
3.复合索引字段顺序:匹配条件在前,范围条件在后(Equality First, Range After)
前面的例子,在创建复合索引时如果条件有匹配和范围之分,那么匹配条件(sport: “marathon”) 应该在复合索引的前面。范围条件(age: <30)字段应该放在复合索引的后面。
4.尽可能使用覆盖索引(Covered Index)
5.建索引要在后台运行
在对一个集合创建索引时,该集合所在的数据库将不接受其他读写操作。对大数据量的集合建索引,建议使用后台运行选项 {background: true}
7.6 explain执行计划详解
通常我们需要关心的问题:
- 查询是否使用了索引
- 索引是否减少了扫描的记录数量
- 是否存在低效的内存排序
MongoDB提供了explain命令,它可以帮助我们评估指定查询模型(querymodel)的执行计划,根据 实际情况进行调整,然后提高查询效率。
explain()方法的形式如下:
db.collection.find().explain(<verbose>)
- verbose 可选参数,表示执行计划的输出模式,默认queryPlanner
模式名称 | 描述 |
---|---|
queryPlanner | 执行计划的详细信息,包括查询计划、集合信息、查询条件、最佳执行计划、查询方式和MongoDB服务信息等 |
exectionStats | 最佳执行计划的执行情况和被拒绝的计划信息等 |
allPlansExecution | 选择并执行最佳执行计划,并返回最佳执行计划和其他执行计划的执行情况 |
queryPlanner
-- 未创建title的索引
db.books.find({title:"book-1"}).explain("queryPlanner")
字段名称 | 描述 |
---|---|
plannerVersion | 执行计划的版本 |
namespace | 查询的集合 |
indexFilterSet | 是否使用索引 |
parsedQuery | 查询条件 |
winningPlan | 最佳执行计划 |
stage | 查询方式 |
filter | 过滤条件 |
direction | 查询顺序 |
rejectedPlans | 拒绝的执行计划 |
serverInfo | mongodb服务器信息 |
executionStats
executionStats 模式的返回信息中包含了 queryPlanner 模式的所有字段,并且还包含了最佳执行计划 的执行情况
-- 创建索引
db.books.createIndex({title:1})
db.books.find({title:"book-1"}).explain("executionStats")
字段名称 | 描述 |
---|---|
winningPlan.inputStage | 用来描述子stage, 并且为其父stage提 供文档和索引关键字 |
winningPlan.inputStage.stage | 子查询方式 |
winningPlan.inputStage.keyPattern | 所扫描的index内容 |
winningPlan.inputStage.indexName | 索引名 |
winningPlan.inputStage.isMultiKey | 是否是Multikey。如 果索引建立在array 上,将是true |
executionStats.executionSuccess | 是否执行成功 |
executionStats.nReturned | 返回的个数 |
executionStats.executionTimeMillis | 这条语句执行时间 |
executionStats.executionStages.executionTimeMillisEstimate | 检索文档获取数据的时间 |
executionStats.executionStages.inputStage.executionTimeMillisEstimate | 扫描获取数据的时间 |
executionStats.totalKeysExamined | 索引扫描次数 |
executionStats.totalDocsExamined | 文档扫描次数 |
executionStats.executionStages.isEOF | 是否到达 steam 结尾,1 或者 true 代表已到达结尾 |
executionStats.executionStages.works | 工作单元数,一个查询会分解成小的工作单元 |
executionStats.executionStages.advanced | 优先返回的结果数 |
executionStats.executionStages.docsExamined | 文档检查数 |
allPlansExecution
allPlansExecution返回的信息包含 executionStats 模式的内容,且包含allPlansExecution:[]块
"allPlansExecution": [{"nReturned": <int>,"executionTimeMillisEstimate": <int>,"totalKeysExamined": <int>,"totalDocsExamined":<int>,"executionStages": {"stage": <STAGEA>,"nReturned": <int>,"executionTimeMillisEstimate": <int>,...}},...
]
stage状态
状态 | 描述 |
---|---|
COLLSCAN | 全表扫描 |
IXSCAN | 索引扫描 |
FETCH | 根据索引检索指定文档 |
SHARD_MERGE | 将各个分片返回的数据进行合并 |
SORT | 在内存中进行了排序 |
LIMIT | 使用limit限制返回数 |
SKIP | 使用skip进行跳过 |
IDHACK | 对_id进行查询 |
SHARDING_FILTER | 通过mongos对分片数据进行查询 |
COUNTSCAN | count不使用Index进行count时的stage返回 |
COUNT_SCAN | count使用了Index进行count时的stage返回 |
SUBPLA | 未使用到索引的$or查询的stage返回 |
TEXT | 使用全文索引进行查询时候的stage返回 |
PROJECTION | 限定返回字段时候的stage返回 |
执行计划的返回结果中尽量不要出现以下stage:
- COLLSCAN(全表扫描)
- SORT(使用sort但是无index)
- 不合理的SKIP
- SUBPLA(未用到index的$or)
- COUNTSCAN(不使用index进行count)