跳至主要內容

《MongoDB 权威指南》笔记一

钝悟...大约 17 分钟笔记数据库数据库文档数据库MongoDB

《MongoDB 权威指南》笔记一

第 1 章 MongoDB 简介

MongoDB 简介

MongoDB 是一个分布式文档数据库,由 C++ 语言编写。

面向文档

面向文档的数据库使用更灵活的“文档”模型取代了“行”的概念。通过嵌入文档和数组,面向文档的方式可以仅用一条记录来表示复杂的层次关系。

MongoDB 中也没有预定义模式(predefined schema):文档键值的类型和大小不是固定的。由于没有固定的模式,因此按需添加或删除字段变得更容易。

综上,MongoDB 支持结构化、半结构化数据模型,可以动态响应结构变化

功能丰富

MongoDB 提供了丰富的功能:

  • 索引 - MongoDB 支持通用的二级索引,并提供唯一索引、复合索引、地理空间索引及全文索引功能。此外,它还支持在不同层次结构(如嵌套文档和数组)上建立二级索引。
  • 聚合 - MongoDB 提供了一种基于数据处理管道的聚合框架。
  • 特殊的集合和索引类型 - MongoDB 支持有限生命周期(TTL)集合,适用于保存将在特定时间过期的数据,比如会话和固定大小的集合,以及用于保存最近的数据(日志)。MongoDB 还支持部分索引,可以仅对符合某个条件的文档创建索引,以提高效率并减少所需的存储空间。
  • 文件存储 - 针对大文件及文件元数据的存储,MongoDB 使用了一种非常易用的协议。
  • ...

分布式

MongoDB 作为分布式存储,自然也具备了分布式的一般特性:

  • 通过副本机制提供高可用
  • 通过分片提供扩容能力

第 2 章 入门指南

文档是 MongoDB 中的基本数据单元,可以粗略地认为其相当于关系数据库管理系统中的行(但表达力要强得多)。

类似地,集合可以被看作具有动态模式的表。

一个 MongoDB 实例可以拥有多个独立的数据库,每个数据库都拥有自己的集合。

每个文档都有一个特殊的键 "_id",其在所属的集合中是唯一的。

MongoDB 自带了一个简单但功能强大的工具:mongo shell。mongo shell 对管理 MongoDB 实例和使用 MongoDB 的查询语言操作数据提供了内置的支持。它也是一个功能齐全的 JavaScript 解释器,用户可以根据需求创建或加载自己的脚本。

文档

文档是一组有序键值的集合。

文档中的值不仅仅是“二进制大对象”,它们可以是几种不同的数据类型之一(甚至可以是一个完整的嵌入文档)。

文档中的键是字符串类型。除了少数例外的情况,可以使用任意 UTF-8 字符作为键。

键中不能含有 \0(空字符)。这个字符用于表示一个键的结束。

.$ 是特殊字符,只能在某些特定情况下使用。

MongoDB 会区分类型和大小写。

下面这两个文档是不同的:

{"count" : 5}
{"count" : "5"}

下面这两个文档也不同:

{"count" : 5}
{"Count" : 5}

需要注意,MongoDB 中的文档不能包含重复的键。例如,下面这个文档是不合法的。

{ "greeting": "Hello, world!", "greeting": "Hello, MongoDB!" }

集合

集合就是一组文档。如果将文档比作关系数据库中的行,那么一个集合就相当于一张表。

集合具有动态模式的特性。这意味着一个集合中的文档可以具有任意数量的不同“形状”。例如,以下两个文档可以存储在同一个集合中:

{"greeting" : "Hello, world!", "views": 3}
{"signoff": "Good night, and good luck"}

集合由其名称进行标识。集合名称可以是任意 UTF-8 字符串,但有以下限制。

  • 集合名称不能是空字符串("")。
  • 集合名称不能含有 \0(空字符),因为这个字符用于表示一个集合名称的结束。
  • 集合名称不能以 system. 开头,该前缀是为内部集合保留的。例如,system.users 集合中保存着数据库的用户,system.namespaces 集合中保存着有关数据库所有集合的信息。
  • 用户创建的集合名称中不应包含保留字符 $。许多驱动程序确实支持在集合名称中使用 $,这是因为某些由系统生成的集合会包含它,但除非你要访问的是这些集合之一,否则不应在名称中使用 $ 字符。

