在MongoDB的日常使用中,我们经常遇到这样的需求:文档里有一个数组字段,希望在不破坏文档结构的前提下把数组元素按某个规则排好序。比如订单文档里的商品明细要按金额从高到低排列,评论列表要按时间倒序展示。早期版本的MongoDB只能通过 $unwind 拆开数组再排序再 $group 重组,不仅聚合语句冗长,文档结构还容易发生变化。从5.2版本开始,MongoDB在聚合管道中引入了 $sortArray 操作符,让数组排序变成了一行表达式就能搞定的事情。本文将详细介绍这个操作符的语法、用法和实战中的注意事项。

$sortArray 的基本语法与参数说明
$sortArray 是一个聚合表达式操作符,必须配合 $addFields、$project 或 $set 等阶段使用,它不能单独作为一个管道阶段。它的基本结构分为两种形式。第一种是简单排序,只指定输入数组和排序方向:
// 简单形式:对整个数组做升序或降序排序
{ $sortArray: { input: <数组表达式>, sortBy: 1 } } // 升序
{ $sortArray: { input: <数组表达式>, sortBy: -1 } } // 降序
第二种是按字段排序,当数组元素是文档时,sortBy 可以指定一个或多个字段,写法和普通 find 查询里的 sort 参数几乎一致:
// 按 score 字段降序,score 相同时再按 name 升序
{
$addFields: {
students: {
$sortArray: {
input: "$students",
sortBy: { score: -1, name: 1 }
}
}
}
}
两个核心参数需要理解清楚。input 是待排序的数组,可以是字段引用、字面量数组,也可以是其他表达式计算出来的结果,如果它解析为 null 或 missing,$sortArray 会返回 null。sortBy 定义排序规则,值为 1 表示升序、-1 表示降序。需要注意的一点是,sortBy 不支持 { field: { $meta: "textScore" } } 这类元数据排序,它只接受纯粹的字段加方向。
常见使用场景与代码示例
场景一:对基本类型数组排序
最简单的用法是对数字或字符串数组直接排序。假设有一个商品集合,每个文档记录了历史价格,我们希望在查询结果中把价格从低到高排列:
db.products.aggregate([
{ $match: { _id: 1001 } },
{
$addFields: {
priceHistory: {
$sortArray: { input: "$priceHistory", sortBy: 1 }
}
}
}
])
如果 priceHistory 原本是 [89, 45, 120, 60],执行后输出就是 [45, 60, 89, 120]。字符串数组同理,会按照字典序排序。这种场景下用 JavaScript 的思路理解就是数组的 sort 方法,只是它发生在数据库端,不占用应用层内存。
场景二:对嵌套文档数组按多字段排序
实际业务里更常见的是文档数组。比如订单文档包含 items 明细数组,每个元素有商品名和金额,现在要求明细按金额降序排列,金额相同的按商品名升序:
db.orders.aggregate([
{ $match: { orderId: "A2024" } },
{
$addFields: {
items: {
$sortArray: {
input: "$items",
sortBy: { amount: -1, name: 1 }
}
}
}
},
{
$project: {
topItem: { $arrayElemAt: ["$items", 0] },
items: 1
}
}
])
这里顺便展示了排序后与其他数组操作符的组合用法:用 $arrayElemAt 取出排序后的第一个元素,就能轻松拿到金额最高的商品。如果不用 $sortArray,这个需求需要 unwind 加 sort 再 group 三步走,而 $group 重组数组时还必须小心保留其他字段,出错概率明显更高。
场景三:与 $map、$filter 配合处理动态数据
$sortArray 的 input 接受任意表达式,这意味着它可以先对数组做加工再排序。例如先过滤掉已删除的评论,再按点赞数排序:
db.posts.aggregate([
{
$addFields: {
visibleComments: {
$sortArray: {
input: {
$filter: {
input: "$comments",
cond: { $eq: ["$$this.deleted", false] }
}
},
sortBy: { likes: -1 }
}
}
}
}
])
这种链式组合充分发挥了聚合表达式的优势,整个处理逻辑都在一次聚合中完成,避免了把数据拉到应用层再加工的网络开销。同理,$sortArray 的输出也可以继续作为 $reduce、$slice 等操作符的输入,实现取前 N 名这类常见需求。
排序规则细节与混合类型的坑
使用 $sortArray 时最容易踩坑的地方是类型不一致。MongoDB 内部有一套 BSON 类型排序顺序(也叫比较顺序),不同类型之间的大小关系是固定的,例如 null 小于数字、数字小于字符串、字符串小于对象。如果数组里混装了数字和字符串,$sortArray 不会报错,而是按 BSON 顺序排,结果可能和直觉不符。
更麻烦的是 BSON 类型排序顺序与排序规则(collation)之间存在冲突。如果集合或聚合指定了 collation,$sortArray 会直接抛出错误,提示排序操作符不支持排序规则。解决办法有两种:一是排序前用 $map 把字段统一转换成同一类型,比如全部转成字符串;二是在聚合时不设置 collation,改在应用层做本地化排序。
还有一个细节值得注意:如果数组中的文档在排序字段上存在缺失值,这些文档会被排在结果的前面还是后面,取决于排序方向。缺失字段按 null 处理,升序时 null 排最前,降序时排最后。写业务代码时如果对顺序敏感,最好在写入层面保证字段完整,或者在排序前用 $ifNull 给缺失字段补默认值:
{
$sortArray: {
input: "$records",
sortBy: { score: -1 },
// 排序前无法直接改写字段,需配合 $map 预处理
}
},
// 实际写法:
{
$addFields: {
records: {
$sortArray: {
input: {
$map: {
input: "$records",
in: { $mergeObjects: [ { score: 0 }, "$$this" ] }
}
},
sortBy: { score: -1 }
}
}
}
}
性能考量与替代方案对比
从执行效率角度看,$sortArray 是在表达式层面完成排序的,它无法利用索引。这一点要和管道阶段的 $sort 区分开:阶段级的 $sort 如果命中索引可以提前完成,而 $sortArray 只能在内存中处理。因此对于超大数组(例如几万个元素),要评估排序带来的内存消耗,避免单文档超过16MB的限制。
和传统方案对比一下就很清楚了。传统写法是 $unwind 拆数组、$sort 排序、$group 用 $push 重组,三个阶段各有一次文档流转,而 $sortArray 方案只需一个 $addFields 阶段,管道更短,中间结果更少。实测在数组元素上千的规模下,$sortArray 的耗时通常只有 unwind 方案的一半左右,且不会因为 group 重组时丢失文档的其他字段。
当然,如果排序结果需要在多个查询中反复使用,更好的做法是在写入时(例如应用层或变更流触发)就维护好数组顺序,读取时直接使用,把排序成本从读路径挪到写路径。对于读多写少的场景,这种空间换时间的策略往往更划算。总结来说,$sortArray 适合排数据量中等、查询时动态决定排序规则的场景,配合 $addFields 和其他数组表达式,可以让聚合语句既简洁又高效。
MongoDB聚合管道$sortArray数组排序修改时间:2026-09-16 10:39:43