399

我希望从庞大的集合(1 亿条记录)中获取随机记录。

最快和最有效的方法是什么?

数据已经存在,并且没有我可以在其中生成随机数并获得随机行的字段。

4

29 回答 29

337

从 MongoDB 3.2 版本开始,您可以使用$sample聚合管道运算符从集合中获取 N 个随机文档:

// Get one random document from the mycoll collection.
db.mycoll.aggregate([{ $sample: { size: 1 } }])

如果要从集合的过滤子集中选择随机文档,$match请在管道前添加一个阶段:

// Get one random document matching {a: 10} from the mycoll collection.
db.mycoll.aggregate([
    { $match: { a: 10 } },
    { $sample: { size: 1 } }
])

如评论中所述,当size大于 1 时,返回的文档样本中可能存在重复。

于 2015-11-07T02:28:27.410 回答
120

对所有记录进行计数,生成一个介于 0 和计数之间的随机数,然后执行以下操作:

db.yourCollection.find().limit(-1).skip(yourRandomNumber).next()
于 2010-05-13T02:48:12.150 回答
90

MongoDB 3.2 更新

3.2 将$sample引入聚合管道。

还有一篇很好的博客文章将其付诸实践。

对于旧版本(以前的答案)

这实际上是一个功能请求: http: //jira.mongodb.org/browse/SERVER-533但它是在“不会修复”下提交的。

食谱有一个很好的方法来从集合中选择一个随机文档:http: //cookbook.mongodb.org/patterns/random-attribute/

套用配方,您分配随机数到您的文档:

db.docs.save( { key : 1, ..., random : Math.random() } )

然后随机选择一个文档:

rand = Math.random()
result = db.docs.findOne( { key : 2, random : { $gte : rand } } )
if ( result == null ) {
  result = db.docs.findOne( { key : 2, random : { $lte : rand } } )
}

同时查询$gte$lte是找到随机数最近的文档所必需的rand

当然,您需要在随机字段上建立索引:

db.docs.ensureIndex( { key : 1, random :1 } )

如果您已经在查询索引,只需将其删除、附加random: 1到它,然后再次添加它。

于 2011-04-01T18:17:11.780 回答
57

您还可以使用 MongoDB 的地理空间索引功能来选择与随机数“最近”的文档。

首先,对集合启用地理空间索引:

db.docs.ensureIndex( { random_point: '2d' } )

要创建一堆在 X 轴上具有随机点的文档:

for ( i = 0; i < 10; ++i ) {
    db.docs.insert( { key: i, random_point: [Math.random(), 0] } );
}

然后你可以像这样从集合中获取一个随机文档:

db.docs.findOne( { random_point : { $near : [Math.random(), 0] } } )

或者您可以检索几个最接近随机点的文档:

db.docs.find( { random_point : { $near : [Math.random(), 0] } } ).limit( 4 )

这只需要一次查询,不需要空检查,而且代码干净、简单、灵活。您甚至可以使用地理点的 Y 轴为您的查询添加第二个随机维度。

于 2012-02-29T12:50:05.020 回答
21

以下配方比 mongo cookbook 解决方案慢一点(在每个文档上添加一个随机键),但返回更均匀分布的随机文档。与解决方案相比,它的分布稍微不均匀skip( random ),但在删除文档的情况下更快且更安全。

function draw(collection, query) {
    // query: mongodb query object (optional)
    var query = query || { };
    query['random'] = { $lte: Math.random() };
    var cur = collection.find(query).sort({ rand: -1 });
    if (! cur.hasNext()) {
        delete query.random;
        cur = collection.find(query).sort({ rand: -1 });
    }
    var doc = cur.next();
    doc.random = Math.random();
    collection.update({ _id: doc._id }, doc);
    return doc;
}

它还要求您在文档中添加一个随机的“随机”字段,所以不要忘记在创建它们时添加它:您可能需要初始化您的集合,如 Geoffrey 所示

function addRandom(collection) { 
    collection.find().forEach(function (obj) {
        obj.random = Math.random();
        collection.save(obj);
    }); 
} 
db.eval(addRandom, db.things);

基准测试结果

这种方法比skip()(ceejayoz 的)方法快得多,并且比 Michael 报告的“cookbook”方法生成更均匀的随机文档:

对于具有 1,000,000 个元素的集合:

  • 这种方法在我的机器上花费不到一毫秒

  • skip()方法平均需要 180 毫秒

Cookbook 方法将导致大量文档永远不会被选中,因为它们的随机数不利于它们。

  • 此方法将随着时间的推移均匀地选取所有元素。

  • 在我的基准测试中,它只比食谱方法慢 30%。

  • 随机性不是 100% 完美但非常好(如有必要可以改进)

