中位数是先把一组数值排序,再取中间位置得到的数字;如果数量为偶数,则取中间两个数的平均值。与平均值不同,中位数不受极端值干扰,因此在收入、响应时间、订单金额这类分布明显倾斜的数据里,中位数经常比平均值更有参考价值。MongoDB从7.0版本开始在聚合管道中提供$median累加器,让数据库端可以直接完成中位数计算,省去了把数据拉到应用层排序的步骤。

下文会先厘清中位数与平均值的差异,再通过$group和$setWindowFields两个阶段展示$median的完整用法,最后讨论精确模式与近似模式的选择以及注意事项。
为什么中位数比平均值更抗干扰
平均值对每个数值一视同仁,任何一个极端值都会直接拉高或拉低结果。例如一组订单金额是 12、15、16、18、200,平均值是 52.2,明显高于大多数真实订单;而中位数是 16,反而更贴近普通订单水平。数据越倾斜,平均值失真越严重,这也是很多统计和监控系统优先选择中位数作为核心指标的原因。
在MongoDB早期版本里,聚合管道没有原生的中位数算子。开发者要么使用$group配合$push把数据收集起来,再在应用层排序取中位数;要么使用MapReduce或者$function自定义脚本。这些方案要么牺牲内存和网络传输,要么写法复杂、维护成本高。从7.0版本开始,$median直接把中位数计算下沉到数据库聚合层,和$sum、$avg一样可以在管道中声明式使用。
不过中位数需要对数据排序,数据库端实现也面临着准确性和性能的平衡。MongoDB的$median通过method参数把选择权交给使用者:默认的近似模式适合大数据量场景,精确模式适合严格结果场景。在讨论具体语法之前,先理解这个背景能帮助你在不同业务里做出合理选择。
在$group阶段用$median计算整体和分组中位数
$median最典型的用法是作为$group阶段的累加器。它接收一个input表达式,通常是要统计的数值字段,例如订单金额。下面的聚合管道可以计算sales集合中所有订单金额的全局中位数:
db.sales.aggregate([
{
$group: {
_id: null,
medianAmount: {
$median: {
input: "$amount",
method: "approximate"
}
}
}
}
])
这里_id设为null表示不分组,管道会扫描整个集合并返回一个文档,其中medianAmount就是中位数。input可以引用字段,也可以写成更复杂的表达式,比如先对值做转换:input: { $multiply: [ "$price", "$quantity" ] }。只要表达式最终得到数值类型,$median都可以处理。
实际业务中更常见的需求是按分类计算中位数。仍以sales集合为例,假设每个文档包含category和amount字段,下面的写法可以输出每个分类的销售金额中位数:
db.sales.aggregate([
{
$group: {
_id: "$category",
medianAmount: {
$median: {
input: "$amount",
method: "precise"
}
}
}
}
])
这段管道将文档按category分组,每个组内部的金额值先被收集并排序,再返回中位数。如果某个分类下的金额字段存在缺失或非数值,$median只考虑数值类型的值,非数值会被忽略;当组内没有任何有效数值时,结果为null。这个行为在处理脏数据时比较友好,不需要额外清洗管道。
还需要注意,$median是聚合累加器,只能用在$group和$setWindowFields这两个阶段,不能像$toUpper那样在普通投影阶段单独使用。如果只想求数组中位数,则需要配合$unwind拆开数组后再分组,或者在7.0以上版本使用相关数组表达式。
approximate与precise模式怎么选
$median的method参数支持两个取值:approximate和precise。如果不显式指定,默认采用approximate。近似模式使用t-digest算法维护数据分布摘要,不需要把所有原始值完整排序,因此内存占用更低,处理速度更快,特别适合高吞吐量的监控和实时分析场景。
具体来说,approximate模式会在聚合过程中维护一个大小可控的分位数摘要,最终根据摘要估算中位数。对于包含数百万、数千万文档的集合,近似结果通常已经足够支撑趋势判断。下面的管道演示了默认近似模式在大数据集上的写法:
db.metrics.aggregate([
{
$group: {
_id: "$service",
medianLatency: {
$median: {
input: "$latency",
method: "approximate"
}
}
}
}
])
而precise模式会完整保留每个分组内的数值,并对这些数值做精确排序,返回数学上严格正确的中位数。它更适合数据量不大、但对结果准确性要求很高的场景,例如财务报表、合规统计、科学计算等。精确模式的代价是更高的内存和CPU消耗,如果分组数据量极大,可能会触发聚合阶段的内存限制,甚至导致查询失败。
一个值得注意的细节是,approximate模式的计算精度与数据分布和算法分桶数量有关,MongoDB文档没有承诺固定的误差范围,只能把它理解为高效近似。因此如果你的业务对中位数精确到小数位有强校验需求,或者数据量只有几千条,直接使用precise并不会带来明显性能问题,反而能避免近似误差带来的争议。
在$setWindowFields中计算窗口中位数
除了全局分组统计,$median还可以作为窗口函数用在$setWindowFields阶段。这意味着你可以在一条管道里为每个文档计算基于局部窗口的中位数,例如最近7条记录的中位数、同一分区内前后两个邻居的中位数等。窗口函数的价值在于它不会把文档折叠成一行,而是保留原始文档并附加统计结果。
下面的示例按category分区,按orderDate升序排列,为每条销售记录计算包含当前文档在内、向前看2条文档的3条金额中位数:
db.sales.aggregate([
{
$setWindowFields: {
partitionBy: "$category",
sortBy: { orderDate: 1 },
output: {
movingMedian: {
$median: {
input: "$amount",
method: "approximate"
},
window: {
documents: [ -2, 0 ]
}
}
}
}
}
])
这段管道会输出所有原始字段,并加上movingMedian字段。窗口边界documents: [ -2, 0 ]表示从当前文档往前数2条到当前文档,共3条记录。MongoDB会按照分区和排序规则滑动这个窗口,在每个位置计算一次中位数。如果窗口前方不足2条文档,就基于已有文档计算。例如分区中第一条记录只能看到自己,中位数就是它自己的amount值;第二条记录看到前2条,中位数为两者的平均值。
窗口中位数非常适合时间序列分析,例如监控指标平滑、移动趋势判断、异常检测等。相比在应用层维护窗口缓存,在数据库端声明窗口边界既简洁又能减少网络传输的数据量。不过需要注意,$setWindowFields中的窗口越大,需要排序和缓存的数据越多,查询资源消耗也越高。
使用$median的注意事项
$median从MongoDB 7.0开始引入,旧版本数据库无法使用。在进行生产环境升级或编写兼容代码时,必须确认目标实例版本。如果团队还在使用6.x或更早版本,可以继续采用$push加应用层处理,或者推动升级到7.0以上。
由于input字段只接受数值表达式,确保字段类型是int、long、double或decimal。字符串形式的数字不会被自动转换,只会被忽略,最终可能出现意外结果为null。在ETL阶段最好先使用$convert或应用层写入时统一类型。
性能方面,precise模式需要把整个分组的数据载入内存做排序,分组基数特别大时容易遇到100MB内存限制。对于近似模式,由于使用t-digest结构,压力会小很多。如果确实需要在超大分组上做精确中位数,可以考虑先通过时间分片或桶化字段缩小分组范围,再对子结果合并,不过合并中位数本身又是另一个复杂话题。总之,默认优先使用approximate,确有精确需求再切换precise,并通过explain观察执行计划和内存消耗。