如何从MongoDB获得随机记录?

我想从一个巨大的集合(1亿条记录)中获得一个随机记录。

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

数据已经在那里,没有字段可以生成随机数并获得随机行。

199738 次浏览

对所有记录进行计数,生成一个0到计数之间的随机数,然后执行:

db.yourCollection.find().limit(-1).skip(yourRandomNumber).next()

如果没有数据,这是很困难的。_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);

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

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

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

有效可靠的方法是:

在每个文档中添加一个名为“random”的字段,并为其分配一个随机值,为该随机字段添加一个索引,如下所示:

让我们假设我们有一个名为“links”的网络链接集合,我们想从它中随机链接:

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

为了确保同一个链接不会第二次弹出,用一个新的随机数更新它的随机场:

db.links.update({random: Math.random()}, link)

MongoDB 3.2更新

3.2在聚合管道中引入了美元的样品

还有一个很好的博客把它付诸实践。

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

这实际上是一个功能请求: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附加到它,然后再次添加它。

我建议使用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')。

“probability”的值在“scope”中定义,当调用mapRreduce(…)

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

如果你想从db中选择n (m)个文档,你可以这样做:

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)是数据库中的文档数量,“count子集”(n)是要检索的文档数量。

这种方法可能会在分片数据库上产生一些问题。

您还可以使用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轴为查询添加第二个随机性维度。

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

ids = @coll.find({},fields:{_id:1}).to_a
@coll.find(ids.sample).first

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

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

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

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

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

如果您正在使用monid,文档到对象的包装器,您可以在 Ruby。(假设你的模型是User)

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

在我的。irbrc,我有

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

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

rando User
rando Article

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

没有一个解决方案对我来说很好。尤其是当缝隙多、集小的时候。 这工作得很好为我(在php):

$count = $collection->count($search);
$skip = mt_rand(0, $count - 1);
$result = $collection->find($search)->skip($skip)->limit(1)->getNext();

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

我已经用50,000个文档测试了这个方法(过滤器将其减少到大约30,000个),并且它在带有16GB ram和SATA3 HDD的Intel i3上大约400毫秒中执行…

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。在我的例子中,我测试了5万个可能的文档中的大约3万个。

Reduce函数简单地从数组中选取0到项数(-1)之间的随机整数,然后从数组中返回_id

400ms听起来是一段很长的时间,而且确实如此,如果您有5000万条记录而不是5万条记录,这可能会增加开销,以至于在多用户情况下无法使用。

MongoDB在核心中包含这个功能有一个悬而未决的问题…https://jira.mongodb.org/browse/SERVER-533

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

下面的方法比mongo烹饪书解决方案稍慢(在每个文档上添加一个随机键),但是返回分布更均匀的随机文档。与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;
}

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

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

基准测试结果

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

对于包含1,000,000个元素的集合:

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

  • skip()方法平均花费180毫秒

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

  • 该方法将在一段时间内均匀地挑选所有元素。

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

  • 随机性并不是100%完美的,但是它已经很好了(如果有必要的话还可以进行改进)

这个配方并不完美-正如其他人所注意到的那样,完美的解决方案将是内置功能 然而,对于许多目的来说,这应该是一个很好的妥协

这工作得很好,它很快,适用于多个文档,并且不需要填充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)
}
})

ps. 如何在mongodb中找到随机记录问题被标记为此问题的重复。区别在于,这个问题显式地询问单个记录,而另一个问题显式地询问随机document<年代trong>年代

您可以选择一个随机的时间戳,并搜索随后创建的第一个对象。 它只会扫描单个文档,尽管它不一定会给你一个统一的分布
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)
};
}();

我对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;
}

在Python中使用pymongo:

import random


def get_random_doc():
count = collection.count()
return collection.find()[random.randrange(count)]

您可以选择随机_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)
})
})
})

在这里,你不需要花空间存储随机数字的集合。

下面是一种使用_id的默认ObjectId值的方法,以及一些数学和逻辑。

// 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值本质上更简单,但在点中基本思想相同。

从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时,返回的文档样本中可能有重复项。

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

  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)
    });
    });
    

现在您可以使用聚合。 例子:< / p >

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

看医生

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

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

这种方法是快多了,而不是对一个随机数(例如collection.find([random_int]))运行查询。对于大型收藏来说尤其如此。

我的PHP/MongoDB排序/顺序随机解决方案。希望这对大家有所帮助。

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

首先,我用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 ] ] ] ],
],
];

在此之后,您可以使用Pipeline排序。

    $aggregate[] = [
'$sort' => [
'random_sort' => 1
]
];

你也可以在执行查询后使用shuffle-array

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

< p > Accounts.find (qry、功能(呃,results_array) { newIndexArr = 洗牌(results_array); < / p >

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

< p > db.users.aggregate ( [{$sample: {size: 3}}] ) < / p >

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

MongoDB现在有美元兰德

要选择n个非重复项,先用{ $addFields: { _f: { $rand: {} } } }聚合,然后通过_f$limit n聚合。

在Mongoose中最好的方法是使用$sample进行聚合调用。 然而,Mongoose并不会将Mongoose文档应用到Aggregation中——尤其是在同样应用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
对于我来说,我想以随机顺序获得相同的记录,所以我创建了一个用于排序的空数组,然后生成1到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]);
}

我最简单的解决办法是……

db.coll.find()
.limit(1)
.skip(Math.floor(Math.random() * 500))
.next()

你至少有500件收藏品