MongoDB 基础知识详解与教程 – wiki基地


MongoDB 基础知识详解与入门教程

引言:告别传统,拥抱文档——为什么选择 MongoDB?

在当今瞬息万变的数字世界中,数据呈爆炸式增长,数据结构也越来越复杂多样。传统的、基于严格表结构的关系型数据库(RDBMS)在处理海量非结构化或半结构化数据时,往往显得力不从心,或者需要付出巨大的设计和维护成本。此时,NoSQL (Not Only SQL) 数据库应运而生,它们提供了更灵活、更具扩展性的数据存储和处理方案。

在这众多 NoSQL 数据库中,MongoDB 凭借其面向文档的存储模式、强大的查询能力、高可用性和易扩展性,迅速崛起并成为最受欢迎的 NoSQL 数据库之一。从 Web 应用、移动应用后台、大数据分析到物联网,MongoDB 在各种场景下都展现出卓越的性能和灵活性。

本篇文章将带你深入了解 MongoDB 的基础知识,从核心概念到安装配置,再到实际操作中的 CRUD (Create, Read, Update, Delete) 以及更进一步的索引和聚合等基础功能。无论你是数据库新手,还是拥有关系型数据库背景想转型 NoSQL,本文都将为你提供一个全面而深入的指引。

第一部分:MongoDB 核心概念与特性

在深入操作之前,理解 MongoDB 的核心概念是至关重要的。它与关系型数据库有着本质的区别。

1. 文档 (Document)

这是 MongoDB 中数据的基本单元。文档是一个类似于 JSON (JavaScript Object Notation) 的结构化数据记录,由键值对组成。例如:

json
{
"name": "张三",
"age": 30,
"city": "北京",
"interests": ["阅读", "旅行"],
"contact": {
"email": "[email protected]",
"phone": "13800000000"
}
}

文档可以包含嵌套的文档、数组以及各种数据类型(字符串、数字、布尔值、日期等)。文档的这种灵活性使得 MongoDB 特别适合存储结构不固定或经常变化的数据。

与 RDBMS 的对比: RDBMS 中的一行 (Row)。

2. 集合 (Collection)

集合是 MongoDB 中存储文档的容器。一个集合可以包含任意数量的文档。与关系型数据库的表不同,集合不需要定义固定的结构(Schema-less)。集合中的文档可以拥有完全不同的字段。当然,在实际应用中,同一个集合中的文档通常会共享相似的结构,以便于查询和管理,但这并不是强制的。

与 RDBMS 的对比: RDBMS 中的一张表 (Table)。

3. 数据库 (Database)

数据库是 MongoDB 的顶层组织单元,它包含多个集合。一个 MongoDB 实例可以托管多个数据库,每个数据库相互独立。

与 RDBMS 的对比: RDBMS 中的一个数据库 (Database) 或模式 (Schema)。

4. BSON (Binary JSON)

MongoDB 在内部使用 BSON 格式来存储文档。BSON 是 JSON 的二进制序列化格式。它扩展了 JSON 的数据类型,例如加入了日期、二进制数据、正则表达式等,并且更易于在各种编程语言中解析和生成。BSON 使得数据存储和传输更加高效。

5. _id 字段

每个 MongoDB 文档在创建时,都会自动生成一个唯一的 _id 字段(如果文档插入时没有指定 _id)。这个字段是文档的主键,用于唯一标识文档。_id 的默认值是一个 ObjectId 类型的值,它是一个12字节的标识符,由时间戳、机器标识符、进程标识符和一个计数器组成,确保了全局唯一性。

6. 无模式 (Schema-less / Schema-on-Read)

这是 MongoDB 与 RDBMS 最大的区别之一。RDBMS 需要提前定义表的结构(Schema),包括字段名、数据类型等。而 MongoDB 的集合没有强制的模式。你可以向同一个集合中插入具有不同字段的文档。这种灵活性在开发初期或需求频繁变更的场景下非常有优势。模式约束是在读取数据时由应用程序层面来处理,因此也常被称为 “Schema-on-Read”。

