【玩转MongoDB:第二章】BSON数据类型与文档结构

【上节回顾】

在上一章节中,我们学习了MongoDB的基本概念,包括数据库、集合和文档。我们了解到MongoDB是一个开源、分布式的文档数据库,相比关系型数据库采用了不同的数据组织方式。我们明确了MongoDB中的数据库是集合的容器,集合是文档的容器,而文档是MongoDB中数据的基本单位,由键值对组成并以BSON格式存储。我们还学习了如何安装MongoDB,创建数据库和集合,以及在集合中插入和查询文档。这些基础知识为我们继续深入学习MongoDB打下了坚实的基础。本节课程,我们将更深入地了解BSON数据类型与文档结构,这是理解和有效使用MongoDB的关键。

【实战介绍】

本节课程是MongoDB实战课程第一部分"MongoDB基础入门"中的第二个章节。在理解了MongoDB的基本概念后,我们需要深入学习MongoDB使用的数据格式——BSON以及各种数据类型,这是有效操作MongoDB的基础。

本节课程主要讲解:

  • BSON格式及其与JSON的关系与区别

  • MongoDB支持的数据类型详解

  • 文档结构设计与嵌套文档

  • 数组操作与使用技巧

  • 文档大小限制与处理方法

学习BSON数据类型与文档结构的价值在于:掌握这些知识可以帮助我们更精确地控制数据存储格式,优化数据访问性能,合理设计文档结构以适应业务需求,避免常见的文档设计陷阱。

【实战任务内容】

1. BSON格式简介

BSON(Binary JSON)是MongoDB使用的二进制格式,专为数据存储和网络传输而设计。

1.1 BSON与JSON的关系

BSON基于JSON格式,但进行了二进制编码,并增加了额外的数据类型支持。

示例:JSON格式的用户文档:

{
  "name": "张三",
  "age": 28,
  "registered": true
}

转换为BSON后,虽然人眼看不到二进制形式,但它能更高效地存储,并支持更多数据类型。

1.2 BSON的优势
  • 轻量级:快速遍历,减少存储空间

  • 可遍历性:便于解析和创建

  • 高效性:编码和解码速度快

  • 额外数据类型:支持日期、二进制数据等JSON不支持的类型

示例:在BSON中,可以直接存储日期类型:

{
  "name": "张三",
  "birthday": new Date("1995-05-15"),
  "created": ISODate("2023-10-20T08:00:00Z")
}

2. MongoDB数据类型

MongoDB支持多种数据类型,使数据存储更灵活精确。

2.1 基本数据类型
数据类型描述示例
String字符串,UTF-8编码"name": "张三"
Integer整数(32位/64位)"age": 28
Double浮点数"score": 98.5
Boolean布尔值(true/false)"active": true
Null空值"middleName": null
Date日期时间(毫秒精度)"created": new Date()
ObjectId12字节的ID"_id": ObjectId("...")
Array数组/列表"tags": ["mongodb", "database"]
Embedded Document嵌入式文档"address": {"city": "北京"}
Binary Data二进制数据"file": BinData(0, "...")

示例:创建包含多种数据类型的文档:

{
  "_id": ObjectId("507f1f77bcf86cd799439011"),
  "name": "商品A",
  "price": 199.99,
  "inStock": true,
  "category": ["电子", "配件"],
  "details": {
    "model": "X100",
    "color": "黑色",
    "weight": 120.5
  },
  "createdAt": new Date()
}
2.2 特殊数据类型

MongoDB还支持一些特殊数据类型,满足特定需求:

  • ObjectId:MongoDB自动为文档创建的唯一标识符

  • Timestamp:内部使用的时间戳类型

  • Regular Expression:正则表达式类型

  • JavaScript Code:存储JavaScript代码

  • MinKey/MaxKey:比较用的最小/最大键

示例:使用ObjectId和正则表达式:

{
  "_id": ObjectId("507f1f77bcf86cd799439011"),
  "name": "张三",
  "email": "zhangsan@example.com",
  "searchPattern": /^zhang/i
}

3. 文档结构设计

3.1 嵌入式文档

嵌入式文档是指文档内部包含的另一个文档,形成一种层次结构。

优点

  • 数据本地化,减少查询次数

  • 原子操作保证一致性

  • 适合一对一和一对少量多的关系