使用 . 字符分隔不同命名空间的子集合是一种组织集合的惯例。例如,有一个具有博客功能的应用程序,可能包含名为 blog.posts 和名为 blog.authors 的集合。这只是一种组织管理的方式,blog 集合(它甚至不必存在)与其“子集合”之间没有任何关系。

数据库

MongoDB 使用集合对文档进行分组,使用数据库对集合进行分组。一个 MongoDB 实例可以承载多个数据库,每个数据库有零个或多个集合。

数据库按照名称进行标识的。数据库名称可以是任意 UTF-8 字符串,但有以下限制:

  • 数据库名称不能是空字符串("")。
  • 数据库名称不能包含 /\."*<>:|?$、单一的空格以及 \0(空字符),基本上只能使用 ASCII 字母和数字。
  • 数据库名称区分大小写。
  • 数据库名称的长度限制为 64 字节。

MongoDB 使用 WiredTiger 存储引擎之前,数据库名称会对应文件系统中的文件名。尽管现在已经不这样处理了,但之前的许多限制遗留了下来。

此外,还有一些数据库名称是保留的。这些数据库可以被访问,但它们具有特殊的语义。具体如下。

  • adminadmin 数据库会在身份验证和授权时被使用。此外,某些管理操作需要访问此数据库。
  • local:在副本集中,local 用于存储复制过程中所使用的数据,而 local 数据库本身不会被复制。
  • config:MongoDB 的分片集群会使用 config 数据库存储关于每个分片的信息。通过将数据库名称与该库中的集合名称连接起来,可以获得一个完全限定的集合名称,称为命名空间

启动 MongoDB

启动 MongoDB 的方式:

  • Unix 系统 - 执行 mongod
  • Windows 系统 - 执行 mongod.exe

如果没有指定参数,则 mongod 会使用默认的数据目录 /data/db/。如果数据目录不存在或不可写,那么服务器端将无法启动。因此在启动 MongoDB 之前,创建数据目录(如 mkdir -p /data/db/)并确保对该目录有写权限非常重要。

默认情况下,MongoDB 会监听 27017 端口上的套接字连接。如果端口不可用,那么服务器将无法启动。

MongoDB Shell

MongoDB 内置了 MongoDB Shell 工具来提供命令行交互工具。

要启动 shell,可以执行 mongo 文件。

【示例】MongoDB Shell 基本操作

# 查看有哪些数据库
> show dbs
admin            0.000GB
config           0.000GB
fc_open_core     0.000GB
local            0.000GB
spring-tutorial  0.000GB
test             0.919GB

# 切换到 test 数据库
> use test
switched to db test

# 插入文档
> db.user.insertOne({ name: "dunwu", sex: 'man' })
{
        "acknowledged" : true,
        "insertedId" : ObjectId("670a281a2647017bf5f42962")
}

# 查询文档
}
> db.user.find()
{ "_id" : ObjectId("670a281a2647017bf5f42962"), "name" : "dunwu", "sex" : "man" }

# 更新文档
> db.user.updateOne({ name: "dunwu" }, { $set: { age: 30 } })
{ "acknowledged" : true, "matchedCount" : 1, "modifiedCount" : 1 }

# 删除文档
> db.user.deleteOne({ name: "dunwu" })
{ "acknowledged" : true, "deletedCount" : 1 }

# 退出 MongoDB Shell
> quit()

数据类型

MongoDB 中的文档可以被认为是“类似于 JSON”的形式。

MongoDB 基本数据类型如下:

null - null 类型用于表示空值或不存在的字段。

{ "x": null }

布尔类型 - 布尔类型的值可以为 true 或者 false。

{ "x": true }

