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

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

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


当前回答

在Python中使用pymongo:

import random

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

其他回答

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

在Python中使用pymongo:

import random

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

下面的方法比mongo烹饪书解决方案稍慢(在每个文档上添加一个随机键),但是返回分布更均匀的随机文档。与跳过(随机)解决方案相比,它的分布稍微不那么均匀,但在删除文档时要快得多,而且更安全。

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%完美的,但是它已经很好了(如果有必要的话还可以进行改进)

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

下面是一种使用_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值本质上更简单,但在点中基本思想相同。

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

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