查询与投影
过滤、排序、limit、projection。
查询与投影
这一课你将能做到
- 编写比较、逻辑、数组、嵌套字段的
find 过滤条件,说出 $in / $all / $elemMatch 的语义差别
- 用 projection 只返回需要的字段,正确处理
_id 与数组裁剪
- 组合
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)
| MQL | SQL 近似 | 备注 |
|---|
{ 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 万条),深分页改用「上一页最后一条的锚点值」做范围查询。
讲清楚要点
- 嵌套字段用点号字符串路径:
"meta.weeks" 是路径不是变量;整体对象相等({ meta: { weeks: 2 } })要求字段完全一致,几乎不是你想要的。
- 数组等值匹配是「含有」语义:
{ tags: "java" } 匹配任何包含该元素的数组;要整体相等才写完整数组字面量。
_id 默认总是返回:包含式投影里不写 _id: 0 就一定带着它。
sort 放在 skip/limit 之前链式书写:语义是「先排序再跳再截」;排序字段的选择直接影响能否用索引(下一课)。
- 深分页是性能陷阱:
skip 成本随页码线性增长;用锚点(如 at < 上一页最小时间)翻页。
- 投影省流量,也省内存:列表接口绝不查
contentMd 这类大字段,详情接口再取。
- 无索引的过滤形状会拖垮大集合:前导通配正则、函数包裹字段都是索引杀手——本课先记现象,下一课用
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 决胜 |
卡点急救
- 现象:明明有这条数据却查不到
多半是类型不一致——字符串
"2" 与数字 2 不相等,日期与日期字符串不相等。用 findOne 取出样本核对类型再写条件。
- 现象:投影报错
Cannot do inclusion on field ... exclusion
include/exclude 混用了。重写为纯包含式(加 _id: 0)或纯排除式。
- 现象:分页第二页出现了第一页的文档
排序键不唯一。
.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 区分。这是隐式约定题,考你对类型系统的理解。
自测三问
- 查询
tags 同时包含 java 与 spring 的写法是什么?和 { tags: "java" } 差在哪?
- 只要
slug、title 且不要 _id,投影怎么写?
.sort({ hot: -1 }).skip(10).limit(5) 对应怎样的 SQL?
小结一句话
filter + projection + sort/skip/limit 构成完整查询:嵌套走点号、数组看语义、投影防混用、分页避深跳。
延伸