这个配方并不完美 - 完美的解决方案将是其他人指出的内置功能。
然而,对于许多目的来说,它应该是一个很好的折衷方案。

于 2014-02-18T23:44:07.763 回答
12

这是一种使用默认ObjectId_id以及一些数学和逻辑的方法。

// Get the "min" and "max" timestamp values from the _id in the collection and the 
// diff between.
// 4-bytes from a hex string is 8 characters

var min = parseInt(db.collection.find()
        .sort({ "_id": 1 }).limit(1).toArray()[0]._id.str.substr(0,8),16)*1000,
    max = parseInt(db.collection.find()
        .sort({ "_id": -1 })limit(1).toArray()[0]._id.str.substr(0,8),16)*1000,
    diff = max - min;

// Get a random value from diff and divide/multiply be 1000 for The "_id" precision:
var random = Math.floor(Math.floor(Math.random(diff)*diff)/1000)*1000;

// Use "random" in the range and pad the hex string to a valid ObjectId
var _id = new ObjectId(((min + random)/1000).toString(16) + "0000000000000000")

// Then query for the single document:
var randomDoc = db.collection.find({ "_id": { "$gte": _id } })
   .sort({ "_id": 1 }).limit(1).toArray()[0];

这是 shell 表示的一般逻辑,并且很容易适应。

所以在点:

  • 查找集合中的最小和最大主键值

  • 生成一个介于这些文档的时间戳之间的随机数。

  • 将随机数添加到最小值并找到大于或等于该值的第一个文档。

这使用“十六进制”中的时间戳值中的“填充”来形成有效值ObjectId,因为这是我们正在寻找的。使用整数作为_id值本质上更简单,但基本思想相同。

于 2015-06-26T11:06:04.700 回答
11

现在您可以使用聚合。例子:

db.users.aggregate(
   [ { $sample: { size: 3 } } ]
)

请参阅文档

于 2017-02-06T17:00:45.640 回答
8

在 Python 中使用 pymongo:

import random

def get_random_doc():
    count = collection.count()
    return collection.find()[random.randrange(count)]
于 2015-01-24T14:38:26.437 回答
8

使用 Python (pymongo),聚合函数也可以工作。

collection.aggregate([{'$sample': {'size': sample_size }}])

这种方法运行随机数查询(例如collection.find([random_int]))要快得多。对于大型集合尤其如此。

于 2018-04-17T14:37:24.707 回答
7

如果没有数据可以关闭,那就很难了。_id 字段是什么?他们是 mongodb 对象 ID 吗?如果是这样,您可以获得最高和最低值:

lowest = db.coll.find().sort({_id:1}).limit(1).next()._id;
highest = db.coll.find().sort({_id:-1}).limit(1).next()._id;

那么如果你假设 id 是均匀分布的(但它们不是,但至少这是一个开始):

unsigned long long L = first_8_bytes_of(lowest)
unsigned long long H = first_8_bytes_of(highest)

V = (H - L) * random_from_0_to_1();
N = L + V;
oid = N concat random_4_bytes();

randomobj = db.coll.find({_id:{$gte:oid}}).limit(1);
于 2010-05-13T13:48:41.953 回答
5

您可以选择一个随机时间戳并搜索之后创建的第一个对象。它只会扫描一个文档,尽管它不一定会给你一个统一的分布。

var randRec = function() {
    // replace with your collection
    var coll = db.collection
    // get unixtime of first and last record
    var min = coll.find().sort({_id: 1}).limit(1)[0]._id.getTimestamp() - 0;
    var max = coll.find().sort({_id: -1}).limit(1)[0]._id.getTimestamp() - 0;

    // allow to pass additional query params
    return function(query) {
        if (typeof query === 'undefined') query = {}
        var randTime = Math.round(Math.random() * (max - min)) + min;
        var hexSeconds = Math.floor(randTime / 1000).toString(16);
        var id = ObjectId(hexSeconds + "0000000000000000");
        query._id = {$gte: id}
        return coll.find(query).limit(1)
    };
}();
于 2014-12-04T23:37:40.050 回答
4

我在 php 上的解决方案:

/**
 * Get random docs from Mongo
 * @param $collection
 * @param $where
 * @param $fields
 * @param $limit
 * @author happy-code
 * @url happy-code.com
 */
