$sample默认全表扫描,与副本集无关,因其不依赖索引,而是估算总数后随机跳过再顺序扫描;5.0+仅在无写入压力等苛刻条件下才用_id索引加速。

$sample 在副本集上默认走全表扫描,不是副本集特有问题,而是它根本没走索引 —— 无论主从,只要没满足优化前提,就必然 COLLSCAN。
为什么 $sample 在副本集里也慢得离谱
很多人误以为副本集同步延迟或 secondary 读取导致 $sample 变慢,其实不是。$sample 的慢和副本集架构无关,根源在它自身执行机制:MongoDB 默认不依赖索引实现随机采样,而是靠“估算总数 → 随机跳过 N 条 → 顺序扫描直到凑够 size”。这个策略在任何节点(包括 primary)上都一样,一旦集合文档数超百万、内存缓存不足,就会触发大量磁盘 I/O。你在 db.currentOp() 里看到 stage: "COLLSCAN" 和高 executionTimeMillisEstimate,就是典型信号。
5.0+ 版本用 _id 索引加速的硬性条件
虽然 MongoDB 5.0+ 声称支持利用 _id 索引优化 $sample,但实际生效极其苛刻:
- 必须是干净写入场景:集合当前无写入压力(
wiredTiger.cache.dirty持续 -
_id必须分布均匀:ObjectId 时间戳成分导致新文档集中在索引尾部,高频写入时采样结果严重偏向“最新数据”,引擎会主动降级回 COLLSCAN - 不能混用无索引
$match:哪怕只加一个{$match: {type: "user"}},而type字段没索引,整个$sample就立刻失效,回落全表扫 - 副本集 secondary 节点若开启
readPreference=secondary,还多一层网络+applyOps 延迟,但瓶颈仍在采样本身
替代方案比硬扛 $sample 更可靠
与其赌 _id 分布和写入节奏,不如换思路:
- 用时间范围 +
$sample组合:先{$match: {createdAt: {$gte: ISODate("2026-08-01")}}}缩小基数,再$sample,前提是createdAt有索引且数据分布合理 - 预生成随机 ID 列表:应用层生成一批合法
ObjectId(比如基于时间戳段+随机字节),用{$in: [...]}查询,命中_id索引,快且可控 - 业务可接受近似随机?建一个带哈希字段的辅助索引,比如
{hash: 1, _id: 1},插入时用MD5(_id)填充hash,再{$sample: {size: 100}}配合{$sort: {hash: 1}}(注意这不是标准写法,需验证版本兼容性)
真正容易被忽略的是:$sample 的“随机”在分布式写入下天然不均匀,而副本集 secondary 的 applyOps 卡顿会让这种不均匀叠加 IO 抖动 —— 所以别在 secondary 上跑重采样任务,更别指望靠调大 oplog 或改 sync 线程数来提速。