数值类型 - shell 默认使用 64 位的浮点数来表示数值类型。因此,下面的数值在 shell 中看起来是“正常”的:

{"x" : 3.14}
{"x" : 3}

对于整数,可以使用 NumberInt 或 NumberLong 类,它们分别表示 4 字节和 8 字节的有符号整数。

{"x" : NumberInt("3")}
{"x" : NumberLong("3")}

字符串类型 - 任何 UTF-8 字符串都可以使用字符串类型来表示。

{ "x": "foobar" }

日期类型 - MongoDB 会将日期存储为 64 位整数,表示自 Unix 纪元(1970 年 1 月 1 日)以来的毫秒数,不包含时区信息。

{"x" : new Date()}

正则表达式 - 查询时可以使用正则表达式,语法与 JavaScript 的正则表达式语法相同。

{"x" : /foobar/i}

数组类型 - 集合或者列表可以表示为数组。

{ "x": ["a", "b", "c"] }

内嵌文档 - 文档可以嵌套其他文档,此时被嵌套的文档就成了父文档的值。

{ "x": { "foo": "bar" } }

Object ID - Object ID 是一个 12 字节的 ID,是文档的唯一标识。MongoDB 中存储的每个文档都必须有一个 "_id" 键。"_id" 的值可以是任何类型,但其默认为 ObjectId。在单个集合中,每个文档的 "_id" 值都必须是唯一的,以确保集合中的每个文档都可以被唯一标识。

{"x" : ObjectId()}

ObjectId 占用了 12 字节的存储空间,可以用 24 个十六进制数字组成的字符串来表示。

0  |  1  |  2  |  3  |  4  |  5  |  6  |  7  |  8  |  9  |  10  |  11
         时间戳       |           随机值            | 计数器(起始值随机)

ObjectId 的前 4 字节是从 Unix 纪元开始以秒为单位的时间戳。这提供了一些有用的属性。时间戳与接下来的 5 字节(稍后会介绍)组合在一起,在秒级别的粒度上提供了唯一性。

二进制数据 - 二进制数据是任意字节的字符串,不能通过 shell 操作。如果要将非 UTF-8 字符串存入数据库,那么使用二进制数据是唯一的方法。

代码 - MongoDB 还可以在查询和文档中存储任意的 JavaScript 代码:

{"x" : function() { /* ... */ }}

最后,还有一些类型主要在内部使用(或被其他类型取代)。这些将根据需要在文中特别说明

使用 MongoDB shell(略)

第 3 章 创建、更新和删除文档

插入文档

insertOne

insertOne 方法用于插入单条文档

insertOne 方法语法如下:

db.collection.insertOne(document, options)
  • document - 要插入的单个文档。
  • options(可选) - 一个可选参数对象,可以包含 writeConcernbypassDocumentValidation 等。

【示例】向 movies 集合中插入一条文档

> db.movies.insertOne({"title" : "Stand by Me"})

insertMany

insertMany 方法用于批量插入文档

insertMany 方法语法如下:

db.collection.insertMany(documents, options)
  • documents - 要插入的文档数组。
  • options(可选) - 一个可选参数对象,可以包含 orderedwriteConcernbypassDocumentValidation 等。
> db.movies.insertMany([{"title" : "Ghostbusters"},{"title" : "E.T."},{"title" : "Blade Runner"}]);

在当前版本中,MongoDB 能够接受的最大消息长度是 48MB,因此在单次批量插入中能够插入的文档是有限制的。如果尝试插入超过 48MB 的数据,则多数驱动程序会将这个批量插入请求拆分为多个 48MB 的批量插入请求。

MongoDB 会对要插入的数据进行最基本的检查:检查文档的基本结构,如果不存在 "_id" 字段,则自动添加一个。

删除文档

deleteOne

deleteOne 方法用于删除单条文档