7. 高可用性 (High Availability) – 副本集 (Replica Set)

MongoDB 通过副本集来实现高可用性。副本集是一组维护相同数据集的 MongoDB 实例。其中一个节点是主节点 (Primary),负责处理所有的写操作;其他节点是从节点 (Secondaries),它们复制主节点的数据,可以处理读操作。当主节点发生故障时,副本集会自动选举一个新的主节点,保证服务不中断。

8. 可扩展性 (Scalability) – 分片 (Sharding)

为了处理海量数据和高并发请求,MongoDB 提供了分片机制。分片将数据水平地分散到不同的服务器(分片)上。每个分片存储数据的一个子集。客户端的请求会被一个称为 mongos 的路由进程转发到正确的那个或多个分片上执行。分片可以显著提高数据库的存储容量和吞吐量。

9. 强大的查询语言 (MQL – MongoDB Query Language)

MongoDB 提供了一套丰富灵活的查询语言,可以方便地进行基于字段、范围、正则表达式等的各种查询。它支持嵌套文档和数组的查询,还提供了聚合框架 (Aggregation Framework) 进行复杂的数据处理和分析。

第二部分:安装与连接

在开始实际操作之前,你需要在你的系统上安装 MongoDB。安装过程因操作系统的不同而略有差异,具体步骤可以参考官方文档:https://docs.mongodb.com/manual/installation/

安装完成后,你需要启动 MongoDB 服务器 (mongod 进程) 和客户端 (mongosh 或旧版本的 mongo 进程)。

启动服务器 (mongod):

通常只需要运行 mongod 命令即可。如果需要指定数据存储路径或其他配置,可以使用命令行参数或配置文件。

bash
mongod --dbpath /path/to/your/data

连接到服务器 (mongosh):

打开另一个终端窗口,运行 mongosh 命令连接到正在运行的 MongoDB 服务器。默认连接到本地主机的 27017 端口。

bash
mongosh

连接成功后,你将看到 MongoDB Shell 的提示符 >

第三部分:基本操作 (CRUD)

MongoDB Shell (mongosh) 是一个交互式 JavaScript 环境,用于与 MongoDB 进行交互。所有的数据库操作都通过操作 db 对象来完成。

1. 选择/切换数据库

使用 use 命令选择要操作的数据库。如果数据库不存在,MongoDB 会在你向其写入数据时自动创建。

javascript
use myNewDatabase

这将切换到 myNewDatabase 数据库。如果该数据库不存在,它会在你第一次向其中写入数据(例如插入文档)时被创建。

你可以使用 db.getName() 查看当前所在的数据库,或使用 show dbs 列出所有的数据库。

2. 创建 (Create) – 插入文档

向集合中插入文档有两种主要方法:insertOneinsertMany

插入单个文档 (insertOne):

“`javascript
// 切换到数据库
use myDatabase

// 插入一个文档到 ‘users’ 集合
db.users.insertOne({
name: “张三”,
age: 30,
city: “北京”
})

// 插入一个带有嵌套结构和数组的文档
db.users.insertOne({
name: “李四”,
age: 25,
interests: [“音乐”, “电影”],
contact: {
email: “[email protected]
}
})
“`

如果 users 集合不存在,它会在插入第一个文档时自动创建。insertOne 返回一个包含插入文档 _id 的结果对象。

插入多个文档 (insertMany):

javascript
// 插入多个文档到 'products' 集合
db.products.insertMany([
{ name: "笔记本电脑", brand: "Dell", price: 7500, tags: ["电子", "办公"] },
{ name: "机械键盘", brand: "Logitech", price: 600, tags: ["电子", "外设"] },
{ name: "显示器", brand: "HP", price: 1500, tags: ["电子", "办公"] }
])

insertMany 接受一个文档数组,并返回一个包含所有插入文档 _id 的结果对象。

