sort()的作用位置:游标上的排序指令
find()返回的是一个游标对象,sort()是这个游标上的方法,而不是或数据库层面的操作。这个调用顺序很关键:先find()拿到游标,再.sort()挂上排序规则,之后用toArray()或forEach()消费结果。排序规则本身是一个普通对象,字段名对应文档中的键,值用1或-1表示方向。
{ name: 1 } // 按 name 升序
{ name: -1 } // 按 name 降序
这里的1和-1不是布尔值,也不是随便的正负数,而是MongoDB约定的排序方向标记。写{name:2}或{name:true}都不会按预期工作,驱动可能直接报错或产生未定义行为。
升序排序示例
下面这段代码按name字段升序排列员工记录。文件名采用带编号的命名方式,方便在教程中按顺序管理。
const MongoClient = require('mongodb').MongoClient;
const url = 'mongodb://localhost:27017/MongoDatabase';
MongoClient.connect(url, function (err, db) {
if (err) throw err;
const mysort = { name: 1 };
db.collection('employees')
.find()
.sort(mysort)
.toArray(function (err, result) {
if (err) throw err;
console.log(result);
db.close();
});
});
运行:
node code01_sort_asc.js
find()没有传查询条件,表示取中全部文档,sort({name:1})再对这批结果按姓名升序排列。控制台输出的数组顺序即为排序后的结果。
降序排序示例
把排序对象的值改为-1,方向即变为降序:
const mysort = { name: -1 };
完整示例文件命名为code02_sort_desc.js:
const MongoClient = require('mongodb').MongoClient;
const url = 'mongodb://localhost:27017/MongoDatabase';
MongoClient.connect(url, function (err, db) {
if (err) throw err;
const mysort = { name: -1 };
db.collection('employees')
.find()
.sort(mysort)
.toArray(function (err, result) {
if (err) throw err;
console.log(result);
db.close();
});
});
运行:
node code02_sort_desc.js
升序与降序的差别只在1和-1,但业务含义可能不同。例如按入职时间降序,通常是为了让新入职的员工排在前面;按薪资升序,则可能是为了做薪酬分布分析。方向选错,结果虽然合法,却和需求相反。
排序与过滤条件的配合
实际项目里,排序很少单独出现,通常和查询条件一起使用。下面这个例子先过滤出地址以L开头的员工,再按姓名升序排列:
db.collection('employees')
.find({ address: /^L/ })
.sort({ name: 1 })
.toArray(function (err, result) {
if (err) throw err;
console.log(result);
db.close();
});
这里有一个容易混淆的点:find()的查询对象和sort()的排序对象是两个独立参数,写在同一层级会出错。正确的链式调用是find(查询).sort(排序),顺序不能颠倒。先排序再过滤在语义上也不成立,驱动不会接受这种写法。
多字段排序
排序对象可以包含多个字段,MongoDB会按书写顺序依次比较。例如先按部门升序,部门相同的再按姓名降序:
const mysort = { department: 1, name: -1 };
这种组合排序在生成报表或分组列表时很常见。需要留意的是,字段的书写顺序决定优先级,{department:1,name:-1}与{name:-1,department:1}的结果不同。前者先比部门,后者先比姓名。
多字段排序对索引的要求更严格。如果排序字段与索引字段的前缀顺序不一致,MongoDB可能无法利用索引完成排序,只能把结果读入内存再排。数据量较大时,这会触发内存排序,甚至因为超出排序内存上限而报错。
索引与排序性能
排序本身是一个相对昂贵的操作。如果排序字段上没有合适的索引,MongoDB需要在内存中对结果集排序。默认情况下,内存排序有100MB的限制,超过后会直接返回错误,而不是自动落盘。这个限制在开发环境不容易碰到,一旦数据量上去就会暴露。
为排序字段建立索引时,索引的方向也有讲究。单字段索引可以双向扫描,所以{name:1}的索引既能支持升序也能支持降序。但复合索引的方向组合必须与排序方向匹配,或者相反。例如索引{department:1,name:-1}可以支持{department:1,name:-1}和{department:-1,name:1}两种排序,但无法支持{department:1,name:1}。
在项目里,我通常会把排序字段和过滤字段放在同一个复合索引中,且过滤字段在前、排序字段在后。这样查询可以先通过索引缩小范围,再按索引顺序直接返回,避免额外的排序阶段。用explain()查看执行计划时,如果出现SORT阶段,就说明排序没有走索引,值得进一步优化。
分页场景中的排序
分页是排序最常见的搭档。skip()和limit()配合sort()使用时,排序必须在分页之前确定,否则每页的顺序可能不一致。
db.collection('employees')
.find()
.sort({ joinDate: -1 })
.skip(20)
.limit(10)
.toArray(function (err, result) {
if (err) throw err;
console.log(result);
db.close();
});
这段代码取按入职时间降序排列后的第21到30条。skip()的数值越大,数据库需要跳过的文档越多,性能越差。深分页场景下,更推荐用"上一页之后一条记录的排序字段值"作为游标来翻页,也就是常说的游标分页,而不是单纯加大skip()。
现在驱动中的写法
旧版回调写法里,db.close()放在toArray回调内部,确保读取完成后才断开连接。这种写法在单次脚本中没问题,但在服务端常驻进程里,频繁建立和关闭连接并不划算。现在驱动推荐复用MongoClient实例,并用async/await组织逻辑:
const { MongoClient } = require('mongodb');
const url = 'mongodb://localhost:27017';
const client = new MongoClient(url);
async function sortEmployees() {
try {
await client.connect();
const db = client.db('MongoDatabase');
const result = await db.collection('employees')
.find({})
.sort({ name: 1 })
.toArray();
console.log(result);
} finally {
await client.close();
}
}
sortEmployees();
sort()在async/await中依然返回游标,链式调用方式不变,只是最终用await消费。错误处理交给try/finally,连接释放更可靠。
常见误区与注意事项
sort()的字段值只接受1和-1,其他数值或布尔值不会按预期工作。排序字段不存在于文档中时,MongoDB会把这些文档排在前面(升序)或后面(降序),具体行为取决于字段缺失的情况,做数据统计时要留意。
排序和过滤使用不同的字段时,如果过滤字段有索引而排序字段没有,查询可能先走索引过滤,再在内存中排序。这种情况在数据量增长后容易成为瓶颈。比较稳妥的做法是让复合索引同时覆盖过滤和排序字段。
对于文本字段的排序,MongoDB默认按二进制比较,结果可能与自然语言习惯不一致。中文排序尤其明显,如果需要按拼音或笔画排序,通常要在应用层处理,或者借助collation指定排序规则。
本节课程知识要点
-
sort()是游标方法,调用顺序为find().sort().toArray()。 -
排序方向用
1和-1表示,其他值不生效。 -
多字段排序按书写顺序决定优先级,字段顺序影响结果。
-
排序字段缺少索引时会触发内存排序,默认上限100MB。
-
复合索引的方向组合需要与排序方向匹配才能被有效利用。
-
分页场景中,深分页的
skip()性能较差,游标分页更合适。 -
现在驱动推荐复用连接并使用async/await,排序链式写法保持不变。
练习与思考
-
把
code01_sort_asc.js改写为async/await版本,并加入try/finally释放连接。 -
写一个查询,先过滤出部门为
研发的员工,再按入职时间降序排列,取前5条。 -
思考:如果上已有索引
{department:1,joinDate:-1},查询条件为{department:'研发'},排序为{joinDate:-1},能否走索引完成排序?如果排序改为{joinDate:1}呢? -
给分页示例增加一个参数化的页码,并说明为什么页码越大查询越慢。
sort()的语法只有1和-1两个取值,但真正决定排序查询质量的,是索引设计与调用顺序的配合。把过滤字段和排序字段统一纳入复合索引,理解多字段排序的优先级,再根据数据量选择合适的分页方式,这套组合在大多数业务场景里都能稳定支撑。后续如果要处理聚合管道中的$sort阶段,这里建立的索引意识同样适用。