分类课程智能体AI
文章
订阅
分类课程AI导师
文章
价格
课程进度
5 / 17
上一节用 CRUD 推进纸舟书店业务下一节原子更新、批量写入与并发语义
自在学

© 2025 - 2026 株洲市自在学教育科技有限公司 版权所有

公网安备湘公网安备43020302000292号 | 湘ICP备2025148919号-1

关于我们隐私政策使用条款

© 2025 - 2026 株洲市自在学教育科技有限公司 版权所有

公网安备湘公网安备43020302000292号湘ICP备2025148919号-1

编程MongoDB 纸舟书店完整课程查询、投影、排序与稳定分页

查询、投影、排序与稳定分页

上一节让纸舟书店的状态发生了变化:book-draft 已删除,book-mongodb 的库存是 19,并新增了 featured: true。这一节会从这个状态出发,把查询拆成四个独立问题:哪些文档能进入结果、返回哪些字段、按什么顺序排列、一次取多少条。

MongoDB 查询过滤器本身也是文档。等值、范围、逻辑、数组和嵌套条件可以组合,但组合得越复杂,越需要先用小结果验证语义,再讨论索引与性能。


确认查询起点

知识点

这一节的固定结果依赖上一节的更新与删除。先核对关键文档,可以快速判断是否需要回到种子导入和 CRUD 步骤。

实操

如果需要,重新进入 shell 并切换数据库:

shell
docker exec -it paperboat-mongo mongosh --quiet
javascript
use bookstore
 
const mongoBook = db.books.findOne(
  { _id: "book-mongodb" },
  { _id: 1, stock: 1, featured: 1 }
);
 
print(EJSON.stringify({
  bookCount: db.books.countDocuments({}),
  mongoBook
}));

结果展示

json
{"bookCount":4,"mongoBook":{"_id":"book-mongodb","stock":19,"featured":true}}

如果书籍总数不是 4,或库存仍是 20,说明前一节没有完整执行。先修正起点,再继续下面的组合查询。


组合范围条件与集合条件

知识点

同一字段的多个比较条件应写在一个对象里。下面的价格条件表示闭区间 60 到 90:

javascript
{ price: { $gte: 60, $lte: 90 } }

$in 表示字段值属于给定集合。把 category 与 price 放在同一个过滤器中,默认是逻辑 AND:两部分都满足的文档才能进入结果。

实操

查找价格在 60 到 90 元之间、类别属于“数据库”的书,并按价格升序返回:

javascript
const databaseBooks = db.books
  .find(
    {
      price: { $gte: 60, $lte: 90 },
      category: { $in: ["数据库"] }
    },
    { _id: 0, title: 1, price: 1 }
  )
  .sort({ price: 1, _id: 1 })
  .toArray();
 
print(EJSON.stringify(databaseBooks));

结果展示

json
[{"title":"数据建模的艺术","price":79},{"title":"MongoDB 从入门到实践","price":89}]

Node.js 项目开发 的价格也在区间内,但类别是“编程”,因此没有进入结果。现代 Web 基础 同时不满足类别和最低价格。


使用逻辑 OR 表达候选规则

知识点

$or 接收一个过滤器数组,只要其中一个分支成立,文档就会匹配。纸舟书店准备一个运营列表:库存少于 10 本,或者被标记为精选的书,都进入候选列表。

字段不存在时不会自动等于 false。book-design 没有 featured 字段,但它凭 stock < 10 命中;book-mongodb 凭 featured: true 命中。

实操

javascript
const operationCandidates = db.books
  .find(
    {
      $or: [
        { stock: { $lt: 10 } },
        { featured: true }
      ]
    },
    { _id: 1, title: 1, stock: 1, featured: 1 }
  )
  .sort({ _id: 1 })
  .toArray();
 
print(EJSON.stringify(operationCandidates));

结果展示

json
[{"_id":"book-design","title":"数据建模的艺术","stock":8},{"_id":"book-mongodb","title":"MongoDB 从入门到实践","stock":19,"featured":true}]

结果清楚展示了两个不同命中原因。投影不会为不存在的 featured 补值,因此第一本文档中没有这个字段。

当 $or 分支越来越多时,先问它是否真的表达了同一个业务集合。把互不相关的条件堆进一个查询,通常比拆成两个清楚的操作更难维护,也更难设计索引。


查询数组字段

知识点

对数组字段使用一个标量条件时,只要数组中至少一个元素相等就能命中。例如 { tags: "后端" } 会匹配包含“后端”的标签数组。

$all 更严格:数组必须包含列出的每个值,但不要求顺序相同,也允许还有其他元素。它适合“同时拥有这些标签”的条件。

实操

查找同时拥有 “MongoDB” 和 “后端” 标签的书:

javascript
const taggedBooks = db.books
  .find(
    { tags: { $all: ["MongoDB", "后端"] } },
    { _id: 1, title: 1, tags: 1 }
  )
  .toArray();
 
print(EJSON.stringify(taggedBooks));

结果展示