3. 读取 (Read) – 查询文档

查询是 MongoDB 最强大的功能之一。你可以使用 findfindOne 方法来检索文档。

查找所有文档 (find):

javascript
// 查找 'users' 集合中的所有文档
db.users.find()

find() 不带参数将返回集合中的所有文档。默认情况下,find() 返回一个游标 (Cursor),在 shell 中会自动打印前20个文档。

查找单个文档 (findOne):

javascript
// 查找 'users' 集合中的一个文档
db.users.findOne() // 返回找到的第一个文档

findOne() 返回一个文档对象,如果没有找到匹配的文档,则返回 null

带条件的查询:

find()findOne() 方法都可以接受一个查询文档作为第一个参数,用于指定查询条件。查询文档使用 { <field>: <value> } 的形式,表示匹配特定字段等于特定值的文档。

“`javascript
// 查找名字为 “张三” 的用户
db.users.find({ name: “张三” })

// 查找年龄为 25 岁的用户
db.users.find({ age: 25 })

// 查找城市为 “北京” 且年龄大于等于 30 岁的用户 (使用 $gte 运算符)
db.users.find({ city: “北京”, age: { $gte: 30 } })
“`

查询运算符:

MongoDB 提供了丰富的查询运算符来构建复杂的查询条件:

  • 比较运算符:

    • $eq: 等于
    • $ne: 不等于
    • $gt: 大于
    • $gte: 大于等于
    • $lt: 小于
    • $lte: 小于等于
    • $in: 匹配数组中的任意一个值
    • $nin: 不匹配数组中的任何值

    “`javascript
    // 查找价格在 1000 到 2000 之间的产品
    db.products.find({ price: { $gt: 1000, $lt: 2000 } })

    // 查找品牌是 Dell 或 HP 的产品
    db.products.find({ brand: { $in: [“Dell”, “HP”] } })
    “`

  • 逻辑运算符:

    • $and: 逻辑与 (通常隐式使用,如前面的多条件查询)
    • $or: 逻辑或
    • $not: 逻辑非
    • $nor: 逻辑非或 (都不满足)

    “`javascript
    // 查找年龄小于 30 或城市是 “上海” 的用户
    db.users.find({ $or: [{ age: { $lt: 30 } }, { city: “上海” }] })

    // 查找年龄不大于等于 30 的用户 (即小于 30)
    db.users.find({ age: { $not: { $gte: 30 } } })
    “`

  • 元素运算符:

    • $exists: 检查字段是否存在
    • $type: 按 BSON 类型查找字段

    javascript
    // 查找有 email 字段的用户
    db.users.find({ "contact.email": { $exists: true } }) // 注意嵌套字段的访问方式 "parent.child"

  • 数组运算符:

    • $all: 匹配包含所有指定元素的数组
    • $elemMatch: 匹配数组中至少有一个元素符合所有指定条件的文档
    • $size: 匹配指定长度的数组

    “`javascript
    // 查找同时喜欢阅读和旅行的用户
    db.users.find({ interests: { $all: [“阅读”, “旅行”] } })

    // 查找标签数组长度为 2 的产品
    db.products.find({ tags: { $size: 2 } })
    “`

投影 (Projection):

查询方法的第二个参数是投影文档,用于指定返回的文档中包含或排除哪些字段。{ field: 1 } 表示包含该字段,{ field: 0 } 表示排除该字段。_id 字段默认包含,除非明确排除。

“`javascript
// 查找所有用户,只返回 name 和 city 字段 (并排除 _id)
db.users.find({}, { name: 1, city: 1, _id: 0 })

// 查找所有产品,排除 tags 字段
db.products.find({}, { tags: 0 })
“`

排序 (Sort):

使用 sort() 方法对查询结果进行排序。排序文档指定字段和排序方向(1 为升序,-1 为降序)。

