CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
MongoDB 入门/课时/查询与投影
RawBlame
课时目录12 节

MongoDB 入门

  1. 01MongoDB 是什么
  2. 02安装与 mongosh
  3. 03文档与 BSON
  4. 04CRUD 操作
  5. 05查询与投影
  6. 06索引
  7. 07聚合管道
  8. 08Spring Data MongoDB
  9. 09何时用 Mongo vs SQL
  10. 10Docker 运行 MongoDB
  11. 11Node.js 驱动与 Mongoose
  12. 12综合练习

查询与投影

过滤、排序、limit、projection。

35 分钟

查询与投影

这一课你将能做到

  1. 编写比较、逻辑、数组、嵌套字段的 find 过滤条件,说出 $in / $all / $elemMatch 的语义差别
  2. 用 projection 只返回需要的字段,正确处理 _id 与数组裁剪
  3. 组合 sort / skip / limit 实现分页,对照 SQL 的 ORDER BY + LIMIT/OFFSET,并知道深分页的隐患

为什么要学

真实接口从不「全表捞回再在内存里过滤」:filter 决定匹配谁,projection 决定网络上传输什么,sort/skip/limit 决定页面怎么翻。三个都写准了,下一课的索引优化才有意义——索引是给「查询形状」服务的。

本课场景依然是做题日志与课程集合:列表页只显示标题不显示正文、按标签筛选课程、按时间倒序翻页——每一条都对应本课的一个知识点。

人话:find 第一个参数像 WHERE,第二个参数像 SELECT 的列清单,后面链式的 .sort().skip().limit() 像 ORDER BY + LIMIT/OFFSET。

先建立图像

db.collection.find(
  { /* filter:匹配谁 */ },
  { /* projection:返回哪些字段 */ }
)
  .sort({ 字段: 1 或 -1 })   // 1 升序,-1 降序
  .skip(n)
  .limit(m)
MQLSQL 近似备注
{ level: "beginner" }WHERE level = 'beginner'隐式 AND
{ weeks: { $gte: 2 } }WHERE weeks >= 2比较算子
{ tags: "java" }数组「含」该元素数组语义
{ tags: { $all: [...] } }同时包含多个元素
projection { title: 1 }SELECT title_id 默认带走
.sort().skip().limit()ORDER BY … LIMIT … OFFSET …

跟着做

步骤 1:准备示例数据

use coderoadmap_lab
db.tracks.deleteMany({})   // 仅实验库

db.tracks.insertMany([
  {
    slug: "sql-essentials", title: "SQL 入门",
    level: "beginner", tags: ["sql", "mysql"],
    meta: { weeks: 2 }, hot: 8.5,
    contentMd: "# 超长正文……"
  },
  {
    slug: "spring-boot-essentials", title: "Spring Boot 入门",
    level: "intermediate", tags: ["java", "spring"],
    meta: { weeks: 3 }, hot: 9.2,
    contentMd: "# 超长正文……"
  },
  {
    slug: "mongodb-essentials", title: "MongoDB 入门",
    level: "beginner", tags: ["mongodb", "nosql", "java"],
    meta: { weeks: 2 }, hot: 9.0,
    contentMd: "# 超长正文……"
  }
])

步骤 2:比较与嵌套字段

// 等值 + 隐式 AND
db.tracks.find({ level: "beginner", "meta.weeks": 2 })

// 嵌套字段必须用点号路径(字符串键)
db.tracks.find({ "meta.weeks": { $gte: 2 } })

// $in:字段值在给定列表内
db.tracks.find({ level: { $in: ["beginner", "intermediate"] } })

// 数值范围
db.tracks.find({ hot: { $gte: 9, $lt: 9.2 } })

注意 { meta: { weeks: 2 } } 写法是「meta 整个对象精确相等」,不是嵌套查询——嵌套一律走点号。

步骤 3:数组查询的三种强度

// ① 含有:数组任一元素等于 "java"
db.tracks.find({ tags: "java" })

// ② $all:同时包含列表中所有元素
db.tracks.find({ tags: { $all: ["java", "spring"] } })

// ③ $size:数组长度精确匹配
db.tracks.find({ tags: { $size: 2 } })

对象数组要按「每元素多条件」筛选时用 $elemMatch:

// 找「该元素同时满足 order>=1 且 slug 以 mongo 开头」的文档
db.tracks.find({
  lessons: { $elemMatch: { order: { $gte: 1 }, slug: /^mongo/ } }
})

步骤 4:逻辑算子与正则

// $or 显式或
db.tracks.find({
  $or: [{ level: "beginner" }, { hot: { $gte: 9 } }]
})

// 前缀正则(可用上索引)
db.tracks.find({ slug: /^mongodb/ })

// 非锚定正则(大集合上易全表扫,慎用)
db.tracks.find({ title: { $regex: "入门" } })

步骤 5:投影

// 包含式:只要 title/slug;_id 必须显式排除
db.tracks.find(
  { level: "beginner" },
  { title: 1, slug: 1, _id: 0 }
)

// 排除式:列表接口甩掉大字段正文
db.tracks.find({}, { contentMd: 0 })

