Mongo

Mongo简介

MongoDB是一个基于分布式文件存储的数据库。由C++语言编写。旨在为 WEB 应用提供可扩展高性能数据存储解决方案。MongoDB 是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的。

版本

偶数版本为稳定版本,技术版本为开发版

NoSQL

Not Only SQL,意为不仅仅为SQL。现代计算机系统每天网络上都会产生庞大的数据量。这些数据很大部分由关系型数据库管理系统(RDBMS)处理。1970年 E.F.Codd's提出的关系模型的论文 "A relational model of data for large shared data banks",这使得数据建模和应用程序编程更加简单。

通过应用实践证明,关系模型是非常适合于客户服务器编程,远远超出预期的利益,今天它是结构化数据存储在网络和商务应用的主导技术。

NoSQL通常用于超大规模数据的存储。(例如谷歌或Facebook每天为他们的用户收集万亿比特的数据)。这些类型的数据存储不需要固定的模式无需多余操作就可以横向扩展

RDBMs VS NoSQL

RDBMsNoSQL

高度组织化、结构化的数据

非结构化和不可预知的数据

结构化查询语言(SQL)

没有声明性的查询语言

数据与关系都存储在表中

k-v存储,列存储,文档存储,图形存储

严格的一致性,ACID

最终一致性,非ACID属性

具有事务

大部分没有事务

高性能,高可用性和可伸缩性

NoSQL的优缺点

优点缺点

高扩展性,架构灵活,半结构化数据

没有标准化

分布式计算

有限的查询功能(到目前为止)

低成本

一致性较差

NoSQL数据库分类

存储方式代表特点

xml数据库

Berkeley DB XML,BaseX

高效存储xml数据,并支持xml内部查询语法,比如XQuery,Xpath

key-value存储

Redis、MemcacheDB、Tokyo Cabine

可以通过key快速查询到对应的value。一般情况下value可以存储任何格式(redis做了拓展)。

对象存储

db4o、Versant

通过类似面向对象语言的语法操作数据库,通过对象的方式存取数据。

文档存储

MongoDB、CouchDB

文档存储一般用类似JSON的格式存储,存储的内容是文档型的。这样也就有机会对某些字段建立索引,实现关系数据库的某些功能

图存储

Neo4J、FlockDB

图形关系的最佳存储。使用传统关系数据库来解决的话性能低下,而且设计使用不方便

列存储

Hbase、Cassandra、Hypertable

按列存储数据的。最大的特点是方便存储结构化和半结构化数据,方便做数据压缩,对针对某一列或者某几列的查询有非常大的IO优势

适用场景

适用场景

  1. 网站实时数据

  2. 数据缓存

  3. 大尺寸、低价值的数据存储:如网盘

  4. 高伸缩性场景

  5. 文档JSON数据存储

不适用场景

  1. 高度事务性系统,比如银行以及会计系统

  2. 需要复杂的查询SQL

相关概念

MongoDB将数据存储为一个文档(document),并以键值(k-v)结构组成。MongoDB文档格式类似JSON,但是是以二进制形式存储,故称为BSON。

关于MongoDB中的属于概念:

SQL术语/概念MongoDB术语/概念解释/说明

database

database

数据库

table

collection

数据库表/集合

row

document

数据记录行/文档

column

field

数据字段/域

index

index

索引

table joins

表连接,MongoDB不支持

primary key

primary key

主键,MongoDB自动将_id字段设置为主键

安装MongoDB

docker安装

客户端工具

官方命令行工具

以下工具都位于 MongoDB的bin目录下,皆是可执行文件。

命令作用

mongod

服务端程序,用于启动MongoDB

mongo

客户端程序,连接MongoDB,可执行js脚本

mongofiles

GridFS工具,内建的分布式文件系统,用于向 MongoDB 存储、读取文件,mongofiles 支持put、get、list等接口

mongoexport/mongoimport

数据导入导出程序,支持CSV、JSON

mongodump/mongorestore

数据备份程序/数据恢复程序,将导出BSON格式,用于定期备份

mongos

数据分片程序,支持数据的横向扩展

mongostat

监视程序,查看MongoDB 实时的增删改查操作的 pqs、以及内存使用、网络吞吐的信息

mongotop

实时查看 MongoDB 在哪些集合上花的读写时间最多,能快速找出实例里的热点集合

mongosniff