示例:用户文档中嵌入地址信息:

{
  "_id": ObjectId("507f1f77bcf86cd799439011"),
  "name": "李四",
  "email": "lisi@example.com",
  "address": {
    "street": "建国路88号",
    "city": "上海",
    "postcode": "200120",
    "country": "中国"
  }
}
3.2 引用文档

引用文档通过存储其他文档的ID来建立关系,类似关系型数据库的外键。

优点

  • 避免数据重复

  • 适合一对多和多对多关系

  • 文档大小可控

示例:订单引用用户和产品:

// 用户文档
{
  "_id": ObjectId("user123"),
  "name": "王五",
  "email": "wangwu@example.com"
}
​
// 产品文档
{
  "_id": ObjectId("product456"),
  "name": "智能手机",
  "price": 2999
}
​
// 订单文档(引用用户和产品)
{
  "_id": ObjectId("order789"),
  "userId": ObjectId("user123"),
  "items": [
    { "productId": ObjectId("product456"), "quantity": 1 }
  ],
  "totalAmount": 2999,
  "status": "已支付",
  "createdAt": new Date()
}

4. 数组操作

MongoDB对数组类型提供强大支持,可以存储多个值并进行复杂查询。

4.1 数组基本操作
  • 创建数组字段

  • 添加、删除、更新数组元素

  • 数组查询和过滤

示例:包含数组的文档:

{
  "_id": ObjectId("article123"),
  "title": "MongoDB数组操作指南",
  "tags": ["数据库", "MongoDB", "NoSQL"],
  "comments": [
    { "user": "用户A", "text": "很有帮助", "date": new Date() },
    { "user": "用户B", "text": "内容详细", "date": new Date() }
  ]
}
4.2 数组中的嵌入式文档

数组可以包含嵌入式文档,形成更复杂的数据结构。

示例:学生课程成绩记录:

{
  "_id": ObjectId("student123"),
  "name": "赵六",
  "courses": [
    { "name": "数据库", "code": "DB101", "score": 92 },
    { "name": "编程基础", "code": "CS101", "score": 85 },
    { "name": "网络技术", "code": "NET101", "score": 88 }
  ],
  "gpa": 3.8
}

5. 文档大小限制

MongoDB对文档大小有16MB的限制,这是为了保持性能和可管理性。

5.1 处理大文档的策略
  • 使用引用代替嵌入

  • 拆分文档

示例:拆分产品文档和产品详情:

// 产品基本信息
{
  "_id": ObjectId("product123"),
  "name": "高性能服务器",
  "price": 9999,
  "category": "电子设备"
}

// 产品详细信息
{
  "_id": ObjectId("productDetail123"),
  "productId": ObjectId("product123"),
  "specifications": { /* 详细规格信息 */ },
  "manual": { /* 使用手册内容 */ },
  "reviews": [ /* 用户评价列表 */ ]
}

【实战操作】

实战1:使用MongoDB Shell查看和创建不同数据类型

首先,连接到MongoDB服务器:

root@ssdevops.com:~$ mongo

查看当前使用的数据库,如果需要,创建并切换到一个新数据库:

root@ssdevops.com:~$ show dbs
root@ssdevops.com:~$ use datatypes_demo

创建包含各种数据类型的文档:

root@ssdevops.com:~$ db.types_example.insertOne({
  string_value: "这是一个字符串",
  int_value: NumberInt(42),
  long_value: NumberLong("9223372036854775807"),
  double_value: 3.14159,
  boolean_value: true,
  null_value: null,
  date_value: new Date(),
  timestamp_value: new Timestamp(),
  object_id: ObjectId(),
  array_value: [1, 2, 3, "四", "五"],
  embedded_doc: {
    field1: "嵌入式文档字段1",
    field2: 123
  },
  regex_value: /^M/i
})

查看插入的文档:

root@ssdevops.com:~$ db.types_example.find().pretty()

执行结果截图:

image-20250523104927557

实战2:ObjectId详解与操作

查看ObjectId的结构和生成:

root@ssdevops.com:~$ var id = ObjectId()
root@ssdevops.com:~$ print("生成的ObjectId:", id)
root@ssdevops.com:~$ print("十六进制字符串:", id.toString())
root@ssdevops.com:~$ print("创建时间戳:", id.getTimestamp())
root@ssdevops.com:~$ print("创建时间:", new Date(id.getTimestamp()))