json
[{"_id":"book-mongodb","title":"MongoDB 从入门到实践","tags":["MongoDB","后端"]}]

只有一本书同时包含两个标签。book-node 虽然包含“后端”,但没有 “MongoDB”,因此不会命中 $all。


查询嵌套字段

知识点

点号路径可以进入嵌套文档和文档数组。addresses.city 表示检查 addresses 数组里每个元素的 city。MongoDB 会在任意元素命中时返回整个顾客文档。

实操

javascript
const hangzhouCustomers = db.customers
  .find(
    { "addresses.city": "杭州" },
    { _id: 1, name: 1, "addresses.city": 1 }
  )
  .toArray();
 
print(EJSON.stringify(hangzhouCustomers));

结果展示

json
[{"_id":"customer-zhou","name":"周雨","addresses":[{"city":"杭州"}]}]

顾客文档被命中,投影只保留地址中的 city 字段。点号路径没有改变数据库中的原文档,只影响这次查询的匹配与返回形状。


用 elemMatch 约束同一个数组元素

知识点

订单的 items 是文档数组。如果我们要求“同一个明细既是 MongoDB 书,数量又至少为 2”,应使用 $elemMatch:

javascript
{
  items: {
    $elemMatch: {
      bookId: "book-mongodb",
      quantity: { $gte: 2 }
    }
  }
}

如果只写 items.bookId 和 items.quantity 两个独立条件,在包含多个明细的订单中,它们可能分别由不同数组元素满足,从而产生并非业务本意的匹配。

实操

javascript
const bulkMongoOrders = db.orders
  .find(
    {
      items: {
        $elemMatch: {
          bookId: "book-mongodb",
          quantity: { $gte: 2 }
        }
      }
    },
    { _id: 1, customerId: 1, status: 1, items: 1 }
  )
  .toArray();
 
print(EJSON.stringify(bulkMongoOrders));

结果展示

json
[{"_id":"order-1003","customerId":"customer-lin","status":"shipped","items":[{"bookId":"book-mongodb","title":"MongoDB 从入门到实践","unitPrice":89,"quantity":2}]}]

只有 order-1003 的同一条明细同时满足书籍标识与数量条件。


用投影控制返回形状

知识点

投影的主要价值是减少无关数据传输,并把调用方真正依赖的字段写清楚。包含式投影使用 1,排除式投影使用 0;除 _id 外,不应在同一个普通投影里混用包含与排除。

数组还可以用 $slice 截取。下面只查看订单的第一条明细,不修改原数组。

实操

javascript
const orderPreview = db.orders.findOne(
  { _id: "order-1001" },
  {
    _id: 1,
    status: 1,
    total: 1,
    items: { $slice: 1 }
  }
);
 
print(EJSON.stringify(orderPreview));

结果展示

json
{"_id":"order-1001","status":"paid","items":[{"bookId":"book-mongodb","title":"MongoDB 从入门到实践","unitPrice":89,"quantity":1}],"total":158}

数据库中的订单仍有两条明细;这次结果只返回第一条。投影是读取视图,不是更新操作。


建立可重复的排序

知识点

分页首先是排序问题。只按 price 排序时,同价书籍之间没有固定先后;数据增长后,它们可能跨页移动或重复出现。把唯一 _id 作为第二排序键,就能为每条文档建立稳定位置:

javascript
{ price: 1, _id: 1 }

这表示价格升序,同价时 _id 升序。后面的游标条件必须与这个顺序保持一致。

实操

先查看完整有序列表:

javascript
const orderedBooks = db.books
  .find(
    { published: true },
    { _id: 1, title: 1, price: 1 }
  )
  .sort({ price: 1, _id: 1 })
  .toArray();
 
print(EJSON.stringify(orderedBooks));

结果展示

json
[{"_id":"book-web","title":"现代 Web 基础","price":59},{"_id":"book-node","title":"Node.js 项目开发","price":69},{"_id":"book-design","title":"数据建模的艺术","price":79},{"_id":"book-mongodb","title":"MongoDB 从入门到实践","price":89}]

这条序列是后面两种分页方式的共同基准。


使用 skip 完成小数据分页

知识点

skip(n).limit(m) 容易理解:跳过前 n 条,再取 m 条。它适合后台列表和偏移量较小的页面;偏移量很大时,数据库仍需走过前面的结果,成本会随偏移增长。

我们把每页大小设为 2,第二页需要跳过前 2 条。

实操

javascript
const secondPageByOffset = db.books
  .find(
    { published: true },
    { _id: 1, title: 1, price: 1 }
  )
  .sort({ price: 1, _id: 1 })
  .skip(2)
  .limit(2)
  .toArray();
 
print(EJSON.stringify(secondPageByOffset));

结果展示

json
[{"_id":"book-design","title":"数据建模的艺术","price":79},{"_id":"book-mongodb","title":"MongoDB 从入门到实践","price":89}]

结果与完整序列的第三、第四条一致。这个方法返回正确,但还不是大偏移列表的理想方案。


使用复合游标读取下一页