抓包工具,直接下载二进制包可能并不包含这个工具,需要下载源码编译出来,mongosniff 可以抓取某个 MongoDB 实例的所有请求及应答数据,对于 MongoDB driver 的开发者非常有帮助,也可以用于一些网络问题的定位

mongoperf

测试磁盘 IO 性能的工具

客户端Driver

Mongo支持10+种语言的客户端,点我看官方文档。所有官方客户端driver都支持MongoDB Connection String URI的方式去连接。

mongodb 的连接串 uri

连接串格式为:

mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/[database][?options]]
  • mongodb:// 协议

  • user:password@ 可选的用户名与密码

  • host1至少指定一个host,代表mongodb服务端的地址

  • portx 端口号,可选,缺省值为27017

  • /database如果制定了username:password@,将会连接登录指定的数据库。如果未指定,则会打开test数据库

  • ?options可选项,如果未指定database,加入此参数时,需要在尾部加入/,多个options中,需要使用&或者;连接。

options的可用选项

选项描述

replicaSet=name

验证replica set的名称。 Impliesconnect=replicaSet.

slaveOk=true|false

true:在connect=direct模式下,驱动会连接第一台机器,即使这台服务器不是主。在connect=replicaSet模式下,驱动会发送所有的写请求到主并且把读取操作分布在其他从服务器。false: 在 connect=direct模式下,驱动会自动找寻主服务器. 在connect=replicaSet 模式下,驱动仅仅连接主服务器,并且所有的读写命令都连接到主服务器。

safe=true|false

true: 在执行更新操作之后,驱动都会发送getLastError命令来确保更新成功。(还要参考 wtimeoutMS).false: 在每次更新之后,驱动不会发送getLastError来确保更新成功。

w=n

驱动添加 { w : n } 到getLastError命令. 应用于safe=true。

wtimeoutMS=ms

驱动添加 { wtimeout : ms } 到 getlasterror 命令. 应用于 safe=true.

fsync=true|false

true: 驱动添加 { fsync : true } 到 getlasterror 命令.应用于 safe=true.false: 驱动不会添加到getLastError命令中。

journal=true|false

如果设置为 true, 同步到 journal (在提交到数据库前写入到实体中). 应用于 safe=true

connectTimeoutMS=ms

可以打开连接的时间。

socketTimeoutMS=ms

发送和接受sockets的时间。

可视化连接工具

  • Navicat For MongoDB: https://www.navicat.com.cn/products/navicat-for-mongodb

  • Studio 3T:https://studio3t.com/

  • NoSQL Manager for MongoDB: https://www.mongodbmanager.com/

  • Robo 3T:https://robomongo.org/

mongod

启动服务端

mongod --dbpath=/d/db/mongodb/data
# dbpath用于指定数据库的磁盘存储路径

mogo shell

数据库操作

// 创建/进入数据库直接使用use
// 只有向数据库中创建collection,数据库才会被创建
use DATABASE_NAME

// 查看所有的数据库
show dbs

// 删除数据库(需要先 use 数据库)
// 如果数据库中没有集合,那么数据库也是删除的
db.dropDatabase()

用户操作

创建用户:

// mongo的用户与数据库关联,如果想要创建可管理所有数据库的用户,需要创建在 admin数据库中。
use admin

db.createUser(
     {
       user:"pws",
       pwd:"pws",
       roles:["userAdminAnyDatabase"]
     }
  )

db.createUser(
     {
       user:"pws",
       pwd:"pws",
       roles:["readWrite"]
     }
  )

修改用户:

db.updateUser("pws",
  {
    pwd: "pws",
    roles: [ "root","userAdminAnyDatabase" ]
  }
)

集合操作

// db.createCollection(name, options) 集合名称,可选参数(指定有关内存以及索引的选项)
db.createCollection("person")
// db.createCollection("person", {capped: true, size: 100000})
/*
可选参数: 
- `capped` 布尔类型,true代表固定集合,此时必须指定他的固定大小`size`。当固定集合达到最大值时,会自动覆盖最早的文档。
- `size` 代表集合的最大大小,单位字节。如果是固定集合,则必须指定此属性,代表集合的固定大小。
- `max` 代表集合最大长度
- `autoIndexId`如果为true,会自动给`_id`字段创建索引。默认为false ,已废弃
*/
// 插入文档时,会先检查size字段,后检查max字段