// $slice:只取数组前 2 个元素
db.tracks.find(
  { slug: "mongodb-essentials" },
  { title: 1, tags: { $slice: 2 }, _id: 0 }
)

应看到:包含式与排除式不能混用(_id 除外)——要么列出要的,要么列出不要的。

步骤 6:排序与分页

// 第 2 页,每页 2 条
db.tracks.find()
  .sort({ hot: -1, title: 1 })   // 先按热度降序,同热度按标题
  .skip(2)
  .limit(2)

对照 SQL:ORDER BY hot DESC, title ASC LIMIT 2 OFFSET 2。 笔记写一句:skip 越大扫描浪费越多(skip 100000 也要先扫过 10 万条),深分页改用「上一页最后一条的锚点值」做范围查询。

讲清楚要点

  1. 嵌套字段用点号字符串路径:"meta.weeks" 是路径不是变量;整体对象相等({ meta: { weeks: 2 } })要求字段完全一致,几乎不是你想要的。
  2. 数组等值匹配是「含有」语义:{ tags: "java" } 匹配任何包含该元素的数组;要整体相等才写完整数组字面量。
  3. _id 默认总是返回:包含式投影里不写 _id: 0 就一定带着它。
  4. sort 放在 skip/limit 之前链式书写:语义是「先排序再跳再截」;排序字段的选择直接影响能否用索引(下一课)。
  5. 深分页是性能陷阱:skip 成本随页码线性增长;用锚点(如 at < 上一页最小时间)翻页。
  6. 投影省流量,也省内存:列表接口绝不查 contentMd 这类大字段,详情接口再取。
  7. 无索引的过滤形状会拖垮大集合:前导通配正则、函数包裹字段都是索引杀手——本课先记现象,下一课用 explain 验证。

你可能会懵的地方

  • 投影能同时 include 和 exclude 吗? 不能(_id 是唯一例外)。要么全列「要的」,要么全列「不要的」,MongoDB 会直接报错提示混用。

  • $elemMatch 和点号查数组元素有什么区别? 点号 lessons.slug 是「任一元素的 slug 满足」;当条件涉及同一个元素的多个字段时必须 $elemMatch,否则各字段可能匹配到不同元素。

  • find 结果刷屏怎么办? 加 .limit(5) 或更严的 filter;mongosh 迭代游标用 it 翻页,DBQuery.shellBatchSize 可调每批条数。

  • null 与字段缺失怎么查? { field: null } 会同时匹配「值为 null」与「字段不存在」两种文档;要区分需配合 $exists。

  • 分页时排序不稳定导致重复/漏数据? 排序键不唯一时同值顺序不保证。工程做法:排序键后追加 _id 作为决胜键,保证全序。

错法对照

错法问题正解
{ meta: { weeks: 2 } } 查嵌套变成对象整体相等匹配"meta.weeks": 2
以为投影默认去掉 _id结果永远带着 _id显式 _id: 0
深分页巨大 skip越翻越慢锚点范围查询翻页
生产接口无条件 find()拖垮库与网络必带过滤 + limit
前导通配正则 /$关键词/无法用索引,全表扫前缀正则或全文检索方案
排序键不唯一就分页页间数据重复或丢失排序键追加 _id 决胜

卡点急救

  1. 现象:明明有这条数据却查不到 多半是类型不一致——字符串 "2" 与数字 2 不相等,日期与日期字符串不相等。用 findOne 取出样本核对类型再写条件。
  2. 现象:投影报错 Cannot do inclusion on field ... exclusion include/exclude 混用了。重写为纯包含式(加 _id: 0)或纯排除式。
  3. 现象:分页第二页出现了第一页的文档 排序键不唯一。.sort({ hot: -1, _id: 1 }) 加决胜键即可稳定全序。

面试常问

  • $elemMatch 什么时候必须用? 当筛选条件要求「数组中同一个元素同时满足多个条件」时。点号路径只保证每个条件各自在数组中「存在某元素满足」,不保证是同一元素。举例子答:找「order≥2 且 slug 匹配」的同一课时,必须 $elemMatch。

  • MongoDB 深分页为什么慢?怎么解决? skip(n) 要定位并丢弃前 n 条,成本随 n 增长。方案:① 基于有序锚点的范围查询(where at < lastAt + limit);② 业务上限制最大页深;③ 大偏移场景改游标式(scroll)接口。答出「skip 成本线性增长 + 锚点翻页」即为合格。

  • 查询字段为 null 会匹配到什么? 值为 null 的文档和字段不存在的文档都会命中;只想要显式 null 需加 { field: { $type: 10 } } 或用 $exists 区分。这是隐式约定题,考你对类型系统的理解。

自测三问

  1. 查询 tags 同时包含 java 与 spring 的写法是什么?和 { tags: "java" } 差在哪?
  2. 只要 slug、title 且不要 _id,投影怎么写?
  3. .sort({ hot: -1 }).skip(10).limit(5) 对应怎样的 SQL?

小结一句话

filter + projection + sort/skip/limit 构成完整查询:嵌套走点号、数组看语义、投影防混用、分页避深跳。

延伸

  • Query Documents 官方教程
  • Project Fields 投影文档
  • 下一课:索引——让这些查询真正快起来。
加载本节练习…
← 上一课CRUD 操作下一课 →索引