使用自定义ObjectId创建文档:

root@ssdevops.com:~$ var customId = ObjectId("507f1f77bcf86cd799439011")
root@ssdevops.com:~$ db.custom_ids.insertOne({
  _id: customId,
  description: "自定义ObjectId示例"
})

通过ObjectId查询文档:

root@ssdevops.com:~$ db.custom_ids.findOne({_id: ObjectId("507f1f77bcf86cd799439011")})

执行结果截图:

image-20250523105100162

实战3:日期类型操作与查询

插入包含日期的文档:

root@ssdevops.com:~$ db.date_examples.insertMany([
  {
    title: "今天的文档",
    created: new Date(),
    updated: new Date(),
    type: "current"
  },
  {
    title: "昨天的文档",
    created: new Date(new Date().setDate(new Date().getDate() - 1)),
    updated: new Date(),
    type: "recent"
  },
  {
    title: "上周的文档",
    created: new Date(new Date().setDate(new Date().getDate() - 7)),
    updated: new Date(new Date().setDate(new Date().getDate() - 2)),
    type: "old"
  },
  {
    title: "去年的文档",
    created: new Date(new Date().setFullYear(new Date().getFullYear() - 1)),
    updated: new Date(new Date().setMonth(new Date().getMonth() - 6)),
    type: "archive"
  }
])

基于日期查询文档:

// 查找今天创建的文档
root@ssdevops.com:~$ var today = new Date()
root@ssdevops.com:~$ today.setHours(0, 0, 0, 0)
root@ssdevops.com:~$ var tomorrow = new Date(today)
root@ssdevops.com:~$ tomorrow.setDate(tomorrow.getDate() + 1)
root@ssdevops.com:~$ db.date_examples.find({
  created: {
    $gte: today,
    $lt: tomorrow
  }
}).pretty()

// 查找一周内更新的文档
root@ssdevops.com:~$ var weekAgo = new Date()
root@ssdevops.com:~$ weekAgo.setDate(weekAgo.getDate() - 7)
root@ssdevops.com:~$ db.date_examples.find({
  updated: {
    $gte: weekAgo
  }
}).pretty()

// 按创建日期排序
root@ssdevops.com:~$ db.date_examples.find().sort({created: -1}).pretty()

执行结果截图:

image-20250523105243148

实战4:数组操作技巧

创建包含数组的文档集合:

root@ssdevops.com:~$ db.array_examples.insertMany([
  {
    name: "产品A",
    tags: ["电子", "智能", "家用"],
    ratings: [4, 5, 3, 5, 4]
  },
  {
    name: "产品B",
    tags: ["办公", "电子", "配件"],
    ratings: [3, 3, 2, 4]
  },
  {
    name: "产品C",
    tags: ["家用", "厨房", "电器"],
    ratings: [5, 5, 4, 5]
  }
])

数组查询操作:

// 查找包含特定标签的产品
root@ssdevops.com:~$ db.array_examples.find({tags: "电子"}).pretty()

// 查找同时包含多个标签的产品
root@ssdevops.com:~$ db.array_examples.find({tags: {$all: ["电子", "家用"]}}).pretty()

// 使用$in操作符(查找包含任一标签的产品)
root@ssdevops.com:~$ db.array_examples.find({tags: {$in: ["家用", "办公"]}}).pretty()

// 按数组大小查询
root@ssdevops.com:~$ db.array_examples.find({tags: {$size: 3}}).pretty()

// 查找评分中包含5分的产品
root@ssdevops.com:~$ db.array_examples.find({ratings: 5}).pretty()

// 查找所有评分均大于3的产品
root@ssdevops.com:~$ db.array_examples.find({ratings: {$gt: 3}}).pretty()

// 查找任意评分大于4的产品
root@ssdevops.com:~$ db.array_examples.find({ratings: {$elemMatch: {$gt: 4}}}).pretty()

数组更新操作:

// 添加新元素到数组中
root@ssdevops.com:~$ db.array_examples.updateOne(
  {name: "产品A"},
  {$push: {tags: "畅销"}}
)

// 一次添加多个元素
root@ssdevops.com:~$ db.array_examples.updateOne(
  {name: "产品B"},
  {$push: {tags: {$each: ["新品", "促销"]}}}
)