> db.movies.find()
{ "_id" : ObjectId("670a31a206fe06538fb4d138"), "title" : "Stand by Me" }
{ "_id" : ObjectId("670a31ab06fe06538fb4d139"), "title" : "Ghostbusters" }
{ "_id" : ObjectId("670a31ab06fe06538fb4d13a"), "title" : "E.T." }
{ "_id" : ObjectId("670a31ab06fe06538fb4d13b"), "title" : "Blade Runner" }

> db.movies.deleteOne({"_id" : ObjectId("670a31a206fe06538fb4d138")})
{ "acknowledged" : true, "deletedCount" : 1 }

> db.movies.find()
{ "_id" : ObjectId("670a31ab06fe06538fb4d139"), "title" : "Ghostbusters" }
{ "_id" : ObjectId("670a31ab06fe06538fb4d13a"), "title" : "E.T." }
{ "_id" : ObjectId("670a31ab06fe06538fb4d13b"), "title" : "Blade Runner" }

deleteMany

deleteMany 方法用于删除满足筛选条件的所有文档

> db.movies.find()
{ "_id" : 0, "title" : "Top Gun", "year" : 1986 }
{ "_id" : 1, "title" : "Back to the Future", "year" : 1985 }
{ "_id" : 3, "title" : "Sixteen Candles", "year" : 1984 }
{ "_id" : 4, "title" : "The Terminator", "year" : 1984 }
{ "_id" : 5, "title" : "Scarface", "year" : 1983 }

> db.movies.deleteMany({"year" : 1984}){ "acknowledged" : true, "deletedCount" : 2 }

> db.movies.find()
{ "_id" : 0, "title" : "Top Gun", "year" : 1986 }
{ "_id" : 1, "title" : "Back to the Future", "year" : 1985 }
{ "_id" : 5, "title" : "Scarface", "year" : 1983 }

可以使用 deleteMany删除集合中的所有文档