// 插入文档会自动创建集合
db.testCollection.insert({"name" : "测试"})
// 没有testCollection,不会报错会创建一个collection

// 删除集合, collection是集合名称
db.collection.drop()

// 显示数据库中的所有集合
show collections

文档操作

插入

插入校验

  1. 检查文档基本结构是否正确,如果没有_id则自动增加一个id

  2. 检查当文档是否小于16MB

  3. mongodb校验有限,其他的校验通常在mongo客户端中进行

插入单条

db.foo.insertOne({bar : "baz"})

批量插入

db.foo.insertMany([
    {bar: "baz1"},
    {bar: "baz2"}
])
  • 一次消息长度最多为48MB

  • 如果插入总共大于48MB的数据,大部分驱动会将其拆分为多个批量插入请求

  • 如果批量插入出现错误,那么错误文档之前的文档,都会被插入成功,后面的都会失败。如果想要忽略错误,需要增加continueOnError选项

查询

  • 查询就是返回一个集合中符合条件的文档子集

find:查询所有文档

// 查询集合中的所有文档
db.foo.find()

find:条件查询

// 使用相等条件查询文档
// 查询 age==18 && bar==“张三”的数据
db.foo.find({
    age: 18,
    bar: "张三"
})

find:$ltlte$gt$gte

// 使用相等条件查询文档
// 查询 age==18 && bar==“张三”的数据
db.foo.find({
    age: 18,
    bar: "张三"
})

// $lt  	小于
// $lte		小于等于
// $gt 		大于
// $gte		大于等于

// 查询 age > 2 && age < 18 的数据
db.foo.find({age :{
    $gt: 2,
    $lt: 18
}})

// 查询age >= 2 && age <= 18 的数据
db.foo.find({age :{
    $gte: 2,
    $lte: 18
}})

// 日期范围匹配
// 查询2020年1月1
startDate = new Date(2020, 1, 1)
db.foo.find({create_at : {
    $gte: startDate
}})

find:$in

// 查询年龄不是18或者19的文档
db.foo.find({
    age: {
        $not :{
            $in: [18, 19]
        }
    }
})

find:$or$and

// 或者
// 查询 age == 18 || age == 19 的数据
db.foo.find({
    $or: [ // 条件数组
        {age: 18}, // 条件1
        {age: 19}, // 条件2
    ]
})

// 并且
// 查询 age == 18 && bar == "张三" 的数据
db.foo.find({
    $and: [ // 条件数组
        {age: 18}, // 条件1
        {bar: "张三"}, // 条件2
    ]
})

find:$not

  • $not是元语句,可以用在任何其他语句之上

find:过滤返回键

  • 节省传输的数据量,也能减少客户端解码文档的内存消耗

  • _id字段始终返回

// 白名单
db.foo.find({
    age: 18,
    bar: "张三"
}, {
    bar: 1 // 只返回bar字段
})

// 黑名单
db.foo.find({
    age: 18,
    bar: "张三"
}, {
    age: 0 // age字段不返回
})

find:返回单条

db.foo.find({
    age: 18,
    bar: "张三"
})

查询文档大小

Object.bsonsize(文档)

删除

  • 删除是永久性的,不可撤销,也不可能恢复

删除单条符合条件的文档

db.foo.deleteOne({
    bar: "baz"
})

删除多条符合条件的文档

db.foo.deleteMany({
    bar: "baz"
})

删除集合中的所有文档

db.foo.deleteMany()

不如使用db.foo.drop()更快更方便

更新

  • 如果有两个客户端同时发送更新请求,先到达服务器的一方先执行,接着执行另一个

  • 并发操作下是原子的,不会破坏文档

  • update需要传入两个参数,第一个是查询文档,第二个是修改器文档

  • 更新修改器是原子性的,用于指定复杂的更新操作,比如修改、增加、删除键,操作数组、数字、或者内嵌文档

文档替换

使用新文档完全替换匹配的文档,replaceOne

db.user.drop()

db.user.insertOne({
    name: "joe",
    friends: 32,
    enimies: 2,
})

var joe = db.user.findOne({name: "joe"})
joe.relationships = {
    friends: joe.friends,
    enimies: joe.enimies,
}
delete joe.friends
delete joe.enimies
joe.username = joe.name
delete joe.name
delete joe._id
db.user.replaceOne({name: "joe"}, joe)