// 从数组中移除元素
root@ssdevops.com:~$ db.array_examples.updateOne(
  {name: "产品C"},
  {$pull: {tags: "厨房"}}
)

// 更新数组中的特定元素
root@ssdevops.com:~$ db.array_examples.updateOne(
  {name: "产品A", ratings: 3},
  {$set: {"ratings.$": 4}}
)

// 查看更新结果
root@ssdevops.com:~$ db.array_examples.find().pretty()

执行结果截图:

image-20250523105436817

实战5:嵌入式文档与引用操作

创建使用嵌入式文档的集合:

root@ssdevops.com:~$ db.embedded_example.insertMany([
  {
    name: "张三",
    contact: {
      email: "zhangsan@example.com",
      phone: "13800138000",
      address: {
        city: "北京",
        district: "海淀区",
        street: "中关村大街1号"
      }
    },
    hobbies: [
      {name: "读书", years: 5},
      {name: "摄影", years: 2}
    ]
  },
  {
    name: "李四",
    contact: {
      email: "lisi@example.com",
      phone: "13900139000",
      address: {
        city: "上海",
        district: "浦东新区",
        street: "张江高科技园区"
      }
    },
    hobbies: [
      {name: "游泳", years: 3},
      {name: "钢琴", years: 7}
    ]
  }
])

嵌入式文档查询:

// 嵌入式文档完全匹配
root@ssdevops.com:~$ db.embedded_example.find({
  "contact.address.city": "北京"
}).pretty()

// 多层嵌入式文档查询
root@ssdevops.com:~$ db.embedded_example.find({
  "contact.address.district": "浦东新区"
}).pretty()

// 查询数组中的嵌入式文档
root@ssdevops.com:~$ db.embedded_example.find({
  "hobbies.name": "摄影"
}).pretty()

// 使用$elemMatch操作符进行复合条件查询
root@ssdevops.com:~$ db.embedded_example.find({
  hobbies: {
    $elemMatch: {
      name: "钢琴",
      years: {$gt: 5}
    }
  }
}).pretty()

创建使用引用关系的集合:

// 用户集合
root@ssdevops.com:~$ db.users.insertMany([
  {
    name: "王五",
    email: "wangwu@example.com"
  },
  {
    name: "赵六",
    email: "zhaoliu@example.com"
  }
])

// 商品集合
root@ssdevops.com:~$ db.products.insertMany([
  {
    name: "笔记本电脑",
    price: 5999,
    stock: 10
  },
  {
    name: "智能手机",
    price: 2999,
    stock: 20
  }
])

// 订单集合(引用用户和商品)注意:以下引用的ObjectId是上边插入用户和商品时生成的id,需要按实际修改
root@ssdevops.com:~$ db.orders.insertOne({
  userId: ObjectId("682fe3e8cb9f47e1bd166701"),
  orderDate: new Date(),
  items: [
    {
      productId: ObjectId("682fe3ffcb9f47e1bd166703"),
      quantity: 1,
      price: 5999
    },
    {
      productId: ObjectId("682fe3ffcb9f47e1bd166704"),
      quantity: 2,
      price: 2999
    }
  ],
  totalAmount: 11997,
  status: "已付款"
})

使用手动连接查询引用关系:

// 查找订单
root@ssdevops.com:~$ var order = db.orders.findOne()

// 查找订单关联的用户
root@ssdevops.com:~$ var user = db.users.findOne({_id: order.userId})
root@ssdevops.com:~$ print("订单用户:", user.name)

// 查找订单包含的商品
root@ssdevops.com:~$ order.items.forEach(function(item) {
  var product = db.products.findOne({_id: item.productId})
  print("商品:", product.name, "数量:", item.quantity)
})

执行结果截图:

image-20250523110242290

实战6:BSON数据转换操作

在MongoDB Shell中进行BSON/JSON转换:

// 创建一个包含各种类型的文档
root@ssdevops.com:~$ var complexDoc = {
  _id: ObjectId(),
  name: "BSON测试",
  date: new Date(),
  binary: BinData(0, "VGhpcyBpcyBhIHRlc3Q="),
  nested: {
    field1: 123,
    field2: "嵌套字段"
  },
  array: [1, 2, 3, {subField: "test"}],
  numberLong: NumberLong("9223372036854775807"),
  numberInt: NumberInt(123)
}
​
// 插入文档
root@ssdevops.com:~$ db.bson_test.insertOne(complexDoc)
​
// 转换为JSON格式
root@ssdevops.com:~$ JSON.stringify(complexDoc)
​
// 转换为松散扩展的JSON格式(包含类型信息)
root@ssdevops.com:~$ printjson(complexDoc)
​
// 转换为严格模式的JSON(会丢失一些BSON特定类型信息)
root@ssdevops.com:~$ var strict = JSON.parse(JSON.stringify(complexDoc))
root@ssdevops.com:~$ printjson(strict)

查看BSON特定类型如何在JSON中表示:

// ObjectId在JSON中的表示
root@ssdevops.com:~$ print("ObjectId在JSON中:", JSON.stringify({id: ObjectId()}))
​
// Date在JSON中的表示
root@ssdevops.com:~$ print("Date在JSON中:", JSON.stringify({date: new Date()}))
​
// NumberLong在JSON中的表示
root@ssdevops.com:~$ print("NumberLong在JSON中:", JSON.stringify({long: NumberLong("9223372036854775807")}))

注意BSON到JSON的转换限制:

root@ssdevops.com:~$ print("BSON转JSON可能会丢失类型信息,特别是对于日期、二进制数据和特殊数字类型")

执行结果截图:

image-20250523111422756

【课后思考】

  1. 在设计MongoDB文档结构时,什么情况下应该选择嵌入式文档,什么情况下应该选择文档引用?考虑数据访问模式、更新频率和数据规模。

  2. MongoDB的BSON格式相比JSON格式有哪些优势和局限性?如何在实际应用中最大化利用BSON的特性?

  3. 对于一个社交媒体应用,如何设计用户、帖子和评论的文档结构?考虑查询效率、数据完整性和可扩展性。

【高频面试题】

面试题1:解释MongoDB中BSON和JSON的区别,以及为什么MongoDB选择BSON作为存储格式?

思路解析: BSON(Binary JSON)和JSON(JavaScript Object Notation)的主要区别在于:

  1. 数据格式:JSON是一种文本格式,易于人类阅读;而BSON是二进制格式,计算机处理更高效。

  2. 数据类型支持:JSON支持的数据类型有限(字符串、数字、布尔值、数组、对象、null);BSON扩展了更多数据类型,如日期、二进制数据、正则表达式等。

  3. 性能表现:BSON在编码和解码速度上优于JSON,特别是对于数字和二进制数据。

  4. 空间效率:对于某些数据(特别是数字),BSON更节省空间;但对于字符串,BSON可能占用更多空间,因为存储了字符串长度。

MongoDB选择BSON作为存储格式的原因:

  • 遍历效率高:BSON设计便于快速扫描

  • 编码/解码速度快:对数据库性能至关重要

  • 类型丰富:支持日期、二进制等数据库常用类型

  • 字段长度前置:使解析更高效

  • 可扩展性:便于添加新的数据类型和功能

在实际应用中,MongoDB在内部使用BSON存储,但对外提供JSON接口,实现了两者的优势结合。用户可以使用易读的JSON格式与数据库交互,而MongoDB内部则利用BSON的高效性能进行存储和处理。这种设计使MongoDB既保持了开发友好性,又获得了高性能的数据处理能力。

面试题2:在MongoDB中,如何处理超过16MB文档大小限制的数据?各种解决方案的优缺点是什么?

思路解析: MongoDB对单个文档的大小限制为16MB,处理超过此限制的数据有以下几种方案:

  1. 文档引用(Document References)

    • 方法:将大文档拆分为多个相关文档,通过ID引用关联

    • 优点:简单直观、维护引用关系清晰

    • 缺点:需要多次查询才能获取完整数据,应用层需要处理连接逻辑

  2. 分桶策略(Bucketing)

    • 方法:按一定规则(如时间段)将相关数据分组到多个文档中

    • 优点:同时获取一组相关数据比较高效

    • 缺点:桶的设计需要仔细考虑以平衡性能和数据组织

  3. 数据压缩

    • 方法:在应用层对数据进行压缩后存储

    • 优点:减少存储空间、可能允许更多数据放入单个文档

    • 缺点:额外的CPU开销、不便于查询和更新部分内容

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

david_代松涛

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值