“`javascript
// 按年龄升序查找所有用户
db.users.find().sort({ age: 1 })

// 按价格降序查找所有产品,如果价格相同,则按品牌升序排序
db.products.find().sort({ price: -1, brand: 1 })
“`

限制和跳过 (Limit and Skip):

使用 limit() 方法限制返回结果的数量,使用 skip() 方法跳过指定数量的文档(常用于分页)。

“`javascript
// 查找前 3 个产品
db.products.find().limit(3)

// 查找第 4 到第 6 个产品 (跳过前 3 个,取接下来的 3 个)
db.products.find().skip(3).limit(3)
“`

可以将链式调用这些方法:db.collection.find(query, projection).sort(sort).limit(limit).skip(skip)

4. 更新 (Update) – 修改文档

更新文档使用 updateOne, updateMany, 或 replaceOne 方法。通常结合更新运算符使用。

更新单个文档 (updateOne):

接受一个查询文档(匹配要更新的文档)和一个更新文档(指定如何更新)。

“`javascript
// 更新名字为 “张三” 的用户,设置其年龄为 31
db.users.updateOne(
{ name: “张三” },
{ $set: { age: 31 } } // 使用 $set 运算符设置字段值
)

// 更新名字为 “李四” 的用户,增加一个 province 字段
db.users.updateOne(
{ name: “李四” },
{ $set: { “contact.province”: “上海” } } // 更新嵌套字段
)

// 更新名字为 “张三” 的用户,如果城市不是 “北京”,则设置城市为 “北京” (使用 $setOnInsert 在插入时设置)
// 注意:这里是 updateOne,如果文档存在,只执行 $set;如果使用了 upsert: true 且文档不存在,会创建新文档并应用 $set 和 $setOnInsert
db.users.updateOne(
{ name: “张三” },
{ $set: { city: “北京” } }
)
“`

常用的更新运算符:

  • $set: 设置字段值。如果字段不存在则创建。
  • $unset: 删除字段。
  • $inc: 原子地增加或减少字段值(仅用于数字)。
  • $push: 向数组末尾添加元素。
  • $addToSet: 向数组中添加元素,只有当该元素不存在时才添加(类似集合)。
  • $pop: 从数组的开头或末尾删除元素 (-1 删除第一个,1 删除最后一个)。
  • $pull: 从数组中删除所有匹配指定条件的元素。

“`javascript
// 给名字为 “张三” 的用户添加一个兴趣 “编程”
db.users.updateOne(
{ name: “张三” },
{ $push: { interests: “编程” } }
)

// 给李四添加一个兴趣 “编程”,如果已经有了则不添加
db.users.updateOne(
{ name: “李四” },
{ $addToSet: { interests: “编程” } }
)

// 将价格大于等于 1000 的产品价格降低 10%
db.products.updateMany(
{ price: { $gte: 1000 } },
{ $mul: { price: 0.9 } } // 使用 $mul 运算符乘以一个值
)
“`

更新多个文档 (updateMany):

updateOne 类似,但更新所有匹配查询条件的文档。

javascript
// 将所有城市为 "北京" 的用户年龄增加 1 岁
db.users.updateMany(
{ city: "北京" },
{ $inc: { age: 1 } }
)

替换单个文档 (replaceOne):

用一个全新的文档替换匹配查询条件的第一个文档。新的文档必须只包含字段和值(不能包含更新运算符)。通常会保留 _id 字段。

javascript
// 替换名字为 "李四" 的用户文档
db.users.replaceOne(
{ name: "李四" },
{
name: "李四 - Replaced",
status: "inactive",
lastUpdated: new Date()
}
)

Upsert (更新或插入):

updateOneupdateMany 方法中,可以添加 { upsert: true } 选项。如果查询条件匹配到文档,则执行更新;如果没有匹配到文档,则根据查询条件和更新文档创建一个新的文档。

javascript
// 查找名字为 "王五" 的用户,如果不存在,则创建
db.users.updateOne(
{ name: "王五" },
{ $set: { age: 28, city: "广州" } },
{ upsert: true } // 如果王五不存在,则插入 { name: "王五", age: 28, city: "广州" }
)