private function _mongodb_get_random (MongoCollection $collection, $where = array(), $fields = array(), $limit = false) {

    // Total docs
    $count = $collection->find($where, $fields)->count();

    if (!$limit) {
        // Get all docs
        $limit = $count;
    }

    $data = array();
    for( $i = 0; $i < $limit; $i++ ) {

        // Skip documents
        $skip = rand(0, ($count-1) );
        if ($skip !== 0) {
            $doc = $collection->find($where, $fields)->skip($skip)->limit(1)->getNext();
        } else {
            $doc = $collection->find($where, $fields)->limit(1)->getNext();
        }

        if (is_array($doc)) {
            // Catch document
            $data[ $doc['_id']->{'$id'} ] = $doc;
            // Ignore current document when making the next iteration
            $where['_id']['$nin'][] = $doc['_id'];
        }

        // Every iteration catch document and decrease in the total number of document
        $count--;

    }

    return $data;
}
于 2014-12-23T17:29:22.970 回答
3

为了获得确定数量的没有重复的随机文档:

  1. 首先获取所有ID
  2. 获取文件大小
  3. 循环获取随机索引并跳过重复

    number_of_docs=7
    db.collection('preguntas').find({},{_id:1}).toArray(function(err, arr) {
    count=arr.length
    idsram=[]
    rans=[]
    while(number_of_docs!=0){
        var R = Math.floor(Math.random() * count);
        if (rans.indexOf(R) > -1) {
         continue
          } else {           
                   ans.push(R)
                   idsram.push(arr[R]._id)
                   number_of_docs--
                    }
        }
    db.collection('preguntas').find({}).toArray(function(err1, doc1) {
                    if (err1) { console.log(err1); return;  }
                   res.send(doc1)
                });
            });
    
于 2015-12-19T20:13:54.540 回答
2

我建议使用 map/reduce,您使用 map 函数仅在随机值高于给定概率时发出。

function mapf() {
    if(Math.random() <= probability) {
    emit(1, this);
    }
}

function reducef(key,values) {
    return {"documents": values};
}

res = db.questions.mapReduce(mapf, reducef, {"out": {"inline": 1}, "scope": { "probability": 0.5}});
printjson(res.results);

上面的 reducef 函数有效,因为 map 函数只发出一个键('1')。

调用 mapRreduce(...) 时,“概率”的值在“范围”中定义

像这样使用 mapReduce 也应该可以在分片数据库上使用。

如果您想从数据库中准确地选择 m 个文档中的 n 个,您可以这样做:

function mapf() {
    if(countSubset == 0) return;
    var prob = countSubset / countTotal;
    if(Math.random() <= prob) {
        emit(1, {"documents": [this]}); 
        countSubset--;
    }
    countTotal--;
}

function reducef(key,values) {
    var newArray = new Array();
for(var i=0; i < values.length; i++) {
    newArray = newArray.concat(values[i].documents);
}

return {"documents": newArray};
}

res = db.questions.mapReduce(mapf, reducef, {"out": {"inline": 1}, "scope": {"countTotal": 4, "countSubset": 2}})
printjson(res.results);

其中“countTotal”(m)是数据库中的文档数,“countSubset”(n)是要检索的文档数。

这种方法可能会给分片数据库带来一些问题。

于 2012-02-26T13:43:07.840 回答
2

您可以选择随机 _id 并返回相应的对象:

 db.collection.count( function(err, count){
        db.collection.distinct( "_id" , function( err, result) {
            if (err)
                res.send(err)
            var randomId = result[Math.floor(Math.random() * (count-1))]
            db.collection.findOne( { _id: randomId } , function( err, result) {
                if (err)
                    res.send(err)
                console.log(result)
            })
        })
    })

在这里,您不需要花费空间来存储集合中的随机数。

于 2015-04-30T04:24:13.703 回答
1

当我遇到类似的解决方案时,我回溯并发现业务请求实际上是为了创建某种形式的库存轮换。在这种情况下,有更好的选择,它们有来自 Solr 等搜索引擎的答案,而不是 MongoDB 等数据存储。

简而言之,对于“智能轮换”内容的要求,我们应该做的不是在所有文档中使用随机数,而是包含一个个人 q 分数修饰符。为了自己实现这一点,假设用户数量很少,您可以为每个用户存储一个文档,其中包含 productId、展示次数、点击次数、上次查看日期以及企业认为对计算 aq 分数有意义的任何其他因素修饰符。检索要显示的集合时,通常您从数据存储中请求的文档比最终用户请求的多,然后应用 q 分数修饰符,获取最终用户请求的记录数,然后随机化结果页面,一个很小的设置,所以只需在应用层(内存中)对文档进行排序。

如果用户范围太大,您可以将用户分类为行为组,并按行为组而不是用户进行索引。

如果产品范围足够小,您可以为每个用户创建一个索引。

