$sample是唯一推荐的随机抽样方案,因skip+random会触发countDocuments()和全表扫描两轮高开销操作,且非真正随机;size应≤集合总数5%以避免sort内存溢出,带条件筛选时需将$match置于$sample前。

$sample 是唯一推荐的方案,其他方式(比如 skip + random)在真实生产环境里基本不可靠。
为什么不能用 skip(Math.floor(Math.random() * count))?
表面看是“随机跳过再取1条”,但实际会触发两轮高开销操作:
— 先执行 countDocuments() 获取总数,这个在无索引、分片集群或大集合上极易超时或不准;
— 再执行全表扫描跳过前 N 条,跳得越靠后越慢,且无法利用索引加速。
更关键的是:它不是真正随机——同一 count 值下,多次调用可能反复抽到同一组文档(尤其当数据写入不均匀时)。
$sample 的 size 参数怎么设才不翻车?
size 不是随便填的整数,它直接影响底层策略和资源消耗:
- 若
size> 集合总文档数的 5%,MongoDB 会退化为“全读 +$sort”,受sort内存限制(默认 100MB),报错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 条,不会报错也不会补足
不同驱动里怎么写?别漏掉 .toArray()
聚合返回的是游标(cursor),不显式转数组就拿不到数据:
- Node.js / Mongoose:
collection.aggregate([{$sample: {size: 5}}]).toArray() - Python / PyMongo:
list(collection.aggregate([{"$sample": {"size": 5}}])) - Go / mongo-go-driver:
cursor.All(ctx, &results),不能直接用cursor.Next()取单条来凑 N 条
最容易被忽略的一点:$sample 在分片集群上由各 shard 独立执行再合并,所以它天然支持水平扩展;但如果你在聚合里混用了 $lookup 跨库关联,那随机性就只发生在本地 shard 上,全局未必均匀——这种场景得提前评估数据分布。


