> db.movies.find()
{ "_id" : 0, "titl
e" : "Top Gun", "year" : 1986 }{ "_id" : 1, "titl
e" : "Back to the Future", "year" : 1985 }{ "_id" : 3, "titl
e" : "Sixteen Candles", "year" : 1984 }{ "_id" : 4, "titl
e" : "The Terminator", "year" : 1984 }{ "_id" : 5, "titl
e" : "Scarface", "year" : 1983 }

> db.movies.deleteMany({})
{ "acknowledged" :true, "deletedCount" : 5 }

> db.movies.find()

更新文档

replaceOne

replaceOne 方法用于将新文档完全替换匹配的文档。这对于进行大规模模式迁移的场景非常有用。

db.collection.replaceOne(filter, replacement, options)
  • filter - 用于查找文档的查询条件。
  • replacement - 新的文档,将替换旧的文档。
  • options - 可选参数对象,如 upsert 等。

【示例】replaceOne 示例

db.collection.replaceOne(
    { name: "Tom" },                  // 过滤条件
    { name: "Tom", age: 18 }          // 新文档
);

updateOne

updateOne 方法用于更新单条文档

updateOne 方法语法如下:

db.collection.updateOne(filter, update, options)
  • filter - 用于查找文档的查询条件。
  • update - 指定更新操作的文档或更新操作符。
  • options - 可选参数对象,如 upsertarrayFilters 等。

【示例】updateOne 示例

db.collection.updateOne(
    { name: "Tom" },                // 过滤条件
    { $set: { age: 19 } },            // 更新操作
    { upsert: false }                 // 可选参数
);

updateMany

updateMany 方法用于批量更新文档

updateMany 方法语法如下:

db.collection.updateMany(filter, update, options)
  • filter - 用于查找文档的查询条件。
  • update - 指定更新操作的文档或更新操作符。
  • options - 可选参数对象,如 upsertarrayFilters 等。

【示例】updateMany 示例

db.collection.updateMany(
    { age: { $lt: 30 } },             // 过滤条件
    { $set: { status: "active" } },   // 更新操作
    { upsert: false }                  // 可选参数
);

更新运算符

"$set" 用来设置一个字段的值。如果这个字段不存在,则创建该字段。

// 使用 "$set" 来修改值
> db.users.updateOne({"name" : "joe"},{"$set" : {"favorite book" : "Green Eggs and Ham"}})

// 使用 "$set" 来修改键的类型
// 将 "favorite book" 键的值更改为一个数组
> db.users.updateOne({"name" : "joe"},{"$set" : {"favorite book" : "Green Eggs and Ham"}})

// 如果用户发现自己其实不爱读书,则可以用 "$unset" 将这个键完全删除
> db.users.updateOne({"name" : "joe"},{"$unset" : {"favorite book" : 1}})

"$inc" 运算符可以用来修改已存在的键值或者在该键不存在时创建它。对于更新分析数据、因果关系、投票或者其他有数值变化的地方,使用这个会非常方便。

> db.games.updateOne({"game":"pinball","user":"joe"},{"$inc":{"score":50}})

如果数组已存在,"$push" 就会将元素添加到数组末尾;如果数组不存在,则会创建一个新的数组

> db.blog.posts.updateOne({"title":"A blog post"},{"$push":{"comments":{"name":"joe","email":"joe@example.com","content":"nice post."}}})

**如果将数组视为队列或者栈,那么可以使用 "pop"从任意一端删除元素**。`{"pop":{"key":1}} 会从数组末尾删除一个元素,{"$pop":{"key":-1}}` 则会从头部删除它。

"$pull" 用于删除与给定条件匹配的数组元素

> db.lists.updateOne({}, {"$pull" : {"todo" : "laundry"}})

upsert

upsert 是一种特殊类型的更新。如果找不到与筛选条件相匹配的文档,则会以这个条件和更新文档为基础来创建一个新文档;如果找到了匹配的文档,则进行正常的更新。

> db.users.updateOne({"rep" : 25}, {"$inc" : {"rep" : 3}}, {"upsert" : true})

第 4 章 查询

find 简介

MongoDB 中使用 find 方法来进行查询。查询就是返回集合中文档的一个子集,子集的范围从 0 个文档到整个集合。要返回哪些文档由 find 的第一个参数决定,该参数是一个用于指定查询条件的文档。

find 的语法格式如下:

db.collection.find(query, projection)
  • query - 用于查找文档的查询条件。默认为 {},即匹配所有文档。
  • projection(可选) - 指定返回结果中包含或排除的字段。

【示例】查找示例

// 查找所有文档
> db.collection.find()

// 按条件查找文档
> db.collection.find({ age: { $gt: 25 } })

// 按条件查找文档,并只返回指定字段
> db.collection.find(
    { age: { $gt: 25 } },
    { name: 1, age: 1, _id: 0 }
)

// 以格式化的方式来显示所有文档
> db.collection.find().pretty()

查询条件

比较操作符

操作符描述示例
$eq等于{ age: { $eq: 25 } }
$ne不等于{ age: { $ne: 25 } }
$gt大于{ age: { $gt: 25 } }
$gte大于等于{ age: { $gte: 25 } }
$lt小于{ age: { $lt: 25 } }
$lte小于等于{ age: { $lte: 25 } }
$in在指定的数组中{ age: { $in: [25, 30, 35] } }
$nin不在指定的数组中{ age: { $nin: [25, 30, 35] } }

【示例】查找年龄大于 25 且城市为 "New York" 的文档:

db.collection.find({ age: { $gt: 25 }, city: "New York" });

逻辑操作符

操作符描述示例
$and逻辑与,符合所有条件{ $and: [ { age: { $gt: 25 } }, { city: "New York" } ] }
$or逻辑或,符合任意条件{ $or: [ { age: { $lt: 25 } }, { city: "New York" } ] }
$not取反,不符合条件{ age: { $not: { $gt: 25 } } }
$nor逻辑与非,均不符合条件{ $nor: [ { age: { $gt: 25 } }, { city: "New York" } ] }

【示例】查找年龄大于 25 或城市为 "New York" 的文档:

db.collection.find({ $or: [ { age: { $gt: 25 } }, { city: "New York" } ] });

元素操作符

操作符描述示例
$exists字段是否存在{ age: { $exists: true } }
$type字段的 BSON 类型{ age: { $type: "int" } }

【示例】查找包含 age 字段的文档:

db.myCollection.find({ age: { $exists: true } });

数组操作符

操作符描述示例
$all数组包含所有指定的元素{ tags: { $all: ["red", "blue"] } }
$elemMatch数组中的元素匹配指定条件{ results: { $elemMatch: { score: { $gt: 80, $lt: 85 } } } }
$size数组的长度等于指定值{ tags: { $size: 3 } }
slice返回一个数组键中元素的子集

查询数组元素的方式与查询标量值相同。

// 插入数组列表
db.food.insertOne({"fruit" : ["apple", "banana", "peach"]}
// 查找数组中的 banana 元素
db.food.find({"fruit" : "banana"})

如果需要通过多个元素来匹配数组,那么可以使用 "$all"。

db.food.insertOne({"_id" : 1, "fruit" : ["apple", "banana", "peach"]})
db.food.insertOne({"_id" : 2, "fruit" : ["apple", "kumquat", "orange"]})
db.food.insertOne({"_id" : 3, "fruit" : ["cherry", "banana", "apple"]})

// 查询同时包含元素 "apple" 和 "banana" 的文档
db.food.find({fruit : {$all : ["apple", "banana"]}})

查询特定长度的数组

db.food.find({"fruit" : {"$size" : 3}})

其他操作符

还有一些其他操作符如下:

操作符描述示例
$regex匹配正则表达式{ name: { $regex: /^A/ } }
$text进行文本搜索{ $text: { $search: "coffee" } }
$where使用 JavaScript 表达式进行条件过滤{ $where: "this.age > 25" }

查找名字以 "A" 开头的文档:

db.myCollection.find({ name: { $regex: /^A/ } })

特定类型查询

null

null 会匹配值为 null 的文档以及缺少这个键的所有文档

> db.c.find({"z" : null})

如果仅想匹配键值为 null 的文档,则需要检查该键的值是否为 null,并且通过 "$exists" 条件确认该键已存在。

db.c.find({"z" : {"$eq" : null, "$exists" : true}})

正则表达式

$regex" 可以在查询中为字符串的模式匹配提供正则表达式功能。正则表达式对于灵活的字符串匹配非常有用。

> db.users.find( {"name" : {"$regex" : /joe/i } })

MongoDB 会使用 Perl 兼容的正则表达式(PCRE)库来对正则表达式进行匹配。任何 PCRE 支持的正则表达式语法都能被 MongoDB 接受。在查询中使用正则表达式之前,最好先在 JavaScript shell 中检查一下语法,这样可以确保匹配与预想的一致。

查询内嵌文档

假设文档形式如下:

{
  "name": {
    "first": "Joe",
    "last": "Schmoe"
  },
  "age": 45
}

查询 first 属性为 Joe,last 属性为 Schmoe 的文档:

db.people.find({"name.first" : "Joe", "name.last" : "Schmoe"})

$where 查询

$where 允许你在查询中执行任意的 JavaScript 代码。这样就能在查询中做大部分事情了。除非绝对必要,否则不应该使用 "$where" 查询:它们比常规查询慢得多。

db.foo.find({"$where" : function () {
	for (var current in this) {
		for (var other in this) {
			if (current != other && this[current] == this[other]) {
				return true;
			}
		}
	}
	return false;
}});

游标

数据库会使用游标返回 find 的执行结果。

var cursor = db.people.find();
cursor.forEach(function(x) {
	print(x.name);
});

limit() 方法用于限制查询结果返回的文档数量。

db.collection.find().limit(10);

skip() 方法用于跳过指定数量的文档,从而实现分页或分批查询。

// 跳过前 10 个文档,返回接下来的 10 个文档
db.collection.find().skip(10).limit(10);

sort() 方法可以通过参数指定排序的字段。

// 先按 age 字段升序排序,再按 createdAt 字段降序排序
db.myCollection.find().sort({ age: 1, createdAt: -1 });

参考资料

评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v2.15.7