$sample是MongoDB唯一推荐的随机抽样方式,因其底层自动选用全量加载或蓄水池抽样算法,避免skip+random导致的全表扫描、性能瓶颈及分片集群下全局count瓶颈,且不依赖索引、支持管道灵活位置与水平扩展。

$sample 是唯一推荐的随机抽样方式,其他方法在大数据集上基本不可靠。
为什么不能用 skip + random 模拟抽样
很多人写 db.collection.find().skip(Math.floor(Math.random() * count)).limit(1),但这个组合在真实环境里极易出问题:
-
count本身在分片集群或无索引集合上可能超时,甚至返回错误值 -
skip必须顺序扫描前 N 条文档,千万级集合跳 500 万条可能卡住几秒到几十秒 - 即使
count准确,skip也无法并行,而$sample在分片集群中由各 shard 独立执行再合并 - 没有索引时,
skip性能断崖式下跌;$sample不依赖索引,表现稳定
$sample 的 size 参数怎么设才不爆内存
$sample 的行为会随 size 和集合总量动态切换,不是设多少就直接抽多少:
- 如果
size> 集合文档总数的 5%,MongoDB 会退化为“全读 +$sort”,受maxSortKeyMemoryUsageBytes限制(默认 100MB),可能报错Sort exceeded memory limit - 集合文档数 $sample 直接加载全部文档再随机选;>100 且
size≤ 5% 总量时,启用蓄水池抽样(reservoir sampling),内存占用低 - 抽 1000 条以上样本已属高负载场景,应评估是否真需要“随机”——可改用
$mod或哈希字段做分桶采样,更可控
$sample 放在聚合管道哪个位置才算“真正随机”
它的随机性只作用于它**前面阶段输出的文档流**,不是对原始集合整体抽样:
- 写成
[{$match: {status: "active"}}, {$sample: {size: 10}}]→ 抽的是活跃用户里的 10 条,不是全量用户的随机样本 - 写成
[{$sample: {size: 10}}, {$match: {...}}]→ 先随机抽 10 条,再过滤,结果可能少于 10 条,甚至为 0 - 如果要用
$lookup关联后再抽样,注意关联结果膨胀可能导致$sample输入流远大于原始集合,触发内存降级
什么时候该换用 $samplerate 而不是 $sample
$samplerate 适合“按概率采样”,不是“固定数量”:
-
{$match: {$samplerate: 0.01}}表示每条文档有 1% 概率被选中,最终数量不确定(比如 10 万条可能得 982 或 1027 条) - 它底层等价于
{$match: {$expr: {$lt: [{$rand: {}}, 0.01]}}},性能开销略高于$sample,但无内存上限风险 - 如果你要统计“约 1% 的日志样本”,或做 A/B 测试分流,
$samplerate更自然;但报表、导出、测试数据准备这类需要精确条数的场景,必须用$sample
真正容易被忽略的点是:抽样前没加 $match 过滤,或者过滤字段没建索引——这时 $sample 就是在全表上跑,和慢没两样。

















