
后端数据库文档数据库【免费下载链接】FerretDBA truly Open Source MongoDB alternative项目地址https://gitcode.com/gh_mirrors/fe/FerretDB点击查看免费下载排序是数据库查询中使用频率最高的操作之一。本文以 FerretDBMongoDB 的开源替代实现为背景系统讲解 MongoDB 中 BSON 标量值的排序机制从 BSON 类型比较顺序表、跨类型比较规则到find().sort()的升序/降序实战、缺失字段与null的等价性以及用_id作为次级排序键保证输出稳定的最佳实践。读完本文你将能准确预测任意混合 BSON 类型的集合排序结果并在自己的查询中写出稳定、可复现的排序语句。排序的本质BSON 值的比较MongoDB 中的文档以 BSONBinary JSON格式存储字段值可以是 Null、Integer、Long、Double、Decimal、String、Object、Array、BinData、ObjectId、Boolean、Date、Timestamp、Regular Expression 等多种类型。排序的过程本质上就是对这些 BSON 值两两比较、判定相等 / 大于 / 小于关系再依据比较结果按升序或降序排列的过程。在 FerretDB 的 DocumentDB 后端中排序被实现为 PostgreSQL 端的一组存储过程。以 internal/documentdb/documentdb_api_internal/documentdb_api_internal.go 为例可以看到排序能力的核心由bson_orderby、bson_orderby_compare等数据库函数承担BsonOrderby(ctx, conn, l, document, filter, collationstring)将带排序条件的查询文档与过滤器一起下推给documentdb_api_internal.bson_orderby返回排序后的 BSON 文档BsonOrderbyCompare(ctx, conn, l, a, b)调用bson_orderby_compare比较两个 BSON 值返回整数表示大小关系配套的bson_orderby_eq、bson_orderby_gt、bson_orderby_lt分别提供相等、大于、小于的布尔判定供排序比较器复用。这组函数说明排序的比较逻辑并不在 Go 应用层逐个手工编写而是交由底层 DocumentDB 的 BSON 比较内核完成从而保证与原生 MongoDB 的行为对齐。BSON 比较顺序跨类型排序的基石当两个 BSON 值属于同一类型时直接比较它们的取值即可例如两个 Integer 比大小、两个 String 按字典序比较。但当两个值的BSON 类型不同时MongoDB 会使用一张预定义的类型比较顺序表来裁决类型顺序靠前的值更小顺序靠后的值更大。下表即为完整的 BSON 比较顺序从低到高比较顺序从低到高BSON 类型1Null2NumbersInteger、Long、Double、Decimal3String4Object5Array6BinData7ObjectId8Boolean9Date10Timestamp11Regular Expression跨类型比较的推演方法跨类型比较非常简单只需查阅上表即可Null 的顺序为 1是最低类型因此Null 小于任何其他 BSON 值Boolean 的顺序为 8。当它与顺序更低的对象如 ObjectId顺序 7比较时Boolean 更大当它与顺序更高的对象如 Timestamp顺序 10比较时Boolean 更小String 的顺序为 3高于 Numbers顺序 2因此字符串值总是排在任何数值之后。需要特别说明的是Array 是唯一的例外虽然表中给 Array 分配了顺序 5但数组内部的元素会先按元素类型递归套用该表比较其具体行为更复杂本文聚焦标量值数组与 Object 的排序将在另一篇博客中单独展开。几个容易踩坑的等价规则Number 内部各类型一律按数值比较Numbers 虽然细分为 Integer、Long、Double、Decimal 四种 BSON 类型但在比较时它们被视为同一类型比较的焦点是真实数值而不是类型标签。例如Integer 值0与 Double 值0.0在排序比较中被视为相等Integer 值8、Double 值8.5、Long 值9会按照数值大小8 8.5 9排列而不会因为类型不同而套用跨类型比较表。这也是上表把四种数值类型合并为一行 Numbers 的原因。缺失字段等价于 Null在排序比较中字段不存在与字段值为 Null 是等价的。也就是说{v: null}与不包含v字段的{}在按v排序时被视为同一优先级。由于 Null 是顺序 1 的最低类型这两类文档在升序排序时都会排在最前面。实战一混合类型的升序排序假设我们有一个outfits集合通过以下命令插入文档刻意让size字段混合多种 BSON 类型db.outfits.insertMany([ { _id: 1, name: flip flops, size: M, color: blue }, { _id: 2, name: sandals, size: 9, color: null }, { _id: 3, name: boots, size: 8, color: black }, { _id: 4, name: sneakers, size: 8.5, color: blue }, { _id: 5, name: slippers } ])其中flip flops的size是String值Msandals与boots的size是Integer值9与8sneakers的size是Double值8.5slippers文档完全没有size字段。按size升序排序排序键为1db.outfits.find().sort({ size: 1 })返回结果response [ { _id: 5, name: slippers }, { _id: 3, name: boots, size: 8, color: black }, { _id: 4, name: sneakers, size: 8.5, color: blue }, { _id: 2, name: sandals, size: 9, color: null }, { _id: 1, name: flip flops, size: M, color: blue } ]逐条解读这个结果正好可以验证前述全部规则slippers排第一它缺少size字段。根据缺失字段等价于 Null的规则它等价于 Null顺序 1是全部文档中最低的类型因此最先出现三个数值文档紧随其后Numbers顺序 2高于 Null因此排在slippers之后。三者内部的顺序是boots(8)→sneakers(8.5)→sandals(9)——尽管boots、sandals是 Integer、sneakers是 Double但按Number 一律按数值比较的规则只比较真实数值8 8.5 9flip flops排最后String顺序 3高于 Numbers顺序 2因此这个唯一带字符串size的文档排在末尾。降序排序按size降序排序排序键为-1db.outfits.find().sort({ size: -1 })返回结果response [ { _id: 1, name: flip flops, size: M, color: blue }, { _id: 2, name: sandals, size: 9, color: null }, { _id: 4, name: sneakers, size: 8.5, color: blue }, { _id: 3, name: boots, size: 8, color: black }, { _id: 5, name: slippers } ]降序只是把上述类型顺序完全倒转String 的flip flops最先其次是数值文档按9 8.5 8排列最后才是等价于 Null 的slippers。实战二用_id作为次级排序键保证稳定再看按color排序的场景。集合中存在多个color相同的文档且sandals的color是null、slippers完全没有color字段flip flops的color为bluesneakers的color也为bluesandals的color为nullslippers缺少color字段。由于Null 与缺失字段在排序中等价sandals和slippers在按color排序时处于同一优先级理论上两者谁先谁后无法由color决定同理两个blue文档之间也完全并列。此时若查询不指定额外条件排序将回退到数据库检索记录的默认顺序——这个顺序是不稳定的可能导致同一查询在不同执行批次中返回不同顺序的结果。推荐做法是把_id作为次级排序键_id在集合内唯一用它作为并列情况下的决胜键可以保证排序输出完全确定、可复现db.outfits.find().sort({ color: 1, _id: 1 })返回结果response [ { _id: 2, name: sandals, size: 9, color: null }, { _id: 5, name: slippers }, { _id: 3, name: boots, size: 8, color: black }, { _id: 1, name: flip flops, size: M, color: blue }, { _id: 4, name: sneakers, size: 8.5, color: blue } ]结果解读sandals(_id: 2)排在slippers(_id: 5)之前尽管两者的colornullvs 缺失等价但次级排序键_id分出了先后2 5flip flops(_id: 1)排在sneakers(_id: 4)之前两者的color同为blue同样由_id决定先后1 4。这套主排序键 _id次级排序键的组合正是保证分页查询、结果比对、兼容性测试可重复执行的关键。从源码与测试看排序的落地默认排序与兼容性测试在 FerretDB 的集成测试中排序行为被严格验证。以 integration/query_compat_test.go 中的TestQueryCompatSort为例其测试用例覆盖了升序{v: 1, _id: 1}、降序{v: -1, _id: 1}、混合方向{v: 1, _id: -1}与{v: -1, _id: -1}仅按_id的AscSingle/DescSingle非法排序键如13、0、nil、以$或.开头的字段路径的错误处理。其中queryCompatTestCase结构体的注释明确写着sort字段默认回退为bson.D{{_id, 1}}见 integration/query_compat_test.go 与 integration/query_compat_test.go 中opts.SetSort的逻辑。这从测试代码层面印证了当查询未显式指定排序时FerretDB 会按_id升序返回文档这既是稳定的默认行为也是上文中_id作为排序兜底键的又一证据。聚合管线中的$sort排序同样作用于聚合管线。在 integration/aggregate_compat_test.go 的兼容性测试中可以看到$sort阶段在兼容测试与目标测试两侧都会被显式加入当管线包含多个阶段但缺少$sort时测试框架会自动追加{$sort, {_id, 1}}以消除顺序不确定性。这再次说明无论走find还是聚合$sort_id都是保证排序确定性的通用手段。小结本文围绕 BSON 标量值排序完整梳理了四条核心规则同类型 BSON 值直接比较取值不同类型按预定义的类型比较顺序表裁决Integer、Long、Double、Decimal 四种数值类型在比较中统一按数值大小处理缺失字段与null在排序中等价且 Null 处于最低顺序升序时总是排最前出现并列同值或等价时用唯一的_id作为次级排序键即可获得稳定、可复现的输出。掌握这套规则你就能像执行db.outfits.find().sort({ size: 1 })一样在任何混合类型的集合上准确预测排序结果。下一篇博客将深入数组Array与对象Object的排序机制——它们虽然在上表中各占一席但内部的比较逻辑远比标量复杂。赞分享后端数据库文档数据库【免费下载链接】FerretDBA truly Open Source MongoDB alternative项目地址https://gitcode.com/gh_mirrors/fe/FerretDB点击查看免费下载相关推荐Laf 云数据库 sort() 排序查询实战基于 MongoDB 原生 API 的升降序与组合排序Laf 云数据库 sort 排序查询实战基于 MongoDB 原生 API 的升降序与组合排序 在 Laf 云数据库中 sort 是最常用的查询修饰器之一后端Serverless前端云原生理解拓扑排序算法原理与LeetCode实战理解拓扑排序算法原理与LeetCode实战 1. 拓扑排序基础概念 拓扑排序是一种对有向无环图DAG进行线性排序的算法。这种排序满足一个关键特性对于图中教程文档知识库FerretDB 索引完全指南createIndexes、listIndexes 与 dropIndexes 实战与原理FerretDB 索引完全指南createIndexes、listIndexes 与 dropIndexes 实战与原理 索引是数据库提升查询性能的核心机制。后端数据库文档数据库创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考