1. 写在前面
最近工作用到了Mongodb,虽然有了gpt,对于这种数据库操作的代码基本上不用自己费多大功夫,但对于知识本身,还是想借机会系统学习下Mongodb的,原因是之前接触数据库一直都是mysql,oracle等关系型数据库,对于非关系型数据库,这还是第一次接触,另外,就是写代码的时候,也不想总是问gpt, 毕竟这样得到的知识总是零散的,根本记不住,所以,抽出了几天的时间系统学习一下mongodb,梳理个框架,把知识拎起来,本篇文章就是系统学习Mongodb数据库的笔记,本次教程主要参考两个网站C语言中文网, 菜鸟教程,当然,在学习过程中,针对里面的一些知识点补充了细节,并全程用自己设计demo进行知识理解,针对里面的错误点或版本不兼容的点进行了整改,增加了python操作mongodb的代码,增加了pyspark连接mongodb的代码,增加了各种python操作的demo。针对里面的一些知识,按照自己的风格进行提炼, 系统整理,查看起来就会非常方便。 一篇文章统筹mongodb的常规操作
内容很长,不需要全部记住, 遇到问题随时查阅就好 😉
大纲如下:
- MongoDB基础(what, why, how, install, 常用概念等)
- MongoDB常规(数据库,集合,文档,查询,复制,分片,pymongo等)
- MongoDB高级(多表关联,索引,原子操作,MapReduce,固定集合,自增ID等)
Ok, let’s go!
2. MongoDB基础
2.1 What
说到数据库,主要分为关系型数据库(Oracle, mysql等)和非关系型数据库(Redis, MongoDB等), 非关系型数据库用NoSQL(Not Only Sql)表示,三个词:非关系型,分布式,不遵循ACID(原子性,一致性,隔离性,持久性)原则的数据库设计模式。
MongoDB 是一个开源的、可扩展的、跨平台的、面向文档的非关系型数据库,它由 C++ 语言编写,旨在为 WEB 应用提供可扩展的高性能数据存储解决方案
在 MongoDB 中支持以类似 json 的 bson(一种计算机数据交换格式)格式来存储数据,可以存储比较复杂的数据类型。MongoDB 最大的特点是它支持的查询语言非常强大,其语法有点类似于面向对象语言中函数调用,几乎可以实现类似关系数据库单表查询的绝大部分功能,而且还可以对数据建立索引。
2.2 Why
那么为什么会用到NoSQL呢?那是因为传统的关系型数据库处理大规模数据,以及高并发方面出现了很多难以克服的问题,NoSQL 数据库的产生就是为了解决大规模数据集合多重数据种类带来的问题,特别是大数据应用的难题。
相比关系型数据库, NoSQL有以下优势:
- 易扩展:去掉关系型数据库的关系特性,数据与数据之间没有关系,非常容易扩展
- 大数据量,高性能:有非常高的读写性能,处理大数据表现优秀
- 灵活:可以存储任意类型的数据,无须提前为存储的数据定义好字段
- 高可用:NoSQL 在不太影响性能的情况下,就可以方便地实现高可用的架构,比如 Cassandra、HBase 模型,通过复制模型也能实现高可用
MongoDB的优势:
- 面向文档: MongoDB不像关系类型数据库有固定的存储数据格式,而是将数据存储在文档中,非常灵活;
- 临时查询:支持按字段、范围和正则表达式查询并返回文档中的数据
- 索引:可以创建索引提高MongoDB的搜索性能,文档中任何字段都可以创建索引
- 复制:支持高可用性的副本集。副本集由两个或多个 MongoDB 数据库实例组成,每个副本集成员可以随时充当主副本或辅助副本的角色,主副本是与客户端交互并执行所有读/写操作的主服务器。辅助副本使用内置复制维护主副本的数据。当主副本失败时,副本集将自动切换到辅助副本,然后将辅助副本作为主服务器;增加了数据的鲁棒
- 负载均衡:可以在多台服务器上运行,以平衡负载或复制数据,以便在硬件出现故障时保持系统正常运行
2.3 How
NoSQL的体系框架,分为四层:
适用场景:
- 数据模型简单;
- 对灵活性要求很强;
- 对数据库性能要求高;
- 不需要高度的数据一致性;
- 对于给定 key,比较容易映射复杂值的场景
MongoDB的适用场景:主要目标是在键/值存储方式和传统的 RDBMS(关系型数据库)系统之间架起一座桥梁,它集两者的优势于一身。
- 网站数据:适合实时的插入、更新与查询数据,并具备网站实时存储数据所需的复制及高度伸缩的特性
- 缓存:适合作为信息基础设施的缓存层
- 庞大的、低价值数据:适合庞大数据的存储,解决传统的关系型数据库存取大量数据时,数据库的运行效率问题
- 高伸缩性场景:内置了 MapReduce 引擎,非常适合由数十或数百台服务器组成的数据库
- 用于对象是JSON的数据存储:BSON 数据格式非常适合文档化格式的存储及查询
不适用场景:
- 高度事务性系统:例如银行或会计系统,传统的关系型数据库目前还是更适用于需要大量原子性复杂事务的应用程序
- 传统的商业智能应用:针对特定问题的 BI(全称“Business Intelligence”,中文意思为“商业智慧或商务智能”,指用现代数据仓库技术、线上分析处理技术、数据挖掘和数据展现技术进行数据分析以实现商业价值)数据库会产生高度优化的查询方式,对于此类应用,数据仓库可能是更合适的选择;
- 需要复杂 SQL 查询的应用
2.4 Install
我系统是Ubuntu, 安装比较简单,但踩了几个小坑
-
先安装服务器server,上社区选择合适的版本
这里选tgz的包,然后copy link, 在命令行下载会快一些:
wget https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu2204-7.0.5.tgz # 附加 # 查看ubuntu系统基本信息 cat /etc/lsb-release # 查看linux系统架构 uname -m # x86_64 # 下面这个命令 也可以看(内核版本) cat /proc/version # Linux version 6.5.0-15-generic (buildd@bos03-amd64-040) (x86_64-linux-gnu-gcc-12 (Ubuntu 12.3.0-1ubuntu1~22.04) 12.3.0, GNU ld (GNU Binutils for Ubuntu) 2.38) #15~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC Fri Jan 12 18:54:30 UTC 2 # 下载到本地之后,解压, 配置到环境变量中 tar -zxvf mongodb-linux-x86_64-ubuntu2204-7.0.5.tgz mv mongodb-linux-x86_64-ubuntu2204-7.0.5 mongodb sudo mv mongodb /usr/local # 配置环境变量 /etc/profile 加入 export MONGODB_HOME=/usr/local/mongodb export PATH=${MONGODB_HOME}/bin:$PATH # 启动mongodb服务器, 可以指定db路径和logpath路径 mongod --dbpath /var/lib/mongodb --logpath /var/log/mongodb/mongod.log --fork # 注意三点: # 这里加--fork之后 如果有报错,看不到具体的报错原因,去掉fork才能看到 # 第一次安装, 报错找不到/data/db,mongodb会适用这个数据库目录, 需要sudo mkdir /data/db创建并赋予当前用户读写权限 # 如果指定了dbpath和logpath, 需要对目录也给读写权限,否则启动的时候会报错 sudo chown `whoami` /data/db sudo chown `whoami` /var/log/mongodb/ sudo chown `whoami` /var/lib/mongodb/
服务器启动成功标志:
-
接下来,要安装mongodb shell, 类似客户端,有这个之后,才能去连接mongodb去访问数据,之前以为安装完server之后一气呵成的,结果输入mongo找不到命令,打不开mongodb shell, 探索了好久还是单独装个mongo shell吧。地址
# 安装 sudo dpkg -i sudo dpkg -i mongodb-mongosh_2.1.4_amd64.deb # 连接mongdo数据库 mongosh "mongodb://127.0.0.1:27017" # 这样就进入mongodb的命令行了,然后就可以查看各个数据了
mongo shell启动成功标志:
2.5 常用概念
数据库: 存储数据的物理容器,每个数据库在文件系统中有自己的文件集。一台mongodb服务器可以创建多个数据库,且数据库之间是独立的。默认数据库test,查看数据库的命令:
# 查看数据库列表, 注意只有非空的数据库才能用这个方式查出来
show dbs
集合:一组Mongodb文档的组合,类似于mysql中的数据表, 存在于数据库中,没有固定的结构。可以向集合中插入不同格式或类型的数据。
文档:Mongodb中数据的基本单位,由BSON格式(计算机数据交换格式,类似JSON)的键/值对组成。类似于mysql中的一行记录,但格式会复杂一些。
Mongodb突出的特点之一:文档有动态模式,即同一集合中的文档不需要有相同的字段(因为集合没有固定的结构),相同的字段也可以是不同的数据类型
关系型数据库与mongodb的差异对比:
一个简单的文档结构:
# _id是一个 12 字节的十六进制数字,可确保每个文档的唯一性。
# 可以在插入文档时提供_id的具体值,如果不提供,MongoDB将为每个文档提供一个唯一的值。
# 自动生成的_id中前4个字节是当前的时间戳,之后的 3 个字节是机器 id,再之后 2 个字节是 MongoDB 服务器的进程 id,剩下的 3 个字节是简单的随机数
{
# 类似于数据库中的主键id,长度是24, 16进值每4位进行划分,共96位, 12个字节 601e2b6b(时间戳) aa203c(机器id) c89f(进程id) 2d31aa(简单随机数)
# 可以.str获取到具体值 "601e288aaa203cc89f2d31a7" 这个和ObjectId("601e288aaa203cc89f2d31a7")不是一回事
_id: ObjectId("601e288aaa203cc89f2d31a7"),
title: 'MongoDB Concept',
description: 'MongoDB is no sql database',
tags: ['mongodb', 'database', 'NoSQL'],
likes: 100,
comments: [
{
user:'user1',
message: 'My first comment',
dateCreated: new Date(2011,1,20,2,15),
like: 0
},
{
user:'user2',
message: 'My second comments',
dateCreated: new Date(2011,1,25,7,45),
like: 5
}
]
}
2.6 数据类型
2.7 数据模型
mongodb中存储数据非常灵活,关系数据库中,插入数据前必须先确定数据表的结构并创建数据表,而mongodb中,对文档结构没有强制要求。
在 MongoDB 模型设计需要注意以下几点:
- 根据具体项目需求选择合适设计模式;
- 如果是要同时使用的数据,可以合并到一个文档中,否则将它们分成若干个文档;
- 可以有适当的数据冗余,因为与计算时间相比,磁盘空间更便宜;
- 针对最常用的用例优化模型;
- 写入时执行连接,而不是读取时执行连接;
- 在模式中执行复杂聚合。
MongoDB 提供两种数据模型,可以根据需要使用其中的任何一种。
-
嵌入式数据模型(非规范化数据模型)
在该模型中可以将所有相关的数据存储到一个文档中,例如在三个不同的文档中分别存储了一个员工的个人信息、联系方式和地址等信息,可以将这些信息整合到一个文档中
{ _id: ObjectId("601f4be6e646844cd045c8a4"), Emp_ID: "10025AE336", Personal_details:{ First_Name: "Radhika", Last_Name: "Sharma", Date_Of_Birth: "1995-09-26" }, Contact: { e-mail: "biancheng.net@gmail.com", phone: "9848022338" }, Address: { city: "Hyderabad", Area: "Madapur", State: "Telangana" } }
-
规范化数据模型
在规范化数据模型中,您可以通过引用来将原始文档与子文档关联起来,例如您可以将上面的文档信息以规范化数据模型重写为以下几个文档
# personal detail { _id: ObjectId("601f50bae646844cd045c8a5"), empDocID: ObjectId("601f4be6e646844cd045c8a4"), First_Name: "Radhika", Last_Name: "Sharma", Date_Of_Birth: "1995-09-26" } # concat { _id: ObjectId("601f50bae646844cd045c8a6"), empDocID: ObjectId("601f4be6e646844cd045c8a4"), e-mail: "biancheng.net@gmail.com", phone: "9848022338" } # address { _id: ObjectId("601f50bae646844cd045c8a7"), empDocID: ObjectId("601f4be6e646844cd045c8a4"), city: "Hyderabad", Area: "Madapur", State: "Telangana" } # employee { _id: ObjectId("601f4be6e646844cd045c8a4"), Emp_ID: "10025AE336", personal_detail: ObjectId("601f50bae646844cd045c8a5"), concat: ObjectId("601f50bae646844cd045c8a6"), address: ObjectId("601f50bae646844cd045c8a7") }
3. MongoDB基本操作
3.1 数据库操作
3.1.1 连接数据库
# 标准URI格式
mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/[database][?options]]
mongodb:// 必填参数,固定格式
username: password@ 可选,用于有用户名和密码验证的数据库登录
host1: 必填,数据库的地址,如果连接复制集,需要指定多个主机地址
portX: 可选,默认是27017
/database: 可选,指定数据库,默认是test
?options: 定义连接选项,一般用不到
# demo 我mongo命令不可用, 用mongosh命令
mongosh "mongodb://127.0.0.1:27017"
3.1.2 创建数据库
# show查看数据库
show dbs # 注意,没有数据的数据库不会展示
admin 40.00 KiB
config 60.00 KiB
local 72.00 KiB
# use命令创建数据库,如果存在则切换,否则新创建
use local # switched to db local
use zhongqiang_test # switched to db zhongqiang_test,此时show dbs,没有这个数据库,原因是这个数据库没有数据
# db查看当前选择的数据库
db # local
# MongoDB 中默认的数据库为 test,如果您没有创建新的数据库或者选择其它数据库,集合将默认存放在 test 数据库中。
3.1.3 删除数据库
# 先切换到要删除的数据库
use xxx
# 删除
db.dropDatabase() # { "dropped" : "xxx", "ok" : 1 }
3.2 集合操作
# 创建集合
db.createCollection(name, options)
# name: 集合名称
# options: 可选参数,指定内存大小和索引选项
# capped: 可选,true,创建固定集合,固定大小,当达到最大值会自动覆盖最早文档,如果是true,必须指定size大小
# autoIndexId: 可选,true会自动在_id字段创建索引,默认是false, mongodb3.2之后不支持该参数
# size: 可选,为固定集合指定一个最大字节数
# max: 可选,指定固定集合中包含的文档最大数量
# 往固定集合插入文档中,首先检查size字段,再检查max字段
# 查看集合 show collections 或者 show tables
# demo
use zhongqiang_test
db.createCollection("user")
show collections # user
# 创建固定集合 集合空间最大102400KB, 最多1000个文档
db.createCollection("mycol", { capped : true, autoIndexId : true, size : 102400, max : 1000 } )
# 自动创建集合: 当往集合中插入文档时,如果集合不存在会自动创建
zhongqiang_test> db.zhongqiang_test2.insert({name:"wuzhongqiang", age:28})
DeprecationWarning: Collection.insert() is deprecated. Use insertOne, insertMany, or bulkWrite.
{
acknowledged: true,
insertedIds: { '0': ObjectId('65d1ce9b6c491ae56baf9415') }
}
# show collections
mycol
user
zhongqiang_test2
# 删除集合
db.mycol.drop()
3.3 文档操作
3.3.1 插入文档
# 插入文档 新版本 insertOne, insertMany
# db.collection_name.insertOne(document): 一个文档的插入
# db.user.insertMany(documents): 传递文档数组,批量插入
# insertOne: 如果插入的数据主键存在,会报重复主键异常 ongoServerError: E11000 duplicate key error collection: zhongqiang_test.user index: _id_ dup key: { _id: "65d1d1fc6c491ae56baf9418" }
# 查看文档 db.collection_name.find() 查看集合中的所有文档
# demo
db.user.insertOne({name: "zhongqiang", age: 28})
db.user.insertMany([{name: "zhansan", age: 27}, {name: "lisi", age: 28}])
# 查看里面所有的集合
zhongqiang_test> db.user.find()
[
{
_id: ObjectId('65d1d1a86c491ae56baf9416'),
name: 'zhongqiang',
age: 28
},
{
_id: ObjectId('65d1d1fc6c491ae56baf9417'),
name: 'zhansan',
age: 27
},
{ _id: ObjectId('65d1d1fc6c491ae56baf9418'), name: 'lisi', age: 28 }
3.3.2 查询文档
db.collection_name.find(query, projection) # 还有个findOne, 这个只返回一个符合查询条件的文档
# query:可选参数,使用查询操作符指定查询条件;
# projection:可选参数,使用投影操作符指定返回的键。查询时若要返回文档中所有键值,只需省略该参数即可
# demo
zhongqiang_test> db.user.find({_id: ObjectId('65d1d1a86c491ae56baf9416')}) # 后面加入.pretty()可以使文档格式更加规整,但我没有加这个函数也挺规整
[
{
_id: ObjectId('65d1d1a86c491ae56baf9416'),
name: 'zhongqiang',
age: 777
}
]
# 条件查询
# 等于 {<key>:<value>}, eg: db.col.find({"by":"编程帮"}) 类似: where by = '编程帮'
# 小于 {<key>:{$lt:<value>}}, eg: db.col.find({"likes":{$lt:50}}) 类似: where likes < 50
# 小于或等于 {<key>:{$lte:<value>}}, db.col.find({"likes":{$lte:50}}) 类似: where likes <= 50
# 大于 {<key>:{$gt:<value>}}, eg: db.col.find({"likes":{$gt:50}}) 类似: where likes > 50
# 大于或等于 {<key>:{$gte:<value>}}, eg: db.col.find({"likes":{$gte:50}}) 类似: where likes >= 50
# 不等于 {<key>:{$ne:<value>}}, eg: db.col.find({"likes":{$ne:50}}) 类似: where likes != 50
# 数组中的值 {<key>:{$in:[<value1>, <value2>, ...<valueN>]}}, eg: db.col.find({title:{$in:["编程帮", "MongoDB教程"]}}) 类似: where title in("编程帮", "MongoDB教程")
# 不是数组中的值 {<key>:{$nin:[<value1>, <value2>, ...<valueN>]}} eg: db.col.find({title:{$nin:["编程帮", "MongoDB教程"]}}) 类似: where title not in("编程帮", "MongoDB教程")
# 有了上面的条件,可以用逻辑的与或非等, 实现多个条件的查询
db.collection_name.find({$and:[{<key1>:<value1>}, {<key2>:<value2>}], ...})
# db.mycol.find({$and:[{title:"MongoDB教程"}, {by:"编程帮"}]}).pretty() # $and还可以省略
db.collection_name.find({$or:[{<key1>: <value1>}, {<key2>:<value2>}]})
# db.mycol.find({$or:[{title:"MongoDB教程"}, {by:"编程帮"}]}).pretty()
# and和or连用
# db.mycol.find({"likes": {$gt:100}, $or: [{"by": "编程帮"},{"title": "MongoDB教程"}]}).pretty()
3.3.3 更新文档
# 更新数据, 探索了两种方式,updateOne和replaceOne
# 格式都是: (query, update, options)
# updateOne: 从原来的数据上更新字段,只会更新匹配的第一条记录
# replaceOne: 用新的update数据替换掉符合query的数据,也只匹配第一条记录
db.user.replaceOne({name: "lisi", age: 30}, {name: "wangwu", age: 28}) # 用wangwu,年龄28的文档替换掉namelisi, 年龄30的文档
# 也可以指定id去替换
zhongqiang_test> db.user.replaceOne({_id: "65d1d1fc6c491ae56baf9418"}, {name: "wangwu", age: 666})
{
acknowledged: true,
insertedId: null,
matchedCount: 1,
modifiedCount: 1,
upsertedCount: 0
}
# updateOne
zhongqiang_test> db.user.updateOne({name: "wangwu"}, {$set: {age: 777}}) # 把wangwu的年龄改成777 注意后面的这个写法
{
acknowledged: true,
insertedId: null,
matchedCount: 1,
modifiedCount: 1,
upsertedCount: 0
}
db.user.updateOne({_id: ObjectId('65d1d1a86c491ae56baf9416')}, {$set: {age: 777}}) # ObjectId('65d1d1a86c491ae56baf9416')和单纯的65d1d1a86c491ae56baf9416不是一个id
# update方式 更新现有文档的值
db.collection_name.update(
<query>, # update 的查询条件,类似 SQL 中 update 语句内 where 后面的内容
<update>, # update 的对象和一些更新的操作符(如 $、$inc...)等,也可以理解为 SQL 中 update 语句内 set 后面的内容
{
upsert: <boolean>, # 可选参数,默认值为 false,用来定义当要更新的记录不存在时,是否当作新记录插入到集合中,当值为 true 时表示插入,值为 false 时不插入;
multi: <boolean>, # 可选参数,默认值为 false,用来表示只更新找到的第一条记录,当值为 true 时,则把按条件查出来的多条记录全部更新
writeConcern: <document> # 可选参数,用来定义抛出异常的级别
}
)
3.3.4 删除文档
db.collection_name.remove(
<query>, # 可选参数,定义要删除文档的条件
{
justOne: <boolean>, # 可选参数,如果设为 true 或 1,则只删除一个文档,如果不设置该参数,或使用默认值 false,则删除所有匹配条件的文档;
writeConcern: <document> # 可选参数,定义抛出异常的级别
}
)
# demo
zhongqiang_test> db.user.remove({_id: ObjectId('65d1d1fc6c491ae56baf9418')})
DeprecationWarning: Collection.remove() is deprecated. Use deleteOne, deleteMany, findOneAndDelete, or bulkWrite.
{ acknowledged: true, deletedCount: 1 }
zhongqiang_test> db.user.find()
[
{
_id: ObjectId('65d1d1a86c491ae56baf9416'),
name: 'zhongqiang',
age: 777
},
{
_id: ObjectId('65d1d1fc6c491ae56baf9417'),
name: 'zhansan',
age: 27
},
{ _id: '65d1d1fc6c491ae56baf9418', name: 'wangwu', age: 777 }
]
# 如果使用 remove() 方法时没有指定具体的条件,那么 MongoDB 将删除集合中的所有文档
# DeprecationWarning: Collection.remove() is deprecated. Use deleteOne, deleteMany, findOneAndDelete, or bulkWrite
zhongqiang_test> db.user.remove({})
{ acknowledged: true, deletedCount: 3 }
zhongqiang_test> db.user.find()
3.4 高级查询操作
3.4.1 查询指定字段(投影)
使用mongodb的投影查询某些特定的字段,还是find函数:
# 指定第二个参数
db.collection_name.find(query,{key1:1, key2:1, ...})
# query:可选参数,使用查询操作符指定查询条件;
# key1、key2、...:为要查询或者隐藏的字段,当值为 1 时表示显示该字段,值为 0 时表示隐藏该字段
# demo user集合里面先插入几条数据
db.user.insert([{"name": "wu", "age": 28, "tel": 111}, {"name": "zhong", "age": 33, "tel": 222}, {"name": "qiang", "age": 55, "tel": 333}])
db.user.updateOne({name: "qiang"}, {$set: {age: 33}})
# 查询
db.user.find({}, {"name": 1, "age": 1}) # 不带条件查询
db.user.find({"age": 33}, {"name": 1, "age": 1}) # 带条件查询
[
{ _id: ObjectId('65d55d596c491ae56baf941a'), name: 'zhong', age: 33 },
{ _id: ObjectId('65d55d596c491ae56baf941b'), name: 'qiang', age: 33 }
]
# 在执行 find() 方法时 _id 字段是始终显示的,如果不希望显示此字段,将其显式的置为0
zhongqiang_test> db.user.find({}, {"name": 1, "age": 1, "_id": 0})
[
{ name: 'wu', age: 28 },
{ name: 'zhong', age: 33 },
{ name: 'qiang', age: 33 }
]
3.4.2 限制查询条数
# find() 方法查询集合中文档的时侯,会一次性的将所有符合条件的文档全部展示出来,如果不需要,可以适用limit限制条数
db.collection_name.find().limit(number)
# MongoDB 中还提供了另一种方法 skip(),它同样也可以接收一个数字类型的参数,用来设置要跳过的文档数 skip() 方法的默认参数为 0
db.collection_name.find().skip(number)
# limit() 方法与 skip() 方法可以联合使用来实现类似分页的效果, 比如每页10个去展示 skip(0*10).limit(10) 这个0就是第1页, 第二页就是1..
# demo
zhongqiang_test> db.user.find({}, {"name": 1, "age": 1, "_id": 0})
[
{ name: 'wu', age: 28 },
{ name: 'zhong', age: 33 },
{ name: 'qiang', age: 33 }
]
zhongqiang_test> db.user.find({}, {"name": 1, "age": 1, "_id": 0}).limit(1)
[ { name: 'wu', age: 28 } ]
zhongqiang_test> db.user.find({}, {"name": 1, "age": 1, "_id": 0}).skip(1)
[ { name: 'zhong', age: 33 }, { name: 'qiang', age: 33 } ]
zhongqiang_test> db.user.find({}, {"name": 1, "age": 1, "_id": 0}).skip(1).limit(1)
[ { name: 'zhong', age: 33 } ]
3.4.3 排序
# 对查询的文档排序 sort方法
db.collection_name.find().sort({key:1})
# key 用来定义要根据那个字段进行排序,1 则表示以升序,-1表示降序
# 如果在使用 sort() 方法时未指定排序的选项,那么 sort() 方法将默认按 _id 的升序显示文档
# demo
zhongqiang_test> db.user.find({}, {"name": 1, "age": 1}).sort({"age": -1})
[
{ _id: ObjectId('65d55d596c491ae56baf941a'), name: 'zhong', age: 33 },
{ _id: ObjectId('65d55d596c491ae56baf941b'), name: 'qiang', age: 33 },
{ _id: ObjectId('65d55d596c491ae56baf9419'), name: 'wu', age: 28 }
]
zhongqiang_test> db.user.find({}, {"name": 1, "age": 1}).sort({"name": -1, "age": -1})
[
{ _id: ObjectId('65d55d596c491ae56baf941a'), name: 'zhong', age: 33 },
{ _id: ObjectId('65d55d596c491ae56baf9419'), name: 'wu', age: 28 },
{ _id: ObjectId('65d55d596c491ae56baf941b'), name: 'qiang', age: 33 }
]
3.4.4 索引
索引是特殊的数据结构,存储在一个方便遍历和读取的数据集合中,通过使用索引,可以大大提高查询语句的执行效率。
# createIndex() 方法创建索引
db.collection_name.createIndex(keys, options)
# keys:由键/值对组成,其中键用来定义要创建索引的字段,值用来定义创建索引的顺序,1 表示按升序创建索引,-1 表示按降序来创建索引;
# options:可选参数,其中包含一组控制索引创建的选项
# background Boolean 可选参数,当值为 true 时,表示在后台构建索引,避免在创建索引的过程阻塞其它数据库操作,默认值为 false
# unique Boolean 创建唯一索引,当值为 true 时表示创建唯一索引,以避免重复数据的插入,默认为 false
# name string 索引的名称。如果未指定,MongoDB 将通过连接索引的字段名和排序顺序生成一个索引名称
# dropDups Boolean 在建立唯一索引时是否删除重复记录,设置为 true 则表示创建唯一索引,默认值为 false,3.0 版本之后废弃
# sparse Boolean 对文档中不存在的字段数据不启用索引,这个参数需要特别注意,如果设置为 true 的话,则在索引字段中不会查询出不包含对应字段的文档。默认值为 false
# expireAfterSeconds integer 指定一个以秒为单位的数值,完成 TTL 设定,设定集合的生存时间
# v index version 索引的版本号,默认的索引版本取决于 mongod 创建索引时运行的版本
# weights document 索引权重值,数值在 1 到 99999 之间,表示该索引相对于其他索引字段的得分权重
# default_language string 对于文本索引,该语言用于确定停用词列表以及词干分析器和令牌生成器的规则,默认为英语
# language_override string 对于文本索引,指定文档中包含要替代默认语言的语言的字段名称,默认值为 language
# 创建索引
zhongqiang_test> db.user.createIndex({"name": 1})
name_1
zhongqiang_test> db.user.createIndex({"name": 1, "age": 1})
name_1_age_1
# 查询索引
zhongqiang_test> db.user.getIndexes()
[
{ v: 2, key: { _id: 1 }, name: '_id_' },
{ v: 2, key: { name: 1 }, name: 'name_1' },
{ v: 2, key: { name: 1, age: 1 }, name: 'name_1_age_1' }
]
# 删除索引
# db.collection_name.dropIndex(index): 删除指定索引 db.user.dropIndex({"name":1})
# db.collection_name.dropIndexes(): 删除多个索引, 没有参数
db.user.dropIndex({"name": 1}) or db.user.dropIndex("name_1")
zhongqiang_test> db.user.dropIndexes() # 删除了除_id之外的其他索引
{
nIndexesWas: 3,
msg: 'non-_id indexes dropped for collection',
ok: 1
}
zhongqiang_test> db.user.getIndexes()
[ { v: 2, key: { _id: 1 }, name: '_id_' } ]
3.4.5 聚合查询
聚合操作可以将多个文档中的值组合在一起,并可对数据执行各种操作,以返回单个结果,有点类似于 SQL 语句中的 count(*)、group by
等。
# 聚合操作: aggregate()方法
db.collection_name.aggregate(aggregate_operation)
# 常用的聚合表达式
# $sum 计算总和 db.user.aggregate([{$group: {_id: "$tel", age_sum: {$sum: "$age"}}}]) # group里面_id,是根据哪一列分组, $sum后面是聚合的哪一列
# $avg 计算平均值 db.user.aggregate([{$group: {_id: "$tel", age_avg: {$avg: "$age"}}}])
# $min 获取集合中所有文档对应值得最小值 db.user.aggregate([{$group : {_id : "$tel", age_min: {$min : "$age"}}}])
# $max 获取集合中所有文档对应值得最大值 db.user.aggregate([{$group : {_id : "$tel", age_max: {$max : "$age"}}}])
# $push 在结果文档中把值汇聚成一个列表 db.user.aggregate([{$group : {_id : "$tel", age_push : {$push: "$age"}}}])
# $addToSet 在结果文档把值汇聚成一个集合,去掉重复数据 db.user.aggregate([{$group : {_id : "$tel", age_set : {$addToSet : "$age"}}}])
# $first 根据资源文档的排序获取第一个文档数据 db.user.aggregate([{$group : {_id : "$tel", age_first : {$first : "$age"}}}])
# $last 根据资源文档的排序获取最后一个文档数据 db.user.aggregate([{$group : {_id : "$tel", age_last : {$last : "$age"}}}])
# demo 我创建了一个user集合,里面的文档如下
zhongqiang_test> db.user.find()
[
{
_id: ObjectId('65d55d596c491ae56baf9419'),
name: 'wu',
age: 28,
tel: 111
},
{
_id: ObjectId('65d55d596c491ae56baf941a'),
name: 'zhong',
age: 33,
tel: 222
},
{
_id: ObjectId('65d55d596c491ae56baf941b'),
name: 'qiang',
age: 33,
tel: 333
},
{
_id: ObjectId('65d582506c491ae56baf941c'),
name: 'zhangsan',
age: 44,
tel: 333
},
{
_id: ObjectId('65d597f36c491ae56baf941d'),
name: 'wangwu',
age: 44,
tel: 333
}
]
# sum avg max, min, first, last同理
# 类似于select tel, sum(age) age_sum from user group by tel
zhongqiang_test> db.user.aggregate([{$group: {_id: "$tel", age_sum: {$sum: "$age"}}}])
[
{ _id: 333, age_sum: 121 },
{ _id: 111, age_sum: 28 },
{ _id: 222, age_sum: 33 }
]
# 如果是统计人的个数 $sum后面换成1 类似于select tel, count(*) from user group by tel
zhongqiang_test> db.user.aggregate([{$group: {_id: "$tel", age_sum: {$sum: 1}}}])
[
{ _id: 333, age_sum: 3 },
{ _id: 111, age_sum: 1 },
{ _id: 222, age_sum: 1 }
]
# 汇聚成列表和集合
zhongqiang_test> db.user.aggregate([{$group : {_id : "$tel", age_set : {$addToSet : "$age"}}}])
[
{ _id: 111, age_set: [ 28 ] },
{ _id: 222, age_set: [ 33 ] },
{ _id: 333, age_set: [ 33, 44 ] }
]
zhongqiang_test> db.user.aggregate([{$group : {_id : "$tel", age_push : {$push: "$age"}}}])
[
{ _id: 111, age_push: [ 28 ] },
{ _id: 222, age_push: [ 33 ] },
{ _id: 333, age_push: [ 33, 44, 44 ] }
]
管道操作:Mongodb也支持linux中的管道操作, 一个操作处理完毕直接传递给下一个管道处理
# 聚合框架中常用的操作:
# $project:用于从集合中选择要输出的字段;
# $match:用于过滤数据,只输出符合条件的文档,可以减少作为下一阶段输入的文档数量;
# $group:对集合中的文档进行分组,可用于统计结果;
# $sort:将输入文档进行排序后输出;
# $skip:在聚合管道中跳过指定数量的文档,并返回余下的文档;
# $limit:用来限制 MongoDB 聚合管道返回的文档数量;
# $unwind:将文档中的某一个数组类型字段拆分成多条,每条包含数组中的一个值。
# 单个管道内的操作
db.user.aggregate({$project:{name:1, age:1}}) # 每个文档只显示name和age列, _id列默认都显示,如果想不显示, 需要_id: 0
db.user.aggregate({$skip:1})
# 管道操作示意
db.user.aggregate([{$group : {_id : "$tel", age_push : {$push: "$age"}}}, {$skip:1}, {$limit: 1}]) # aggregate里面可以用列表,连着往后操作
3.5 针对集群操作
3.5.1 复制(副本集)
MongoDB 中的复制就是跨多个服务器同步数据,在多个服务器中存储数据副本,提高数据的可用性和安全性。防止数据丢失,如果硬件发生故障还可以快速恢复。
原理:
MongoDB 使用副本集来实现复制。副本集是一组托管相同数据集的 mongod 实例。在副本中,一个节点是接收所有写操作的主节点,其余的所有实例,例如第二实例,都将应用来自第一个实例的操作,以便它们具有相同的数据集。副本集只能有一个主节点。
- 副本集是一组两个或更多节点(通常最少需要 3 个节点);
- 在副本集中,一个节点是主要节点,其余节点是从节点;
- 所有数据从主节点复制到从节点;
- 在自动故障转移或维护时,将为主节点建立选举,并选举一个新的主节点;
- 恢复失败的节点后,它再次加入副本集并用作辅助节点
具体操作,这个单机也可以玩, 后面的分片也会用到
# 设置副本集
# 将独立的 MongoDB 实例转换为副本集
# 关闭正在运行的mongodb服务器
# 通过指定--replSet选项启动Mongodb服务器 mongod --port "PORT" --dbpath "YOUR_DB_DATA_PATH" --replSet "REPLICA_SET_INSTANCE_NAME"
mongod --port 27023 --shardsvr --dbpath=/www/mongoDB/shard/s3 --logpath=/www/mongoDB/shard/log/s3.log --logappend --replSet shard3 --fork
# 在Mongo客户端中,请使用 rs.initiate() 命令来启动新的副本集。要检查副本集配置,请使用 rs.conf() 命令。要检查副本集的状态,请使用 rs.status() 命令。
mongosh "mongodb://127.0.0.1:27023"
test> rs.initiate() # 初始化的时候,也可以指定成员以及名称等 rs.initiate({_id: "shard3",version: 1, members:[{_id: 0, host: "localhost:27023"}]})
{
info2: 'no configuration specified. Using a default configuration for the set',
me: 'localhost:27023',
ok: 1
}
# 将成员添加到副本集 rs.add(HOST_NAME:PORT) 要将成员添加到副本集,需要在多台计算机上启动 mongod 实例
rs.add("mongod1.net:27017")
# 仅当连接到主节点时,才能将 mongod 实例添加到副本集。要检查您是否连接到主服务器,可以在 mongo 客户端中使用 db.isMaster() 命令
zhongqiang_test> db.isMaster()
{
ismaster: true,
topologyVersion: {
processId: ObjectId('65d16cab2d1eacade07c6921'),
counter: Long('0')
},
maxBsonObjectSize: 16777216,
maxMessageSizeBytes: 48000000,
maxWriteBatchSize: 100000,
localTime: ISODate('2024-02-21T07:36:11.775Z'),
logicalSessionTimeoutMinutes: 30,
connectionId: 19,
minWireVersion: 0,
maxWireVersion: 21,
readOnly: false,
ok: 1,
isWritablePrimary: true
}
3.5.2 分片
分片是跨多台机器存储数据的过程,是MongoDB 满足数据增长需求的方法。随着数据的不断增加,单台机器可能不足以存储全部数据,也无法提供足够的读写吞吐量。通过分片,可以添加更多计算机来满足数据增长和读/写操作的需求。
在上图中,有三个主要组件
- Shards:用于存储实际的数据块,在生产环境中,每个分片都是一个单独的副本集,它们提供了高可用性和数据一致性;
- Config Servers(配置服务器):用于存储集群的元数据,此数据包含集群数据集到Shard的映射。查询路由器使用此元数据将操作定向到特定的Shard。在生产环境中,分片集群恰好具有 3 个配置服务器;
- Query Routers(查询路由器):查询路由器基本上都是 mongo 实例,可与客户端应用程序接口并将操作定向到适当的分片。查询路由器处理操作并将其定位到分片,然后将结果返回给客户端。分片集群可以包含多个查询路由器来划分客户端请求负载。
这个现场玩一下,单机就能玩, 不过遇到了一些坑, 和教程上不太一样了。
# 分片实例各接口端口设置如下:
Shard Server 1:27020
Shard Server 2:27021
Shard Server 3:27022
Shard Server 4:27023
Config Server :27100
Route Process:40001
# 创建shard目录
sudo mkdir -p /www/mongoDB/shard/s1
sudo mkdir -p /www/mongoDB/shard/s2
sudo mkdir -p /www/mongoDB/shard/s3
sudo mkdir -p /www/mongoDB/shard/log
# 赋予写权限
sudo chown `whoami` /www/mongoDB/shard
sudo chown `whoami` /www/mongoDB/shard/log
# 依次启动shared server
mongod --port 27020 --dbpath=/www/mongoDB/shard/s0 --logpath=/www/mongoDB/shard/log/s0.log --logappend --fork
# 没起来,查看日志vim /www/mongoDB/shard/log/s0.log, 发现 "directory":"/www/mongoDB/shard/s0/journal","error":"boost::filesystem::create_directory: Permission denied [system:13]: \"/www/mongoDB/shard/s0/journal\
# 再赋予写权限
sudo chown `whoami` /www/mongoDB/shard/s0
sudo chown `whoami` /www/mongoDB/shard/s1
sudo chown `whoami` /www/mongoDB/shard/s2
sudo chown `whoami` /www/mongoDB/shard/s3
# 再依次启动shared server, 注意这里和教程里面不一样, 这里需要声明成--shared server 并且需要设置成副本集才可以,否则后面增加shared的时候会报错
mongod --port 27021 --shardsvr --dbpath=/www/mongoDB/shard/s1 --logpath=/www/mongoDB/shard/log/s1.log --logappend --replSet shard1 --fork
mongod --port 27022 --shardsvr --dbpath=/www/mongoDB/shard/s2 --logpath=/www/mongoDB/shard/log/s2.log --logappend --replSet shard2 --fork
mongod --port 27020 --shardsvr --dbpath=/www/mongoDB/shard/s0 --logpath=/www/mongoDB/shard/log/s0.log --logappend --replSet shard0 --fork
mongod --port 27023 --shardsvr --dbpath=/www/mongoDB/shard/s3 --logpath=/www/mongoDB/shard/log/s3.log --logappend --replSet shard3 --fork
# 开启服务之后,需要一次对每个副本实例初始化
mongosh "mongodb://127.0.0.1:27020" rs.initiate()
mongosh "mongodb://127.0.0.1:27021" rs.initiate()
mongosh "mongodb://127.0.0.1:27022" rs.initiate()
mongosh "mongodb://127.0.0.1:27023" rs.initiate()
# 启动config server
mkdir -p /www/mongoDB/shard/config
sudo chown `whoami` /www/mongoDB/shard/config
# 这里也需要加上configsvr参数启动配置服务器, 也必须设置成副本集模式
mongod --port 27100 --configsvr --dbpath=/www/mongoDB/shard/config --logpath=/www/mongoDB/shard/log/config.log --logappend --replSet config_replset --fork
# 进入并初始化
mongosh "mongodb://127.0.0.1:27100"
rs.initiate( {_id: "config_replset",configsvr: true,version: 1, members:[{_id: 0, host: "127.0.0.1:27100"}]})
# 启动router process
mongos --port 40000 --configdb localhost:27100 --fork --logpath=/www/mongoDB/shard/log/route.log
# 新版的mongodb 需要的configdb必须是副本集了, 如果上面不把configdb设置成副本集, 会报错BadValue: configdb supports only replica set connection string
# 这样可以正常启动起来
[direct: mongos] admin> db.runCommand({ addshard:"localhost:27020" })
MongoServerError[OperationFailed]: host is part of set shard0; use replica set url format <setname>/<server1>,<server2>, ...
# 上面这个原因是版本不匹配,用下面这个
[direct: mongos] admin> sh.addShard("shard0/localhost:27020")
{
shardAdded: 'shard0',
ok: 1,
'$clusterTime': {
clusterTime: Timestamp({ t: 1708512966, i: 6 }),
signature: {
hash: Binary.createFromBase64('AAAAAAAAAAAAAAAAAAAAAAAAAAA=', 0),
keyId: Long('0')
}
},
operationTime: Timestamp({ t: 1708512966, i: 6 })
}
# 其他的一样
[direct: mongos] admin> sh.addShard("shard0/localhost:27021")
[direct: mongos] admin> sh.addShard("shard0/localhost:27022")
[direct: mongos] admin> sh.addShard("shard0/localhost:27023")
[direct: mongos] admin> sh.addShard("shard0/localhost:27024")
# 设置分片存储的数据库
[direct: mongos] admin> db.runCommand({ enablesharding:"test"})
{
ok: 1,
'$clusterTime': {
clusterTime: Timestamp({ t: 1708513161, i: 8 }),
signature: {
hash: Binary.createFromBase64('AAAAAAAAAAAAAAAAAAAAAAAAAAA=', 0),
keyId: Long('0')
}
},
operationTime: Timestamp({ t: 1708513161, i: 2 })
}
[direct: mongos] admin> db.runCommand({ shardcollection: "test.log", key: { id:1,time:1}})
{
collectionsharded: 'test.log',
ok: 1,
'$clusterTime': {
clusterTime: Timestamp({ t: 1708513187, i: 36 }),
signature: {
hash: Binary.createFromBase64('AAAAAAAAAAAAAAAAAAAAAAAAAAA=', 0),
keyId: Long('0')
}
},
operationTime: Timestamp({ t: 1708513187, i: 36 })
}
3.6 备份和恢复
MongoDB导入导出和备份的命令工具从4.4版本开始不再自动跟随数据库一起安装,而是需要自己手动安装 https://www.mongodb.com/try/download/database-tools
# 下载对应版本,解压,加入到环境变量
tar -zxvf mongodb-database-tools-ubuntu2204-x86_64-100.9.4.tgz
mv mongodb-database-tools /usr/local/mongodb
sudo vim /etc/profile
export MONGODB_HOME=/usr/local/mongodb
export PATH=${MONGODB_HOME}/bin:${MONGODB_HOME}/mongodb-database-tools/bin:$PATH
source /etc/profile
# 在MongoDB中可以使用mongodump命令来对MongoDB进行数据备份,该命令可以导出所有数据到指定目录中,也可以通过参数将导出数据转存的服务器。其语法格式如下:
mongodump -h dbhost -d dbname -o dbdirectory
# -h:MongDB 所在服务器的地址,例如:127.0.0.1,同时也可以指定端口号,例如:127.0.0.1:27017;
# -d:需要备份的数据库实例,例如:test;
# -o:备份数据存放的位置,例如:c:\data\dump,该目录需要提前建立,在备份完成后,系统会自动在 dump 目录下建立一个 test 目录,并在这个目录里面存放该数据库实例的备份数据。
# demo
$ mkdir mongodb_beifen
$ mongodump -h 127.0.0.1:27017 -o ~/mongodb_beifen # 也可以通过-d参数指定具体的数据库
2024-02-21T21:24:08.417+0800 writing admin.system.version to /home/wuzhongqiang/mongodb_beifen/admin/system.version.bson
2024-02-21T21:24:08.418+0800 done dumping admin.system.version (1 document)
2024-02-21T21:24:08.419+0800 writing zhongqiang_test.zhongqiang_test2 to /home/wuzhongqiang/mongodb_beifen/zhongqiang_test/zhongqiang_test2.bson
2024-02-21T21:24:08.419+0800 writing zhongqiang_test.user to /home/wuzhongqiang/mongodb_beifen/zhongqiang_test/user.bson
2024-02-21T21:24:08.420+0800 done dumping zhongqiang_test.zhongqiang_test2 (1 document)
2024-02-21T21:24:08.420+0800 done dumping zhongqiang_test.user (5 documents)
$ ls mongodb_beifen
admin zhongqiang_test
# 恢复
mongorestore --host <hostname> --port <port> --username <username> --password <password> <backup_directory>
# demo
mongorestore -h 127.0.0.1:27017 ~/mongodb_beifen
$ mongorestore -h 127.0.0.1:27017 ~/mongodb_beifen # 当然我这个已经有一份数据了,所以会报重复
2024-02-21T21:26:18.940+0800 preparing collections to restore from
2024-02-21T21:26:18.941+0800 reading metadata for zhongqiang_test.user from /home/wuzhongqiang/mongodb_beifen/zhongqiang_test/user.metadata.json
2024-02-21T21:26:18.941+0800 reading metadata for zhongqiang_test.zhongqiang_test2 from /home/wuzhongqiang/mongodb_beifen/zhongqiang_test/zhongqiang_test2.metadata.json
2024-02-21T21:26:18.942+0800 restoring to existing collection zhongqiang_test.user without dropping
2024-02-21T21:26:18.942+0800 restoring to existing collection zhongqiang_test.zhongqiang_test2 without dropping
2024-02-21T21:26:18.942+0800 restoring zhongqiang_test.user from /home/wuzhongqiang/mongodb_beifen/zhongqiang_test/user.bson
2024-02-21T21:26:18.942+0800 restoring zhongqiang_test.zhongqiang_test2 from /home/wuzhongqiang/mongodb_beifen/zhongqiang_test/zhongqiang_test2.bson
2024-02-21T21:26:18.946+0800 continuing through error: E11000 duplicate key error collection: zhongqiang_test.zhongqiang_test2 index: _id_ dup key: { _id: ObjectId('65d1ce9b6c491ae56baf9415') }
2024-02-21T21:26:18.947+0800 continuing through error: E11000 duplicate key error collection: zhongqiang_test.user index: _id_ dup key: { _id: ObjectId('65d55d596c491ae56baf9419') }
2024-02-21T21:26:18.948+0800 continuing through error: E11000 duplicate key error collection: zhongqiang_test.user index: _id_ dup key: { _id: ObjectId('65d55d596c491ae56baf941a') }
2024-02-21T21:26:18.948+0800 continuing through error: E11000 duplicate key error collection: zhongqiang_test.user index: _id_ dup key: { _id: ObjectId('65d55d596c491ae56baf941b') }
2024-02-21T21:26:18.948+0800 continuing through error: E11000 duplicate key error collection: zhongqiang_test.user index: _id_ dup key: { _id: ObjectId('65d582506c491ae56baf941c') }
2024-02-21T21:26:18.948+0800 continuing through error: E11000 duplicate key error collection: zhongqiang_test.user index: _id_ dup key: { _id: ObjectId('65d597f36c491ae56baf941d') }
2024-02-21T21:26:18.953+0800 finished restoring zhongqiang_test.zhongqiang_test2 (0 documents, 1 failure)
2024-02-21T21:26:18.953+0800 finished restoring zhongqiang_test.user (0 documents, 5 failures)
2024-02-21T21:26:18.953+0800 no indexes to restore for collection zhongqiang_test.zhongqiang_test2
2024-02-21T21:26:18.954+0800 no indexes to restore for collection zhongqiang_test.user
2024-02-21T21:26:18.954+0800 0 document(s) restored successfully. 6 document(s) failed to restore.
3.7 监控
mongostat
命令能够检查所有正在运行的 mongod 实例的状态,并返回数据库操作的计数器。这些计数器包括插入、查询、更新、删除和游标。当内存不足、写入量不足或者出现一些性能问题时,该命令还会显示发生错误的时间,并显示锁定百分比
mongotop
命令可以跟踪并报告 MongoDB 实例的读写活动。默认情况下,mongotop 能够提供每个集合的水平统计数据,并每秒钟返回一次,您也可以根据需要对其进行修改。
$ mongostat
insert query update delete getmore command dirty used flushes vsize res qrw arw net_in net_out conn time
*0 *0 *0 *0 0 0|0 0.0% 0.0% 0 2.55G 128M 0|0 0|0 111b 69.2k 10 Feb 21 21:28:06.521
*0 *0 *0 *0 0 0|0 0.0% 0.0% 0 2.55G 128M 0|0 0|0 111b 69.5k 10 Feb 21 21:28:07.525
*0 *0 *0 *0 0 3|0 0.0% 0.0% 0 2.55G 128M 0|0 0|0 298b 70.4k 10 Feb 21 21:28:08.524
*0 *0 *0 *0 0 1|0 0.0% 0.0% 0 2.55G 128M 0|0 0|0 113b 70.7k 10 Feb 21 21:28:09.511
*0 *0 *0 *0 0 0|0 0.0% 0.0% 0 2.55G 128M 0|0 0|0 110b 68.9k 10 Feb 21 21:28:10.524
*0 *0 *0 *0 0 1|0 0.0% 0.0% 0 2.55G 128M 0|0 0|0 112b 69.8k 10 Feb 21 21:28:11.523
*0 *0 *0 *0 0 1|0 0.0% 0.0% 0 2.55G 128M 0|0 0|0 112b 69.8k 10 Feb 21 21:28:12.522
*0 *0 *0 *0 0 0|0 0.0% 0.0% 0 2.55G 128M 0|0 0|0 111b 69.6k 10 Feb 21 21:28:13.524
*0 *0 *0 *0 0 1|0 0.0% 0.0% 0 2.55G 128M 0|0 0|0 112b 69.8k 10 Feb 21 21:28:14.522
*0 *0 *0 *0 0 2|0 0.0% 0.0% 0 2.55G 128M 0|0 0|0 176b 71.1k 10 Feb 21 21:28:15.508
$ mongotop
2024-02-21T21:30:39.593+0800 connected to: mongodb://localhost/
ns total read write 2024-02-21T21:30:40+08:00
admin.atlascli 0ms 0ms 0ms
admin.system.version 0ms 0ms 0ms
config.system.sessions 0ms 0ms 0ms
config.transactions 0ms 0ms 0ms
local.system.replset 0ms 0ms 0ms
zhongqiang_test.user 0ms 0ms 0ms
zhongqiang_test.zhongqiang_test 0ms 0ms 0ms
zhongqiang_test.zhongqiang_test2 0ms 0ms 0ms
3.8 Python操作mongodb
教程中列了java和php如何操作mongodb, 而我工作中主要的语言是python, 所以这里我主要是写下python如何操作mongodb数据库,需要安装pymongo包
# 安装pip3 install pymongo
import pymongo
# 创建客户端实例
myclient = pymongo.MongoClient("mongodb://localhost:27017/")
# 展示数据库
dblist = myclient.list_database_names()
print(dblist)
# 选择数据库
mydb = myclient["runoobdb"]
# 展示集合
collist = mydb.list_collection_names()
print(collist)
# 选择集合
mycol = mydb["sites"]
# 删除集合
mycol.drop()
# 文档的操作
# 添加数据
# 集合中插入文档使用 **insert_one()** 方法,该方法的第一参数是字典 **name => value** 对, 该对象返回InsertOneResult,包含inserted_id属性,它是插入文档的id值
mydict = { "name": "Toby", "age": "23", "url": "https://juejin.cn/user/3403743731649863" }
x = mycol.insert_one(mydict)
print(x, inserted_id)
mylist = [
{ "name": "Tom", "age": "100", "url": "https://juejin.cn/user/3403743731649863" },
{ "name": "Mary", "age": "101", "url": "https://juejin.cn/user/3403743731649863" },
{ "name": "Timi", "age": "10", "url": "https://juejin.cn/user/3403743731649863" },
]
x = mycol.insert_many(mylist)
# 输出插入的所有文档对应的 _id 值
print(x.inserted_ids)
# 查询数据
# MongoDB 中使用了 find 和 find_one 方法来查询集合中的数据,它类似于 SQL 中的 SELECT 语句。
# 查询col_set的第一条记录
x = mycol.find_one()
print(x)
# 查询集合中的所有记录
for x in mycol.find():
print(x)
# 查询指定字段的数据
for x in mycol.find({}, {"_id": 0, "name": 1, "age": 1}):
print(x) # 只会输出age和name字段
# 注意: 除了 _id,你不能在一个对象中同时指定 0 和 1,如果你设置了一个字段为 0,则其他都为 1,反之亦然
# 这种会报错for x in mycol.find({},{ "name": 1, "alexa": 0 })
# 根据指定条件查询
myquery = {"name": "Toby"} # 正则表达式查询 myquery = { "name": { "$regex": "^R" } }
mydoc = mycol.find(myquery)
for x in mydoc:
print(x)
# 返回指定条数的记录
myresult = mycol.find().limit(3)
for x in myresult:
print(x)
# 更改数据
myquery = {"age": "23"}
newvalues = {"$set": {"age": "123"}}
mycol.update_one(myquery, newvalues)
for x in mycol.find():
print(x)
# 数据排序
mydoc = mycol.find().sort("age") # 默认是1,正序, 如果逆序,指定-1, sort("age", -1)
for x in mydoc:
print(x)
# 删除数据
# 一条
myquery = { "name": "Timi" }
mycol.delete_one(myquery)
# 多条
myquery = { "name": {"$regex": "^F"} }
x = mycol.delete_many(myquery)
# 删除所有数据
x = mycol.delect_many({})
# 删除后输出
for x in mycol.find():
print(x)
# 最后再整理一个工作中pyspark读取mongodb的操作
spark = SparkSession.builder \
.config("spark.master", "k8s://https://nc4-serving-k8s.kube-apiserver.cc.d.xiaomi.net:6443") \
.config("spark.kubernetes.namespace", "ad-inf-data") \
.config("spark.kubernetes.authenticate.oauthToken", token) \
.config("spark.kubernetes.container.image", "micr.cloud.mioffice.cn/spark-k8s/apache/spark-py:spark-3.1.2-mdh-v4") \
.config("spark.kubernetes.executor.apiPollingInterval", "300s") \
.config("spark.executor.memory", "16g") \
.config("spark.driver.host", ip) \
.config("spark.sql.execution.arrow.enabled", "true") \
.config("spark.jars.packages", "org.mongodb.spark:mongo-spark-connector:10.0.2") \
.config("spark.mongodb.read.database", database) \
.config("spark.mongodb.read.collection", collection) \
.config("spark.mongodb.read.connection.uri", uri) \
.getOrCreate()
sc = spark.sparkContext
df = spark.read.format("mongodb").load(schema=schema)
4. MongoDB高级
4.1 多表关联
4.1.1 关系(文档之间的关联)
Mongodb的关系表示文档与文档之间的关联,文档之间可以通过嵌入或引用来建立联系,这种联系可以是 1:1(1对1)、1:N(1对多)、N:1(多对1)、N:N(多对多)
# 这里用pymongo走两个demo
import pymongo
from bson import ObjectId
# 创建客户端实例
myclient = pymongo.MongoClient("mongodb://localhost:27017/")
dblist = myclient.list_database_names()
print("dblist", dblist)
mydb = myclient["zhongqiang_test"]
collist = mydb.list_collection_names()
print("collections", collist)
collection_user = mydb["user"]
print(list(collection_user.find()))
# 插入3条数据 (嵌入的关系)
# 直接把地址信息嵌入到用户文档
users = [
{ "name": "Tom", "age": "100", "address": [{"place": "beijing", "country": "china"}, {"place": "shanghai", "country": "china"}]},
{ "name": "Mary", "age": "101", "address": [{"place": "jiujinshan", "country": "america"}]},
{ "name": "Timi", "age": "10", "address": [{"place": "biandian", "country": "yidali"}, {"place": "nanjing", "country": "china"}]},
]
# result = collection_user.insert_many(users)
# print(result.inserted_ids)
# 创建一个address集合
address = [
{"place": "beijing", "country": "china"},
{"place": "shanghai", "country": "china"},
{"place": "jiujinshan", "country": "america"},
{"place": "biandian", "country": "yidali"},
{"place": "nanjing", "country": "china"}
]
collection_address = mydb['address']
# result = collection_address.insert_many(address)
# print(result.inserted_ids)
# 重新建立一个集合, 此时采用引用的方式添加文档
# 设计数据库时经常用到的方法,在这种方法中,用户文档和用户地址文档是分开的,通过引用文档的 id 字段来建立它们之间的关系
collection_user2 = mydb['users2']
users = [
{ "name": "Tom", "age": "100", "address_ids": [ObjectId('65d6b2ce3c3e5b7e1a7b1d58'), ObjectId('65d6b2ce3c3e5b7e1a7b1d59')]},
{ "name": "Mary", "age": "101", "address_ids": [ObjectId('65d6b2ce3c3e5b7e1a7b1d5a')]},
{ "name": "Timi", "age": "10", "address_ids": [ObjectId('65d6b2ce3c3e5b7e1a7b1d5b'), ObjectId('65d6b2ce3c3e5b7e1a7b1d5c')]},
]
# result = collection_user2.insert_many(users)
# print(result.inserted_ids)
# 嵌入方式: 所有相关数据都保存在一个文档中的方式,可以使得文档的检索和维护变的更加容易, 缺点:数据量不断变大时,会大大影响数据库的读写性能
print(list(collection_user.find({"name": "Tom"}, {"address": 1, "_id": 0}))[0]['address'])
# [{'place': 'beijing', 'country': 'china'}, {'place': 'shanghai', 'country': 'china'}]
# 引用方式: 文档之间做到解耦, 便于维护,保证查询性能, 缺点就是检索的时候可能会复杂
# 比如想查Tom的地址,需要先从user集合中拿到adderss_ids, 然后再从地址集合中找到具体的地址
address_ids_tom = list(collection_user2.find({"name": "Tom"}, {"address_ids": 1}))[0]
print(list(collection_address.find({"_id":{"$in":address_ids_tom["address_ids"]}}, {"_id": 0})))
# [{'place': 'beijing', 'country': 'china'}, {'place': 'shanghai', 'country': 'china'}]
4.1.2 DBRefs
为了在 MongoDB 中实现规范化的数据库结构,我们使用了引用式关系(也称为手动引用)的概念,在手动引用中,我们需要将被引用文档的 _id 存储在其他文档中。当文档中需要引用来自不同集合数据的情况下,我们可以使用 MongoDB 中的 DBRefs(Database References).
有一个 users 集合,用来存储用户信息,其它一些集合(例如 address_home、address_office、address_mailing 等),用来存储不同类型的地址数据。当我们需要通过 users 集合来引用这些存有地址信息的集合时,需要根据地址类型来指定要查看的集合,在这种文档需要引用其它多个集合中文档的情况下,我们可以使用 DBRefs。
# 语法
{ $ref : value, $id : value, $db : value }
# $ref:此字段用来指定要引用文档所在的集合;
# $id:此字段用来指定要引用文档的 _id 字段值;
# $db:可选字段,用来指定要引用文档所在的数据库名称;
# value:表示各个字段所对应的值
# demo
# 重新建立一个user3集合, 引用方式添加文档,且加上被引用文档的数据库以及集合信息
collection_user3 = mydb['users3']
users = [
{ "name": "Tom", "age": "100", "address_ids": [{"$ref": "address", "$id": ObjectId('65d6b2ce3c3e5b7e1a7b1d58'), "$db": "zhongqiang_test"}, {"$ref": "address", "$id": ObjectId('65d6b2ce3c3e5b7e1a7b1d59'), "$db": "zhongqiang_test"}]},
{ "name": "Mary", "age": "101", "address_ids": [{"$ref": "address", "$id": ObjectId('65d6b2ce3c3e5b7e1a7b1d5a'), "$db": "zhongqiang_test"}]},
{ "name": "Timi", "age": "10", "address_ids": [{"$ref": "address", "$id": ObjectId('65d6b2ce3c3e5b7e1a7b1d5b'), "$db": "zhongqiang_test"}, {"$ref": "address", "$id": ObjectId('65d6b2ce3c3e5b7e1a7b1d5c'), "$db": "zhongqiang_test"}]},
]
# result = collection_user3.insert_many(users)
# print(result.inserted_ids)
address_ids_tom = list(collection_user3.find({"name": "Tom"}, {"address_ids": 1}))[0]
result = []
for address_id_tom in address_ids_tom['address_ids']: # address_id_tom是DBRef类
print(address_id_tom.collection, address_id_tom.id, address_id_tom.database)
# 这里就可以加入if来指定特定的数据库以及特定的集合, 获取指定数据库,指定集合里面的地址等
address = myclient[address_id_tom.database][address_id_tom.collection].find_one({"_id": address_id_tom.id}, {"_id": 0})
if address:
result.append(address)
print(result) # [{'place': 'beijing', 'country': 'china'}, {'place': 'shanghai', 'country': 'china'}]
# 上面如果是想看某个人,在特定集合里面的地址信息等,我觉得这样存储反而会更快
# users = [
# { "name": "Tom", "age": "100", "address_ids": {"zhongqiang_test": {"collection_address": [ObjectId('65d6b2ce3c3e5b7e1a7b1d58'), ObjectId('65d6b2ce3c3e5b7e1a7b1d59')]}}},
# { "name": "Mary", "age": "101", "address_ids": {"zhongqiang_test": {"collection_address": [ObjectId('65d6b2ce3c3e5b7e1a7b1d5a')]}}},
# { "name": "Timi", "age": "10", "address_ids": {"zhongqiang_test": {"collection_address": [ObjectId('65d6b2ce3c3e5b7e1a7b1d5b'), ObjectId('65d6b2ce3c3e5b7e1a7b1d5c')]}}}
# ]
这个东西类似于基于外键进行查询, 在多表联合查询中还是非常有用的, 当然这个也和文档的设计有关系, 到底是谁是的外键,这个需要看设计,比如拿发布文章举例, 一个人可以发布多篇文章, 可以查询人发布了哪些文章,也可以查一篇文章的作者, 需求不同, 这个外键的设计方式也在会有所不同。
4.2 再谈索引
4.2.1 覆盖索引查询
列索引是通过最大限度地减少查询所需的磁盘访问次数来优化查询性能的好方法。
MongoDB 有一个字段索引的特定应用程序,称为覆盖索引查询(Covered Queries),覆盖索引查询是以下查询:
- 所有查询字段是索引的一部分
- 所有查询返回字段在同一个索引中
由于所有出现在查询中的字段是索引的一部分, MongoDB 无需在整个数据文档中检索匹配查询条件和返回使用相同索引的查询结果。因为索引存在于RAM中,从索引中获取数据比通过扫描文档读取数据要快得多。
# 在db.user集合中创建索引
zhongqiang_test> db.user.createIndex({age:1,name:1})
age_1_name_1
# 分析下面两种查询方式的不同
# 这种查询方式速度会很快, 原因是MongoDB的不会去数据库文件中查找。相反,它会从索引中提取数据,这是非常快速的数据查询
zhongqiang_test> db.user.find({age:'10'},{name:1,_id:0})
[ { name: 'Timi' } ]
# 下面这个就不一样了, 这个会遍历数据库文档, 原因是MongoDB 在默认情况下会在每个查询中返回 _id 字段, 而这个_id字段没有建立在索引里面, 需要遍历文档去找
# 所以下面这种方式没法使用覆盖索引去查
zhongqiang_test> db.user.find({age:'10'},{name:1})
[ { _id: ObjectId('65d6b2b397cee8b32319cc68'), name: 'Timi' } ]
# 另外,如果是以下的查询,也不能使用覆盖索引查询:
# 所有索引字段是一个数组;
# 所有索引字段是一个子文档
4.2.2 高级索引
高级索引解决的问题:就是假设文档里面嵌入文档,或者文档里面的字段是数组类型,还想用嵌入文档里面的字段,或者是数组里面的值作查询的时候, 此时应该怎么去建立一个索引提高查询效率。
# 再建立一个users4集合,插入一条数据
collection_user4 = mydb['users4']
users = [
{
"address": {
"city": "Los Angeles",
"state": "California",
"pincode": "123"
},
"tags": [
"music",
"cricket",
"blogs"
],
"name": "Tom Benzamin"
}
]
result = collection_user4.insert_many(users)
# tags是一个列表, 假设想基于里面的值进行查找
zhongqiang_test> db.users4.find({tags:"cricket"}).explain()
{
explainVersion: '1',
queryPlanner: {
namespace: 'zhongqiang_test.users4',
indexFilterSet: false,
parsedQuery: { tags: { '$eq': 'cricket' } },
queryHash: 'D1C3BFDB',
planCacheKey: 'D1C3BFDB',
maxIndexedOrSolutionsReached: false,
maxIndexedAndSolutionsReached: false,
maxScansToExplodeReached: false,
winningPlan: {
stage: 'COLLSCAN', # 集合扫描
filter: { tags: { '$eq': 'cricket' } },
direction: 'forward'
},
rejectedPlans: []
},
command: {
find: 'users4',
filter: { tags: 'cricket' },
'$db': 'zhongqiang_test'
},
# 此时也可以查找出结果,但是是基于集合扫描的,没有基于索引
zhongqiang_test> db.users4.find({"address.city":"Los Angeles"}).explain() # 这个也是同理
# 下面我们给这种嵌套里面的字段或数组里面的值建立索引,让其查询性能提高
zhongqiang_test> db.users4.createIndex({"tags":1}) # 为数组 tags 创建索引时,会为 music、cricket、blogs三个值建立单独的索引
tags_1
zhongqiang_test> db.users4.createIndex({"address.city":1,"address.state":1,"address.pincode":1}) # 嵌入文档里面的每个字段建立索引
address.city_1_address.state_1_address.pincode_1
# 此时再查询
zhongqiang_test> db.users4.find({tags:"cricket"}).explain()
{
explainVersion: '1',
queryPlanner: {
namespace: 'zhongqiang_test.users4',
indexFilterSet: false,
parsedQuery: { tags: { '$eq': 'cricket' } },
queryHash: 'D1C3BFDB',
planCacheKey: '41CA82BC',
maxIndexedOrSolutionsReached: false,
maxIndexedAndSolutionsReached: false,
maxScansToExplodeReached: false,
winningPlan: {
stage: 'FETCH',
inputStage: {
stage: 'IXSCAN', # 基于索引去查询了
keyPattern: { tags: 1 },
indexName: 'tags_1',
isMultiKey: true,
multiKeyPaths: { tags: [ 'tags' ] },
isUnique: false,
isSparse: false,
isPartial: false,
indexVersion: 2,
direction: 'forward',
indexBounds: { tags: [ '["cricket", "cricket"]' ] }
}
},
rejectedPlans: []
},
4.2.3 全文检索
全文检索对每一个词建立一个索引,指明该词在文章中出现的次数和位置,当用户查询时,检索程序就根据事先建立的索引进行查找,并将查找的结果反馈给用户的检索方式。
这个过程类似于通过字典中的检索字表查字的过程。
# demo
db.posts.insert({
"post_text": "enjoy the mongodb articles on Runoob",
"tags": [
"mongodb",
"runoob"
]
})
# 没有建立全文检索之前
zhongqiang_test> db.posts.find({$text:{$search:"runoob"}})
MongoServerError[IndexNotFound]: text index required for $text query
# 为text建立全文检索
zhongqiang_test> db.posts.createIndex({post_text:"text"})
post_text_text
zhongqiang_test> db.posts.find({$text:{$search:"runoob"}})
[
{
_id: ObjectId('65d72bdff23e1f105cd04be4'),
post_text: 'enjoy the mongodb articles on Runoob',
tags: [ 'mongodb', 'runoob' ]
}
]
# 删除全文检索
db.posts.getIndexes()
db.posts.dropIndex("post_text_text")
4.2.4 正则表达式
正则表达式是使用单个字符串来描述、匹配一系列符合某个句法规则的字符串。MongoDB 使用 $regex 操作符来设置匹配字符串的正则表达式。
不同于全文检索,我们使用正则表达式不需要做任何配置。
# demo
db.posts.insertOne({
"post_text": "enjoy the mongodb articles on runoob",
"tags": [
"mongodb",
"runoob"
]
})
# 正则表达式查找
zhongqiang_test> db.posts.find({"post_text":{$regex:"runoob"}})
[
{
_id: ObjectId('65d72e87f23e1f105cd04be5'),
post_text: 'enjoy the mongodb articles on runoob',
tags: [ 'mongodb', 'runoob' ]
}
]
# 不区分大小写, 加i选项
zhongqiang_test> db.posts.find({post_text:{$regex:"runoob",$options:"i"}}) # 不是$i, 可能新版本不需要$了
[
{
_id: ObjectId('65d72bdff23e1f105cd04be4'),
post_text: 'enjoy the mongodb articles on Runoob',
tags: [ 'mongodb', 'runoob' ]
},
{
_id: ObjectId('65d72e87f23e1f105cd04be5'),
post_text: 'enjoy the mongodb articles on runoob',
tags: [ 'mongodb', 'runoob' ]
}
]
# 还可以在数组字段中使用正则表达式来查找内容。 这在标签的实现上非常有用
zhongqiang_test> db.posts.find({tags:{$regex:"run"}})
[
{
_id: ObjectId('65d72bdff23e1f105cd04be4'),
post_text: 'enjoy the mongodb articles on Runoob',
tags: [ 'mongodb', 'runoob' ]
},
{
_id: ObjectId('65d72e87f23e1f105cd04be5'),
post_text: 'enjoy the mongodb articles on runoob',
tags: [ 'mongodb', 'runoob' ]
}
]
# 如果你的文档中字段设置了索引,那么使用索引相比于正则表达式匹配查找所有的数据查询速度更快。
# 如果正则表达式是前缀表达式,所有匹配的数据将以指定的前缀字符串为开始。例如: 如果正则表达式为 ^tut ,查询语句将查找以 tut 为开头的字符串
# 这里面使用正则表达式有两点需要注意:
# 正则表达式中使用变量。一定要使用eval将组合的字符串进行转换,不能直接将字符串拼接后传入给表达式。否则没有报错信息,只是结果为空!
# 这个在python中好像没验通
query = "runoob"
collection_posts = mydb['posts']
print(list(collection_posts.find({"post_text":{"$regex":query,"$options":"i"}})))
# [{'_id': ObjectId('65d72bdff23e1f105cd04be4'), 'post_text': 'enjoy the mongodb articles on Runoob', 'tags': ['mongodb', 'runoob']}, {'_id': ObjectId('65d72e87f23e1f105cd04be5'), 'post_text': 'enjoy the mongodb articles on runoob', 'tags': ['mongodb', 'runoob']}]
4.2.5 查询分析
查询分析是衡量数据库和索引设计有效性的一个非常重要的方式。
MongoDB 查询分析常用函数有:explain()
和 hint()
。
explain()
操作提供了查询信息,使用索引及查询统计等。结果将查询计划以阶段树的形式呈现,有利于我们对索引的优化hint()
操作也叫“强制查询优化器”, 虽然MongoDB查询优化器一般工作的很不错,但也可以使用 hint 来强制 MongoDB 使用一个指定的索引,以此来测试查询的性能
zhongqiang_test> db.user.find({age:'10'},{name:1,_id:0}).explain()
{
explainVersion: '1',
queryPlanner: { # 被查询优化器选择出来的查询计划
namespace: 'zhongqiang_test.user', # 指定运行查询的命名空间(即<database>.<collection>)
indexFilterSet: false, # boolan值,表示MongoDB 对于此query shape 是否使用了索引过滤器
parsedQuery: { age: { '$eq': '10' } },
queryHash: '0CE5692E',
planCacheKey: '21C95C51',
maxIndexedOrSolutionsReached: false,
maxIndexedAndSolutionsReached: false,
maxScansToExplodeReached: false,
winningPlan: { # 文档类型,详细显示查询优化程序选择的查询计划
stage: 'PROJECTION_COVERED', # 阶段名称。每个阶段都有每个阶段特有的信息。 例如,IXSCAN 阶段将包括索引边界以及特定于索引扫描的其他数据。 如果阶段具有子阶段或多个子阶段,则阶段将具有inputStage 或 inputStages
transformBy: { name: 1, _id: 0 },
inputStage: { # 描述子阶段的文档。
stage: 'IXSCAN', # 索引扫描
keyPattern: { age: 1, name: 1 },
indexName: 'age_1_name_1',
isMultiKey: false,
multiKeyPaths: { age: [], name: [] },
isUnique: false,
isSparse: false,
isPartial: false,
indexVersion: 2,
direction: 'forward',
indexBounds: { age: [ '["10", "10"]' ], name: [ '[MinKey, MaxKey]' ] }
}
},
rejectedPlans: [] # 查询优化器考虑和拒绝的候选计划数组
},
command: {
find: 'user',
filter: { age: '10' },
projection: { name: 1, _id: 0 },
'$db': 'zhongqiang_test'
},
serverInfo: {
host: 'wuzhongqiang-Mi-Laptop-Pro-15-2020',
port: 27017,
version: '7.0.5',
gitVersion: '7809d71e84e314b497f282ea8aa06d7ded3eb205'
},
serverParameters: {
internalQueryFacetBufferSizeBytes: 104857600,
internalQueryFacetMaxOutputDocSizeBytes: 104857600,
internalLookupStageIntermediateDocumentMaxSizeBytes: 104857600,
internalDocumentSourceGroupMaxMemoryBytes: 104857600,
internalQueryMaxBlockingSortMemoryUsageBytes: 104857600,
internalQueryProhibitBlockingMergeOnMongoS: 0,
internalQueryMaxAddToSetBytes: 104857600,
internalDocumentSourceSetWindowFieldsMaxMemoryBytes: 104857600,
internalQueryFrameworkControl: 'trySbeRestricted'
},
ok: 1
}
zhongqiang_test> db.user.find({age:'10'},{name:1}).explain()
{
explainVersion: '1',
queryPlanner: {
namespace: 'zhongqiang_test.user',
indexFilterSet: false,
parsedQuery: { age: { '$eq': '10' } },
queryHash: 'FF7496DC',
planCacheKey: '92D16C0D',
maxIndexedOrSolutionsReached: false,
maxIndexedAndSolutionsReached: false,
maxScansToExplodeReached: false,
winningPlan: {
stage: 'PROJECTION_SIMPLE',
transformBy: { name: 1 },
inputStage: {
stage: 'FETCH', # 检出文档
inputStage: {
stage: 'IXSCAN', # 索引扫描
keyPattern: { age: 1, name: 1 },
indexName: 'age_1_name_1',
isMultiKey: false,
multiKeyPaths: { age: [], name: [] },
isUnique: false,
isSparse: false,
isPartial: false,
indexVersion: 2,
direction: 'forward',
indexBounds: { age: [ '["10", "10"]' ], name: [ '[MinKey, MaxKey]' ] }
}
}
},
rejectedPlans: []
},
......
}
# 用hint指定某个特定的索引
db.user.find({age:"10"},{name:1,_id:0}).hint({age:1,name:1})
4.2.6 索引使用注意事项
正确的使用索引可以提高检索性能,但使用索引时也有一些注意事项:
- 每个索引会占用一些空间,并且在每次执行插入、更新和删除等操作时也需要对索引进行操作,导致额外的开销。因此,如果很少将某个集合用于读取操作,最好不要在集合中使用索引
- 由于索引存储在 RAM(内存)中,因此应确保索引的总大小不超过 RAM 的限制。如果总大小大于 RAM 的大小,那么 MongoDB 将删除一些索引,这就会导致性能下降。
- 以下查询中不能使用索引:
- 正则表达式或否定运算符,例如 n i n 、 nin、 nin、not 等
- 算术运算符,例如 $mod 等;
- $where 子句
- 如果文档索引字段的值超过了索引键的限制,那么 MongoDB 不会将任何文档插入到集合中
- 在定义索引时有以下几点需要注意:
- 集合的索引不能超过 64 个
- 索引名称的长度不能超过 128 个字符
- 复合索引最多可以拥有 31 个字段
4.3 原子操作
mongodb不支持事务,所以,在你的项目中应用时,要注意这点。无论什么设计,都不要要求mongodb保证数据的完整性。但是mongodb提供了许多原子操作,比如文档的保存,修改,删除等,都是原子操作。所谓原子操作就是要么这个文档保存到Mongodb,要么没有保存到Mongodb,不会出现查询到的文档没有保存完整的情况。
这里就直接拿图书馆借书的例子来理解了:
# 先建立一个books集合,里面加入2本书
collection_books = mydb['books']
books = [
{
"_id": 123456789,
"title": "MongoDB: The Definitive Guide",
"author": [ "Kristina Chodorow", "Mike Dirolf" ],
"published_date": "2010-09-24",
"pages": 216,
"language": "English",
"publisher_id": "oreilly",
"available": 3,
"checkout": [{"by": "joe", "date": "2012-10-15"}]
}
]
result = collection_books.insert_many(books)
# 可以使用 db.collection.findAndModify() 方法来判断书籍是否可结算并更新新的结算信息, 以确保准确
zhongqiang_test> db.books.findAndModify({query: {_id:123456789, available:{$gt:0}}, update:{$inc:{available:-1},$push:{checkout:{by:"abc", date: "2024-02-22"}}}})
zhongqiang_test> db.books.find()
[
{
_id: 123456789,
title: 'MongoDB: The Definitive Guide',
author: [ 'Kristina Chodorow', 'Mike Dirolf' ],
published_date: '2010-09-24',
pages: 216,
language: 'English',
publisher_id: 'oreilly',
available: 2,
checkout: [
{ by: 'joe', date: '2012-10-15' },
{ by: 'abc', date: '2024-02-22' }
]
}
]
# 假设我改成available为2的时候,就不能借了, 此时就会看到结果就为空了
zhongqiang_test> db.books.findAndModify({query: {_id:123456789, available:{$gt:2}}, update:{$inc:{available:-1},$push:{checkout:{by:"abc", date: "2024-02-22"}}}})
null
# 这样就保证了数据的准确,别借成负数了。
# 常用的原子操作:
# $set: 用来指定一个键并更新键值,若键不存在则创建, {$set:{field:value}}
# $unset: 用来删除一个键, {$unset:{field:1}}
# $inc: 用来对文档的某个数值类型的键值进行增减操作, {$inc:{field:value}}
# $push: 用来向文档中追加一些信息, {$push:{field:value}} 把value追加到field里面去,field 一定要是数组类型才行,如果 field 不存在,则会新增一个数组类型加进去
# $pushAll: 与 $push 类似,它可以一次追加多个值到一个数组类型的字段内, {$pushAll:{field:value_array}}
# $pull: 从数组field内删除一个等于value的值, {$pull:{field:_value}}
# $pop: 删除数组的第一个或最后一个元素, {$pop:{field:1}}
# $rename: 修改字段的名称,{$rename:{old_field_name:new_field_name}}
# $bit和偏移操作符感觉没啥用, 先不整理
4.4 MapReduce
Map-Reduce是一种计算模型,简单的说就是将大批量的工作(数据)分解(MAP)执行,然后再将结果合并成最终结果(REDUCE)。
在用 MongoDB 查询时,若返回的数据量很大,或者做一些比较复杂的统计和聚合操作做花费的时间很长时,可以使用 MongoDB 中的 mapReduce 进行实现。即把一个聚合任务分解为多个小的任务,分配到多个服务器上并行处理。
# 使用 MapReduce 要实现两个函数 Map 函数和 Reduce 函数
# Map 函数调用 emit(key, value), 遍历 collection 中所有的记录, 将 key 与 value 传递给 Reduce 函数进行处理。
>db.collection.mapReduce(
function() {emit(key,value);}, # 映射函数,生成键值对序列,作为 reduce 函数参数
function(key,values) {return reduceFunction}, # 统计函数,reduce函数的任务就是将key-values变成key-value,也就是把values数组变成一个单一的值value
{
out: collection, # 统计结果存放集合 (不指定则使用临时集合,在客户端断开后自动删除)
query: document, # 一个筛选条件,只有满足条件的文档才会调用map函数
sort: document, # 和limit结合的sort排序参数(也是在发往map函数前给文档排序),可以优化分组机制,比如10000万个学生里面计算前top200的同学的成绩平均分,就可以map前,先排序选出top200,再发
limit: number # 发往map函数的文档数量的上限(要是没有limit,单独使用sort的用处不大)
}
)
# demo
zhongqiang_test> db.user.mapReduce(
... function() { emit(this.name,this.age); },
... function(key, values) {return Array.sum(values)},
... {
... query:{"age":{$gt: 2}},
... out:"test_mapreduce"
... }
... )
{ result: 'test_mapreduce', ok: 1 }
zhongqiang_test> db.test_mapreduce.find()
[
{ _id: 'Timi', value: 10 },
{ _id: 'Tom', value: 100 },
{ _id: 'Mary', value: 101 }
]
4.5 管理工具
这里我直接使用官方的compass, 下载地址: https://www.mongodb.com/try/download/compass
# 安装
sudo dpkg -i mongodb-compass_1.42.1_amd64.deb
这个安装完成之后直接打开即可, 图形化界面可以很方便的对数据做各种操作
4.6 固定集合
MongoDB 固定集合(Capped Collections)是性能出色且有着固定大小的集合,对于大小固定,我们可以想象其就像一个环形队列,当集合空间用完后,再插入的元素就会覆盖最初始的头部的元素。功能特点:
- 可以插入及更新,但更新不能超出collection的大小,否则更新失败
- 不允许删除,但是可以调用drop()删除集合中的所有行,但是drop后需要显式地重建集合
- 在32位机子上一个cappped collection的最大值约为482.5M,64位上只受系统文件大小的限制。
固定集合的属性:
- 对固定集合进行插入速度极快
- 按照插入顺序的查询输出速度极快
- 能够在插入最新数据时,淘汰最早的数据
用法:
- 储存日志信息
- 缓存一些少量的文档
# 创建集合时,指定capped为True, 10000个字节上限
db.createCollection("cappedLogCollection",{capped:true,size:10000})
# 指定最多文档1000个
db.createCollection("cappedLogCollection2",{capped:true,size:10000,max:1000})
# 判断是否时固定集合
db.cappedLogCollection.isCapped()
# 将已存在的集合转成固定集合
zhongqiang_test> db.runCommand({"convertToCapped":"posts",size:10000})
{ ok: 1 }
zhongqiang_test> db.posts.isCapped()
true
# 固定集合的查询
# 固定集合文档按照插入顺序储存的,默认情况下查询就是按照插入顺序返回的,也可以使用$natural调整返回顺序
db.posts.find().sort({$natural:-1})
4.7 自增ID的实现
MongoDB 没有像 SQL 一样有自动增长的功能, MongoDB 的 _id 是系统自动生成的12字节唯一标识。
但在某些情况下,我们可能需要实现 ObjectId 自动增长功能。这个需要编程来实现。
# 这里需要借助counters集合实现统计
db.counters.insert({_id:"productid",sequence_value:0})
# 这里用python实现一个递增id的函数
collection_counter = mydb['counters']
def get_next_sequence_val(sequence_name: str) -> int:
sequence_doc = collection_counter.find_one_and_update(
{"_id": sequence_name},
{"$inc":{"sequence_value":1}},
new=True
);
return sequence_doc['sequence_value']
# 下面插入实现id自增
collection_products = mydb['products']
collection_products.insert_one({
"_id": get_next_sequence_val("productid"),
"product_name":"Apple iPhone",
"category":"mobiles"
})
collection_products.insert_one({
"_id": get_next_sequence_val("productid"),
"product_name":"xiaomi phone",
"category":"mobiles"
})
print(list(collection_products.find()))
# [{'_id': 3, 'product_name': 'Apple iPhone', 'category': 'mobiles'},
# {'_id': 4, 'product_name': 'xiaomi phone', 'category': 'mobiles'},
# {'_id': 5, 'product_name': 'Apple iPhone', 'category': 'mobiles'},
# {'_id': 6, 'product_name': 'xiaomi phone', 'category': 'mobiles'}]
5. 小总
零零散散用了一周多的时间,才把mongodb的知识框架搭建起来,后续再碰到新知识,会进行相应的补充。随着大模型时代的到来,各种工具确实给我们的工作带来了效率上的提升,各种代码gpt都能搞定,但是这个过程中我逐渐发现一个问题,就是我们自己越来越不太喜欢思考, 积累的知识也是非常的零散, 很容易就会忘记,然后再问gpt,如此反复,最终会发现,工作中虽然也是如期的把任务完成交付,可我们自身能力并没有得到多少提升,知识也没有积累出多少来,针对这种变化,我本身有一种强烈的不适应感,所以就想着,对于新知识,还是要抽时间系统过一遍,有一个框架出来,再通过大模型也好,其他途径也好,把框架慢慢的丰富,加上时间的加持,不知不觉,就会沉淀下一些东西来,这样的学习方式比较适合我。
所以后面也会借工作外的时间,把一些重要的东西系统学习,整理成博客笔记,让知识更系统一些, 后面的两篇是redis和mysql的系统学习和梳理,数据库的知识在实际工作中非常重要, 其次打算开一个新专栏,从小白的身份开始学习大模型, 就像当初入门推荐系统那样, 把大模型的相关知识也系统梳理, 带更多的伙伴入门大模型,使用大模型,拥抱大模型,总之,希望这些知识能帮助到更多的伙伴呀 😉