5. 删除 (Delete) – 移除文档

删除文档使用 deleteOnedeleteMany 方法。

删除单个文档 (deleteOne):

接受一个查询文档,删除匹配条件的第一个文档。

javascript
// 删除名字为 "王五" 的用户
db.users.deleteOne({ name: "王五" })

删除多个文档 (deleteMany):

接受一个查询文档,删除所有匹配条件的文档。

“`javascript
// 删除所有年龄小于 25 岁的用户
db.users.deleteMany({ age: { $lt: 25 } })

// 删除 ‘products’ 集合中的所有文档 (清空集合)
db.products.deleteMany({})
“`

6. 丢弃集合/数据库

丢弃集合 (drop):

javascript
// 丢弃 'products' 集合
db.products.drop()

丢弃数据库 (dropDatabase):

“`javascript
// 切换到要丢弃的数据库
use myDatabase

// 丢弃当前数据库
db.dropDatabase()
“`
注意: 这些操作是不可逆的,请谨慎使用!

第四部分:进阶基础 – 索引与聚合框架初步

掌握了基本的 CRUD 操作后,我们可以进一步了解两个对性能和数据处理至关重要的概念:索引和聚合框架。

1. 索引 (Indexes)

索引可以显著提高查询效率,尤其是在大型集合中。MongoDB 支持多种类型的索引。

创建索引 (createIndex):

在需要频繁查询的字段上创建索引。索引文档指定字段和索引方向(1 为升序,-1 为降序)。

“`javascript
// 在 ‘users’ 集合的 ‘age’ 字段上创建升序索引
db.users.createIndex({ age: 1 })

// 在 ‘products’ 集合的 ‘price’ 字段上创建降序索引
db.products.createIndex({ price: -1 })

// 创建复合索引:先按 brand 升序,再按 price 升序
db.products.createIndex({ brand: 1, price: 1 })
“`

查看索引 (getIndexes):

javascript
// 查看 'users' 集合上的所有索引
db.users.getIndexes()

每个集合默认都会在 _id 字段上有一个唯一的升序索引。

丢弃索引 (dropIndex):

使用索引名称丢弃索引。

“`javascript
// 丢弃 ‘age_1’ 索引 (索引名称通常是字段名加方向,如 ‘age_1’, ‘price_-1’)
db.users.dropIndex(“age_1”)

// 丢弃所有索引 (除了 _id 索引)
db.users.dropIndexes()
“`

何时使用索引?

  • 经常用于查询条件的字段。
  • 用于排序的字段。
  • 用于连接(在嵌入或引用关系中模拟连接)。

索引的代价: 索引会占用存储空间,并且在插入、更新、删除文档时会增加额外的写开销,因为数据库需要同步更新索引。因此,不应在所有字段上都创建索引,而应根据实际的查询模式进行优化。

2. 聚合框架 (Aggregation Framework)

聚合框架是 MongoDB 中用于进行数据转换和分析的强大工具。它通过一个管道 (Pipeline) 模型工作,文档流经一系列阶段 (Stages),每个阶段对文档流进行处理,然后将结果传递给下一个阶段。

核心概念:管道 (Pipeline) 与阶段 (Stages)

一个聚合操作就是一个管道,由多个阶段组成。每个阶段执行一个特定的数据处理任务,例如:

  • $match: 过滤文档 (类似 find)
  • $group: 按指定字段对文档进行分组,并执行聚合计算(如计数、求和、平均值等)
  • $project: 重塑文档的结构,可以添加、移除或重命名字段
  • $sort: 对文档流进行排序
  • $limit: 限制文档数量
  • $skip: 跳过文档数量
  • $lookup: 执行左外连接,从同一个数据库的另一个集合中查找相关文档
  • $unwind: 将数组字段中的每个元素拆分成单独的文档