我发现这种技术效率更高,但更重要的是更有效地创造了使用该软件解决方案的相关、有价值的体验。

于 2013-09-11T16:32:43.107 回答
1

non of the solutions worked well for me. especially when there are many gaps and set is small. this worked very well for me(in php):

$count = $collection->count($search);
$skip = mt_rand(0, $count - 1);
$result = $collection->find($search)->skip($skip)->limit(1)->getNext();
于 2014-01-21T18:07:44.703 回答
1

我建议为每个对象添加一个随机 int 字段。然后你可以做一个

findOne({random_field: {$gte: rand()}}) 

选择一个随机文档。只要确保你确保Index({random_field:1})

于 2010-05-17T18:47:23.017 回答
1

以下聚合操作从集合中随机选择 3 个文档:

db.users.aggregate( [ { $sample: { size: 3 } } ] )

https://docs.mongodb.com/manual/reference/operator/aggregation/sample/

于 2020-10-16T09:09:04.210 回答
1

我的 PHP/MongoDB 按 RANDOM 解决方案排序/排序。希望这对任何人都有帮助。

注意:我的 MongoDB 集合中有引用 MySQL 数据库记录的数字 ID。

首先,我创建一个包含 10 个随机生成数字的数组

    $randomNumbers = [];
    for($i = 0; $i < 10; $i++){
        $randomNumbers[] = rand(0,1000);
    }

在我的聚合中,我将 $addField 管道运算符与 $arrayElemAt 和 $mod(模数)结合使用。模运算符会给我一个 0 - 9 的数字,然后我用它从数组中选择一个随机生成的数字。

    $aggregate[] = [
        '$addFields' => [
            'random_sort' => [ '$arrayElemAt' => [ $randomNumbers, [ '$mod' => [ '$my_numeric_mysql_id', 10 ] ] ] ],
        ],
    ];

之后,您可以使用排序管道。

    $aggregate[] = [
        '$sort' => [
            'random_sort' => 1
        ]
    ];
于 2018-12-20T14:06:54.130 回答
1

MongoDB 现在有$rand

要选择 n 个非重复项,请使用{ $addFields: { _f: { $rand: {} } } }then $sortby_f$limitn 进行聚合。

于 2021-02-23T15:38:46.433 回答
0

这很好用,速度很快,可以处理多个文档并且不需要填充rand字段,最终会填充自己:

  1. 将索引添加到集合中的 .rand 字段
  2. 使用查找和刷新,例如:
// Install packages:
//   npm install mongodb async
// Add index in mongo:
//   db.ensureIndex('mycollection', { rand: 1 })

var mongodb = require('mongodb')
var async = require('async')

// Find n random documents by using "rand" field.
function findAndRefreshRand (collection, n, fields, done) {
  var result = []
  var rand = Math.random()

  // Append documents to the result based on criteria and options, if options.limit is 0 skip the call.
  var appender = function (criteria, options, done) {
    return function (done) {
      if (options.limit > 0) {
        collection.find(criteria, fields, options).toArray(
          function (err, docs) {
            if (!err && Array.isArray(docs)) {
              Array.prototype.push.apply(result, docs)
            }
            done(err)
          }
        )
      } else {
        async.nextTick(done)
      }
    }
  }

  async.series([

    // Fetch docs with unitialized .rand.
    // NOTE: You can comment out this step if all docs have initialized .rand = Math.random()
    appender({ rand: { $exists: false } }, { limit: n - result.length }),

    // Fetch on one side of random number.
    appender({ rand: { $gte: rand } }, { sort: { rand: 1 }, limit: n - result.length }),

    // Continue fetch on the other side.
    appender({ rand: { $lt: rand } }, { sort: { rand: -1 }, limit: n - result.length }),

    // Refresh fetched docs, if any.
    function (done) {
      if (result.length > 0) {
        var batch = collection.initializeUnorderedBulkOp({ w: 0 })
        for (var i = 0; i < result.length; ++i) {
          batch.find({ _id: result[i]._id }).updateOne({ rand: Math.random() })
        }
        batch.execute(done)
      } else {
        async.nextTick(done)
      }
    }

  ], function (err) {
    done(err, result)
  })
}

// Example usage
mongodb.MongoClient.connect('mongodb://localhost:27017/core-development', function (err, db) {
  if (!err) {
    findAndRefreshRand(db.collection('profiles'), 1024, { _id: true, rand: true }, function (err, result) {
      if (!err) {
        console.log(result)
      } else {
        console.error(err)
      }
      db.close()
    })
  } else {
    console.error(err)
  }
})

附言。How to find random records in mongodb question被标记为这个问题的重复。不同之处在于,这个问题明确询问了单个记录,而另一个问题明确询问了获取随机文档s

