来自MongoDB的随机记录

时间:2010-05-13 02:43:17

标签: mongodb

我希望从巨大的(1亿条记录)mongodb中获取随机记录。

最快,最有效的方法是什么?数据已存在,并且没有字段可以生成随机数并获得随机行。

有什么建议吗?

31 个答案:

答案 0 :(得分:188)

从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 :(得分:116)

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

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

答案 2 :(得分:84)

MongoDB 3.2的更新

3.2将$sample引入聚合管道。

将这一点付诸实践还有一个很好的blog post

旧版本(上一个答案)

这实际上是一个功能请求: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的随机数的文档,有必要使用$lterand进行查询。

当然,你想要在随机字段上编制索引:

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

如果您已经查询了索引,只需将其删除,将random: 1添加到该索引,然后重新添加。

答案 3 :(得分:55)

您还可以使用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 )

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

答案 4 :(得分:20)

以下配方比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)快得多,并且生成比迈克尔报告的“cookbook”方法更均匀的随机文档:

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

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

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

菜谱方法会导致大量文档永远不会被选中,因为它们的随机数不支持它们。

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

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

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

这个配方并不完美 - 完美的解决方案将是其他人注意到的内置功能 然而,它应该是一个很好的妥协,用于许多目的。

答案 5 :(得分:9)

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

答案 6 :(得分:7)

在Python中使用pymongo:

import random

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

答案 7 :(得分:6)

如果那里没有关键数据,那就太难了。 _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);

答案 8 :(得分:5)

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

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

See the doc

答案 9 :(得分: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)
    };
}();

答案 10 :(得分:3)

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

答案 11 :(得分:3)

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

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

    <table id="myTable">           
                <tr>
                    <td>
                        cell4
                    </td>
                    <td>
                        cell5
                    </td>
                    <td>
                        cell6
                    </td>
                </tr>
                <tr >
                    <td >
                        cell 12
                    </td>
                    <td >
                        cell8
                    </td>
                    <td  rowspan="2">
                        cell8
                    </td>
                </tr>
                <tr >
                    <td >
                        cell9
                    </td>
                    <td >
                        cell10
                    </td>
                </tr>
            </table>
    

答案 12 :(得分: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)
            })
        })
    })

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

答案 13 :(得分: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函数有效,因为只有一个键('1')从map函数发出。

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

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

如果要从db中精确选择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)是db中的文档数,“countSubset”(n)是要检索的文档数。

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

答案 14 :(得分:2)

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

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

选择一个随机文件。请确保您使用ensureIndex({random_field:1})

答案 15 :(得分:2)

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

<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
<li class="dropdown"><a class="dropdown-toggle" data-toggle="dropdown" href="#">Object Type <span class="caret"></span></a>
  <ul class="dropdown-menu">
    <li><a href="#">Cloth</a></li>
    <li><a href="#">Toy</a></li>
    <li><a href="#">Furniture</a></li>
    <li><a href="#">Gift</a></li>
    <li><a href="#">Household</a></li>
    <li><a href="#">Instrument</a></li>
  </ul>
</li>

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

答案 16 :(得分:1)

非解决方案对我来说效果很好。特别是当有很多空隙而且设置很小时。 这对我来说非常好(在PHP中):

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

答案 17 :(得分:1)

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

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

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

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

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

答案 18 :(得分:1)

如果您使用的是猫鼬,那么您可以使用mongoose-random mongoose-random

答案 19 :(得分: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

用于获取猫鼬文档数组:

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

答案 20 :(得分:0)

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

我已经使用50,000个文档测试了这个方法(过滤器将其减少到大约30,000个),并且在具有16GB RAM和SATA3 HDD的Intel i3上大约 400ms 执行... < / p>

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

400毫秒听起来很长一段时间,如果你有五千万条记录而不是五万条,这可能会增加开销,使其在多用户情况下无法使用。

MongoDB存在一个未解决的问题,即将此功能纳入核心... https://jira.mongodb.org/browse/SERVER-533

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

答案 21 :(得分:0)

MongoDB 现在有 $rand

要选择 n 个不重复的项目,先用 { $addFields: { _f: { $rand: {} } } } 然后用 $sort_f$limit n 聚合。

答案 22 :(得分:0)

与答案实际上相反,$ sample可能不是最快的解决方案。

因为在使用$ sample时,取决于情况,mongo可能会进行收集扫描以进行随机排序。请参阅:参考:https://docs.mongodb.com/manual/reference/operator/aggregation/sample/

也许对结果集进行计数并进行一些随机跳过会更好。

答案 23 :(得分:0)

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

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

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

答案 24 :(得分:0)

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

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

Accounts.find(qry,function(err,results_array){ newIndexArr = 随机播放(results_array);

答案 25 :(得分:0)

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

注意:我的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 ] ] ] ],
        ],
    ];

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

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

答案 26 :(得分:0)

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

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

答案 27 :(得分:0)

这很好用,速度很快,适用于多个文档,并且不需要填充rand字段,最终将自行填充:

  1. 将索引添加到集合中的.rand字段
  2. 使用find和refresh,例如:
  3. // 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。 How to find random records in mongodb问题被标记为此问题的副本。不同之处在于,这个问题明确要求单个记录作为另一个明确地要求获取随机文档 s

答案 28 :(得分:-1)

我正在使用这种方式

db.collection.aggregate(
   [ { $sample: { size: 5 } } ]
)

完整文档https://docs.mongodb.com/manual/reference/operator/aggregation/sample/

答案 29 :(得分:-2)

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

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

在我的.irbrc中,我有

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

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

rando User
rando Article

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

答案 30 :(得分:-7)

这是有效可靠的:

为每个文档添加一个名为“random”的字段,并为其分配一个随机值,为随机字段添加索引并按以下步骤操作:

我们假设我们有一个名为“链接”的网络链接集合,我们想要一个随机链接:

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

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

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