一个简单的聚合例子:

javascript
// 统计每个品牌的产品数量和总价格
db.products.aggregate([
// 阶段 1: $group - 按品牌分组,计算数量和总价格
{
$group: {
_id: "$brand", // 按 brand 字段分组 (_id 表示分组键)
count: { $sum: 1 }, // 计算每个组的文档数量
totalPrice: { $sum: "$price" } // 计算每个组的 price 字段的总和
}
},
// 阶段 2: $sort - 按总价格降序排序结果
{
$sort: { totalPrice: -1 }
}
])

这个例子演示了如何使用 $group 进行分组计算,以及如何使用 $sort 对结果排序。聚合框架非常灵活,可以实现各种复杂的数据分析和报表生成需求。更复杂的聚合操作可能涉及更多的阶段和运算符。

第五部分:应用与最佳实践概览

1. Schema 设计 (Schema Design)

尽管 MongoDB 是无模式的,但良好的 Schema 设计对性能至关重要。主要有两种设计模式:

  • 嵌入 (Embedding): 将相关数据作为子文档或数组嵌入到主文档中。适合数据之间关系紧密,且子文档数量有限的情况。优点是查询时无需额外的查询或连接,性能好。缺点是文档可能变得非常大,更新嵌套数组可能复杂。
    • 示例: 用户文档中嵌入地址信息,订单文档中嵌入商品列表。
  • 引用 (Referencing): 在一个文档中存储另一个文档的 _id,通过引用来建立关系。适合数据之间关系不那么紧密,或者相关文档数量较多且可能频繁独立更新的情况。需要额外的查询(在应用层面或使用聚合框架 $lookup)来获取关联数据。
    • 示例: 用户文档中引用其订单的 _id 列表,商品文档中引用其评论的 _id 列表。

通常会结合使用这两种模式,根据具体的数据访问模式进行权衡。

2. 安全性 (Security)

生产环境中必须启用安全认证。可以通过基于角色的访问控制 (RBAC) 来限制用户对数据库和集合的操作权限。建议启用 SSL/TLS 加密连接。

3. 连接管理 (Connection Management)

在使用各种编程语言的驱动程序连接 MongoDB 时,应使用连接池来高效管理数据库连接,避免频繁地创建和关闭连接。

4. 监控与备份 (Monitoring and Backup)

定期监控 MongoDB 实例的性能指标(如 CPU、内存、网络、查询延迟等)和状态是必要的。同时,建立可靠的数据备份和恢复策略是保证数据安全的关键。

结论:MongoDB 的力量与下一步

通过本文,我们详细探讨了 MongoDB 的核心概念,学习了如何进行基本的 CRUD 操作,并初步了解了索引和聚合框架的使用。MongoDB 的文档模型为处理灵活多变的数据提供了便利,其分布式特性使其天然具备高可用性和横向扩展的能力。

作为入门,掌握 CRUD 操作和基本的查询语法是基础。接下来,你可以进一步探索以下主题:

  • 更深入地学习 MongoDB 的查询语言,包括文本搜索、地理空间查询等。
  • 精通聚合框架,处理更复杂的数据分析任务。
  • 深入理解索引的工作原理和高级索引类型。
  • 学习如何搭建和管理副本集 (Replica Set) 以实现高可用。
  • 学习如何搭建和管理分片集群 (Sharded Cluster) 以实现大数据量和高并发处理。
  • 了解并使用各种编程语言(如 Python, Node.js, Java 等)的 MongoDB 驱动程序来构建应用程序。
  • 学习 MongoDB 的管理工具,如 MongoDB Compass (GUI 工具) 或命令行工具。

MongoDB 是一个功能强大且不断发展的数据库系统。随着你对它的深入学习和实践,你将能够利用它的优势来构建高性能、可伸缩且灵活的应用程序。

希望这篇教程能够帮助你迈出学习 MongoDB 的坚实第一步!祝你学习愉快!


发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部