于 2014-11-19T22:08:23.587 回答
0

如果您有一个简单的 id 键,则可以将所有 id 存储在一个数组中,然后选择一个随机 id。(红宝石回答):

ids = @coll.find({},fields:{_id:1}).to_a
@coll.find(ids.sample).first
于 2013-03-19T14:10:47.087 回答
0

使用 Map/Reduce,您当然可以获得随机记录,但不一定非常有效,具体取决于您最终使用的过滤集合的大小。

我已经用 50,000 个文档(过滤器将其减少到大约 30,000 个)测试了这种方法,它在具有 16GB 内存和 SATA3 硬盘的 Intel i3 上执行大约400ms ...

db.toc_content.mapReduce(
    /* map function */
    function() { emit( 1, this._id ); },

    /* reduce function */
    function(k,v) {
        var r = Math.floor((Math.random()*v.length));
        return v[r];
    },

    /* options */
    {
        out: { inline: 1 },
        /* Filter the collection to "A"ctive documents */
        query: { status: "A" }
    }
);

Map 函数只是创建一个包含与查询匹配的所有文档的 id 的数组。就我而言,我用 50,000 个可能的文档中的大约 30,000 个进行了测试。

Reduce 函数只是在 0 和数组中的项目数 (-1) 之间选择一个随机整数,然后从数组中返回该_id

400ms 听起来很长,而且确实如此,如果您有 5000 万条记录而不是 50000 万条记录,这可能会增加开销,使其在多用户情况下变得无法使用。

MongoDB 在核心中包含此功能存在一个未解决的问题... https://jira.mongodb.org/browse/SERVER-533

如果这种“随机”选择被构建到索引查找中,而不是将 id 收集到一个数组中然后选择一个,这将非常有用。(去投票吧!)

于 2014-01-29T23:26:46.127 回答
0

对我来说,我想以随机顺序获得相同的记录,所以我创建了一个用于排序的空数组,然后生成 1 到 7 之间的随机数(我有 7 个字段)。所以每次我得到不同的值时,我都会分配一个不同的随机排序。这是“外行”,但对我有用。

//generate random number
const randomval = some random value;
//declare sort array and initialize to empty

const sort = [];

//write a conditional if else to get to decide which sort to use

if(randomval == 1)
{


sort.push(...['createdAt',1]);

}

else if(randomval == 2)

{
   sort.push(...['_id',1]);
}

....
else if(randomval == n)
{
   sort.push(...['n',1]);
}
于 2021-11-06T09:15:57.320 回答
0

Mongoose 中最好的方法是使用 $sample 进行聚合调用。但是,Mongoose 不会将 Mongoose 文档应用于聚合 - 特别是如果要应用 populate() 则更是如此。

从数据库中获取“精益”数组:

/*
Sample model should be init first
const Sample = mongoose …
*/

const samples = await Sample.aggregate([
  { $match: {} },
  { $sample: { size: 33 } },
]).exec();
console.log(samples); //a lean Array

获取一组 mongoose 文档:

const samples = (
  await Sample.aggregate([
    { $match: {} },
    { $sample: { size: 27 } },
    { $project: { _id: 1 } },
  ]).exec()
).map(v => v._id);

const mongooseSamples = await Sample.find({ _id: { $in: samples } });

console.log(mongooseSamples); //an Array of mongoose documents
于 2021-04-06T09:21:52.953 回答
-2

如果您使用的是文档到对象包装器 mongoid,您可以在 Ruby 中执行以下操作。(假设您的模型是用户)

User.all.to_a[rand(User.count)]

在我的 .irbrc 中,我有

def rando klass
    klass.all.to_a[rand(klass.count)]
end

所以在rails控制台中,我可以做,例如,

rando User
rando Article

从任何集合中随机获取文档。

于 2013-12-06T12:22:06.507 回答
-5

您还可以在执行查询后使用shuffle-array

var shuffle = require('shuffle-array');

Accounts.find(qry,function(err,results_array){ newIndexArr= shuffle(results_array);

于 2019-05-12T05:43:50.253 回答
-8

有效和可靠的工作是这样的:

为每个文档添加一个名为“random”的字段并为其分配一个随机值,为该随机字段添加一个索引,然后执行以下操作:

假设我们有一个名为“links”的网络链接集合,我们希望从中获得一个随机链接:

link = db.links.find().sort({random: 1}).limit(1)[0]

为确保不会再次弹出相同的链接,请使用新的随机数更新其随机字段:

db.links.update({random: Math.random()}, link)
于 2011-03-25T13:56:27.920 回答