db.user.findOne({username: "joe"})

/* 
// 上述文档被替换为:
{
    "_id": {"$oid": "6167eb1a64474624ff16ccff"},
    "relationships": {
        "friends": 32,
        "enimies": 2
    },
    "username": "joe"
}*/

更新单条

更新多条

$setunset

$set用于修改字段的甚至是类型,如果字段不存在,则创建该字段;而$unset用于删除某个字段。

db.user.drop()

db.user.insertOne({
    username: "张三",
    age: 19,
    hobby: "🏀"
})

db.user.updateOne({username: "张三"}, {$set: {hobby: ["⚽️", "🏀", "🏓"]}, $unset: {age: 1}})

db.user.find({username: "张三"})

所得结果:

{
    "_id": {"$oid": "6167f4ba93e9ab74824731d2"},
    "hobby": ["⚽️", "🏀", "🏓"],
    "username": "张三"
}

$inc

$inc修改器用于增加键的值,如果键不存在就会创建。它可以用于整型、长整型、双精度浮点型的值,并且值必须为数字,不可是数字字符串,如果不是合法的值,将会造成操作失败。

db.analytics.drop()
db.analytics.insertOne({
    url: "baidu.com",
    pageviews: 0
})
// 增加百度的访问量
db.analytics.updateOne({
    url: "baidu.com"
}, {
    $inc: {pageviews: 1}
})

数组修改器

$push

末尾增加单个元素:

db.blog.drop()

db.blog.posts.insertOne({
    title: "博客标题",
    content: "我是文章内容"
})

// $push 添加一个元素
db.blog.posts.updateOne({title: "博客标题"},  {
    $push: {
        comments: {
            name: "joe",
            email: "joe.example.com",
            content: "nice post"
        }
    }
})

db.blog.posts.findOne({title: "博客标题"})

结果:

[
  {
    "_id": {"$oid": "616939af2f37325a7eb4c623"},
    "comments": [
      {
        "name": "joe",
        "email": "joe.example.com",
        "content": "nice post"
      }
    ],
    "content": "我是文章内容",
    "title": "博客标题"
  }
]

$each + $push

一次在末尾添加多个值:

db.blog.posts.drop()

db.blog.posts.insertOne({
    title: "博客标题",
    content: "我是文章内容"
})

// $each + $push 添加多个元素
// 向该博客添加两条评论
db.blog.posts.updateOne({title: "博客标题"},  {
    $push: {
        comments: {
            $each: [
                {
                    name: "joe",
                    email: "joe.example.com",
                    content: "nice post"
                },
                {
                    name: "lily",
                    email: "lily.example.com",
                    content: "nice post2"
                },
            ]
        }
    }
})

db.blog.posts.findOne({title: "博客标题"})

结果:

[
  {
    "_id": {"$oid": "61693cb72f37325a7eb4c62d"},
    "comments": [
      {
        "name": "joe",
        "email": "joe.example.com",
        "content": "nice post"
      },
      {
        "name": "lily",
        "email": "lily.example.com",
        "content": "nice post2"
      }
    ],
    "content": "我是文章内容",
    "title": "博客标题"
  }
]

$slice + $push

  • $slice的作用类似数组切片,指定一个数值,如果是正整数,则对数组的[0, n)进行切片,如果值为负整数,则对数组从后往前进行切片,也就是 (length-1-n, length-1]

索引

如何选择需要建立索引的字段?

如何强制使用索引?如何评估索引的效率?

创建索引和删除索引

什么是索引?为什么需要索引?

索引类似书的索引,记录某个数据的位置,不使用索引的查询称为全表扫描,全表扫描在搜寻某个字段时,会依次遍历表中的所有记录并查询该条记录的字段。所以当单张表数据很多时,这个操作将会非常耗时,所以需要使用索引解决这类问题。

索引可以根据给定的字段组织数据,让MongoDB能够非常快地找到目标文档。

db.user.drop()

for (let i = 0; i < 100000; i++) {
    db.user.insertOne({
        userid: 1,
        username: "name" + i
    })
}

db.user.find({userid: 99999}).limit(1).explain()
// 创建索引
db.users.ensureIndex({"username": 1})
// 如果集合数量很大或者机器性能较差,索引可能创建的很慢,可以使用如下方式查询索引的创建进度
db.currentOp()

最后更新于