$sample是唯一可靠、可落地的随机查询方案,因其避免了skip+random导致的性能拖垮与偏差;size应≤集合总数5%以防退化为全读排序;带条件筛选时须将$match置于$sample前以确保在目标子集中随机抽样。

$sample 是唯一可靠、可落地的随机查询方案,其他方式(比如 skip + random)在真实集合上基本会拖垮性能或返回偏差结果。
为什么不能用 skip(Math.floor(Math.random() * count))?
表面看是“随机跳过再取1条”,但实际触发两轮高开销操作:
- 先执行
countDocuments()获取总数——在无索引、分片集群或大集合上极易超时或不准 - 再全表扫描跳过前 N 条——跳得越靠后越慢,且无法利用任何索引加速
- 更关键的是:它不是真正随机——同一
count值下,多次调用可能反复抽到同一组文档(尤其当写入不均匀时)
$sample 的 size 参数怎么设才不翻车?
size 不是随便填的整数,它直接影响底层策略和资源消耗:
- 若
size> 集合总文档数的 5%,MongoDB 会退化为“全读 +$sort”,受默认 100MB sort 内存限制,报错Sort exceeded memory limit的概率陡增 - 即使总量够大,
size: 5000也会显著拉高 CPU 和内存占用(蓄水池算法本身有开销) - 真实业务中,>1000 条的随机样本已属高需求;如果只是做统计分析或 UI 展示,优先考虑是否真需要“随机”——可改用
$mod或哈希字段分桶实现近似均匀采样
带条件筛选时,$sample 放哪儿才对?
$sample 的随机性只作用于它前面阶段输出的文档流。顺序错了,抽出来的就不是你想要的“随机”:
- 错误写法:
[{$sample: {size: 10}}, {$match: {status: "active"}}]→ 先随机抽 10 条,再筛status,结果可能为空 - 正确写法:
[{$match: {status: "active"}}, {$sample: {size: 10}}]→ 先筛出所有active文档,再从中随机抽 10 条 - 注意:如果
$match后结果集很小(比如只剩 3 条),$sample仍会返回最多 3 条,不会报错也不会补足
真正容易被忽略的点是:即使你写了 $sample,如果它前面有耗时聚合阶段(比如多层 $lookup 或复杂 $unwind),那随机成本其实是叠加在这些阶段之后的——别只盯着 size 看,得先压降前置阶段的输出量。

