知识点

游标式分页不说“跳过多少条”,而是说“从上一页最后一个排序键之后继续”。第一页最后一本是:

text
price = 69, _id = book-node

对于 { price: 1, _id: 1 } 的升序,下一页条件是:价格大于 69,或者价格仍等于 69 但 _id 大于 book-node。第二个分支处理同价数据,不能省略。

实操

javascript
const cursor = {
  price: 69,
  id: "book-node"
};
 
const secondPageByCursor = db.books
  .find(
    {
      published: true,
      $or: [
        { price: { $gt: cursor.price } },
        {
          price: cursor.price,
          _id: { $gt: cursor.id }
        }
      ]
    },
    { _id: 1, title: 1, price: 1 }
  )
  .sort({ price: 1, _id: 1 })
  .limit(2)
  .toArray();
 
print(EJSON.stringify(secondPageByCursor));

结果展示

json
[{"_id":"book-design","title":"数据建模的艺术","price":79},{"_id":"book-mongodb","title":"MongoDB 从入门到实践","price":89}]

它与 skip(2) 得到同一页,但查询表达的是一个范围,而不是越来越大的偏移。数据量增长后,还要为 { published: 1, price: 1, _id: 1 } 这一访问模式设计索引;索引部分会用 explain 验证,而不是仅凭字段顺序猜测。

游标值必须来自上一页最后一条真实结果,并同时携带所有排序键。只传 price: 69 会漏掉或重复同价书籍;修改排序方向时,比较符号也要随之调整。


组合一个业务查询

知识点

纸舟书店想找出“已发布、库存至少 10、本身带有后端标签”的书,并只返回标题与库存。先自己写过滤器,再展开答案。

三个不同字段放在同一个过滤器中时默认是逻辑 AND;对数组字段使用标量 "后端",表示数组中至少包含这个值。结果按库存降序,同库存时用 _id 固定顺序。

实操

先在自己的 shell 中完成查询。需要核对时展开参考命令:

javascript
const backendInStock = db.books
  .find(
    {
      published: true,
      stock: { $gte: 10 },
      tags: "后端"
    },
    { _id: 0, title: 1, stock: 1 }
  )
  .sort({ stock: -1, _id: 1 })
  .toArray();
 
print(EJSON.stringify(backendInStock));

结果展示

执行结果:

json
[{"title":"MongoDB 从入门到实践","stock":19},{"title":"Node.js 项目开发","stock":15}]

两本书都已发布、库存不少于 10,并且标签数组包含“后端”。book-design 与 book-web 的标签不匹配,因此没有进入结果。


检查你的理解

1
列表按 `{ price: 1 }` 分页时,为什么通常还要加入唯一 `_id` 作为第二排序键?

这一节已经覆盖纸舟书店的核心读取路径:范围与类别组合、逻辑 OR、数组 $all、嵌套点号路径、同元素 $elemMatch、投影以及两种分页。下一节会把重点转向写入并发:怎样用原子操作避免库存丢失,怎样安全批量写入和删除。

  • 确认查询起点
    • 知识点
    • 实操
    • 结果展示
  • 组合范围条件与集合条件
    • 知识点
    • 实操
    • 结果展示
  • 使用逻辑 OR 表达候选规则
    • 知识点
    • 实操
    • 结果展示
  • 查询数组字段
    • 知识点
    • 实操
    • 结果展示
  • 查询嵌套字段
    • 知识点
    • 实操
    • 结果展示
  • 用 elemMatch 约束同一个数组元素
    • 知识点
    • 实操
    • 结果展示
  • 用投影控制返回形状
    • 知识点
    • 实操
    • 结果展示
  • 建立可重复的排序
    • 知识点
    • 实操
    • 结果展示
  • 使用 skip 完成小数据分页
    • 知识点
    • 实操
    • 结果展示
  • 使用复合游标读取下一页
    • 知识点
    • 实操
    • 结果展示
  • 组合一个业务查询
    • 知识点
    • 实操
    • 结果展示
  • 检查你的理解

目录

  • 确认查询起点
    • 知识点
    • 实操
    • 结果展示
  • 组合范围条件与集合条件
    • 知识点
    • 实操
    • 结果展示
  • 使用逻辑 OR 表达候选规则
    • 知识点
    • 实操
    • 结果展示
  • 查询数组字段
    • 知识点
    • 实操
    • 结果展示
  • 查询嵌套字段
    • 知识点
    • 实操
    • 结果展示
  • 用 elemMatch 约束同一个数组元素
    • 知识点
    • 实操
    • 结果展示
  • 用投影控制返回形状
    • 知识点
    • 实操
    • 结果展示
  • 建立可重复的排序
    • 知识点
    • 实操
    • 结果展示
  • 使用 skip 完成小数据分页
    • 知识点
    • 实操
    • 结果展示
  • 使用复合游标读取下一页
    • 知识点
    • 实操
    • 结果展示
  • 组合一个业务查询
    • 知识点
    • 实操
    • 结果展示
  • 检查你的理解