【问题标题】:MySQL: Select Random Entry, but Weight Towards Certain EntriesMySQL:选择随机条目,但偏重某些条目
【发布时间】:2011-01-25 22:24:35
【问题描述】:

我有一个 MySQL 表,其中包含一堆条目,以及一个名为“乘数”的列。此列的默认(也是最常见的)值为 0,但它可以是任何数字。

我需要做的是从该表中随机选择一个条目。但是,这些行是根据“乘数”列中的数字加权的。值为 0 意味着它根本没有加权。值 1 表示它的权重是两倍,就好像该条目在表中出现两次一样。值 2 表示它的权重是表的三倍,就好像条目在表中的三倍一样。

我正在尝试修改我的开发人员已经给我的内容,如果设置没有太大意义,非常抱歉。我可能会更改它,但希望尽可能多地保留现有的表设置。

我一直试图弄清楚如何使用 SELECT 和 RAND() 来执行此操作,但不知道如何进行加权。有可能吗?

【问题讨论】:

  • “好像条目在表中出现了两次”听起来是一个很好的起点。将每一行重复Multiplier 次,然后像往常一样进行随机选择。
  • 当你说“重复每一行”是什么意思?

标签: php mysql select random database-table


【解决方案1】:

@ali 的回答效果很好,但您无法控制结果偏向更高或更低权重的程度,您可以更改乘数,但这不是一种非常动态的方法。

我通过添加 POWER(weight,skewIndex) 而不是 weight 来优化代码,这使得更高的权重在 skewIndex 的值大于 1 时显示得更多,而在 0 和 1 之间的值显示得更少。

SELECT * FROM tbl AS t1 JOIN (SELECT id FROM tbl ORDER BY -LOG(1-RAND())/POWER(weight,skewIndex) LIMIT 10) AS t2 ON t1.id = t2.id

您可以使用

分析查询结果

SELECT AVG(weight) FROM tbl AS t1 JOIN (SELECT id FROM tbl ORDER BY -LOG(1-RAND())/POWER(weight,skewIndex) LIMIT 10) AS t2 ON t1.id = t2.id

例如,将 skewIndex 设置为 3 平均为 78%,而 skewIndex 为 1 则平均为 65%

【讨论】:

    【解决方案2】:

    为了更好的性能(特别是在大表上),首先索引权重列并使用以下查询:

    SELECT * FROM tbl AS t1 JOIN (SELECT id FROM tbl ORDER BY -LOG(1-RAND())/weight LIMIT 10) AS t2 ON t1.id = t2.id
    

    在 40MB 的表上,通常的查询在我的 i7 机器上需要 1 秒,而这个需要 0.04 秒

    有关为什么这更快的解释,请参阅MySQL select 10 random rows from 600K rows fast

    【讨论】:

    • 你能解释一下子查询的意义吗?为什么不在最里面的子查询中 SELECT * 并取消其他两个?这就是通常查询的形式。
    • @concat 那是因为 SQL 的工作原理:当您在大表上执行订单时,它会加载整个数据,然后根据 order by 子句进行排序,但这里的子查询仅适用于索引数据在内存中可用。查看这些测试:通常 > i.stack.imgur.com/006Ym.jpg,子查询 > i.stack.imgur.com/vXU8e.jpg 响应时间突出显示。
    • 我现在可以确认,虽然非常出乎意料,但我想现在我明白了它是如何工作的。感谢您今天向我展示了一些新东西!
    • 不客气,SQL中有很多意想不到的东西,这就是其中之一!
    【解决方案3】:

    This guy 提出同样的问题。他和弗兰克说的一样,但权重不正确,在 cmets 中有人建议使用ORDER BY -LOG(1.0 - RAND()) / Multiplier,在我的测试中给出了非常完美的结果。

    (如果有任何数学家想解释为什么这是正确的,请赐教!但它确实有效。)

    缺点是您无法将权重设置为 0 以暂时禁用某个选项,因为您最终会被零除。但您始终可以使用 WHERE Multiplier > 0 将其过滤掉。

    【讨论】:

    • 1 - RAND() 等价于 RAND(),它(理想情况下)是 0 和 1 之间的一致。-LOG(RAND())/weight 是指数,速率为 weight。将 Expo 视为从现在开始直到您收到特定类型的电子邮件的时间,而速率是每种电子邮件到达的速度。 LIMIT 1 只是挑选下一封电子邮件。
    • 太棒了!我将其修改为对相关表中的聚合值进行加权。 SELECT l.name, COUNT(l.id) FROM consignments c INNER JOIN locations l ON c.current_location_id = l.id GROUP BY l.id ORDER BY -LOG(RAND()) / COUNT(l.id) DESC跨度>
    • 此解决方案是否意味着 OP 必须稍微更改其乘数逻辑?他们最初说0 的乘数表示它没有加权,但您的解决方案意味着0 的乘数被排除在结果集中。 OP 必须稍微改变他们的逻辑,以便 1 的乘数表示不加权,2 表示它在表中两次,等等。无论如何这似乎更有意义,但只是想确认改变是必要的.
    • @flyingL123 是的,好点。或者他们可以用Multiplier + 1替换Multiplier
    • @KenArnold 正如 Crissistian Leonte 在 same thread 中的评论所指出的那样,1 - RAND() 实际上稍微“更干净”,因为它消除了您最终执行 LOG(0) 的微小机会,它返回 @ 987654338@。这是因为 RAND() 返回 0
    【解决方案4】:

    虽然我意识到这是一个关于 MySQL 的问题,但以下内容可能对使用 SQLite3 的人有用,因为 RANDOM 和 LOG 的实现略有不同。

    SELECT * FROM table ORDER BY (-LOG(abs(RANDOM() % 10000))/weight) LIMIT 1;
    

    weight 是表中包含整数的列(我使用 1-100 作为表中的范围)。

    SQLite 中的 RANDOM() 生成介于 -9.2E18 和 +9.2E18 之间的数字(有关更多信息,请参阅 SQLite docs)。我使用了模运算符将数字范围缩小了一点。

    abs() 将删除负数以避免 LOG 仅处理非零正数的问题。

    LOG() 实际上并不存在于 SQLite3 的默认安装中。我使用 php SQLite3 CreateFunction 调用来使用 SQL 中的 php 函数。有关这方面的信息,请参阅 the PHP docs

    【讨论】:

      【解决方案5】:
      SELECT * FROM tablename ORDER BY -LOG(RAND()) / Multiplier;
      

      是给你正确分布的那个。

      SELECT * FROM tablename ORDER BY (RAND() * Multiplier);
      

      给你错误的分布。

      例如,表中有两个条目 A 和 B。 A 的重量为 100,而 B 的重量为 200。 对于第一个(指数随机变量),它给你 Pr(A 获胜) = 1/3,而第二个给你 1/4,这是不正确的。 我希望我能告诉你数学。但是我没有足够的代表来发布相关链接。

      【讨论】:

        【解决方案6】:
        <?php
        /**
         * Demonstration of weighted random selection of MySQL database.
         */
        $conn = mysql_connect('localhost', 'root', '');
        
        // prepare table and data.
        mysql_select_db('test', $conn);
        mysql_query("drop table if exists temp_wrs", $conn);
        mysql_query("create table temp_wrs (
            id int not null auto_increment,
            val varchar(16),
            weight tinyint,
            upto smallint,
            primary key (id)
        )", $conn);
        $base_data = array(    // value-weight pair array.
            'A' => 5,
            'B' => 3,
            'C' => 2,
            'D' => 7,
            'E' => 6,
            'F' => 3,
            'G' => 5,
            'H' => 4
        );
        foreach($base_data as $val => $weight) {
            mysql_query("insert into temp_wrs (val, weight) values ('".$val."', ".$weight.")", $conn);
        }
        
        // calculate the sum of weight.
        $rs = mysql_query('select sum(weight) as s from temp_wrs', $conn);
        $row = mysql_fetch_assoc($rs);
        $sum = $row['s'];
        mysql_free_result($rs);
        
        // update range based on their weight.
        // each "upto" columns will set by sub-sum of weight.
        mysql_query("update temp_wrs a, (
            select id, (select sum(weight) from temp_wrs where id <= i.id) as subsum from temp_wrs i 
        ) b
        set a.upto = b.subsum
        where a.id = b.id", $conn);
        
        $result = array();
        foreach($base_data as $val => $weight) {
            $result[$val] = 0;
        }
        // do weighted random select ($sum * $times) times.
        $times = 100;
        $loop_count = $sum * $times;
        for($i = 0; $i < $loop_count; $i++) {
            $rand = rand(0, $sum-1);
            // select the row which $rand pointing.
            $rs = mysql_query('select * from temp_wrs where upto > '.$rand.' order by id limit 1', $conn);
            $row = mysql_fetch_assoc($rs);
            $result[$row['val']] += 1;
            mysql_free_result($rs);
        }
        
        // clean up.
        mysql_query("drop table if exists temp_wrs");
        mysql_close($conn);
        ?>
        <table>
            <thead>
                <th>DATA</th>
                <th>WEIGHT</th>
                <th>ACTUALLY SELECTED<br />BY <?php echo $loop_count; ?> TIMES</th>
            </thead>
            <tbody>
            <?php foreach($base_data as $val => $weight) : ?>
                <tr>
                    <th><?php echo $val; ?></th>
                    <td><?php echo $weight; ?></td>
                    <td><?php echo $result[$val]; ?></td>
                </tr>
            <?php endforeach; ?>
            <tbody>
        </table>
        

        如果要选择 N 行...

        1. 重新计算总和。
        2. 重置范围(“upto”列)。
        3. 选择$rand指向的行。

        应在每个选择循环中排除先前选择的行。 where ... id not in (3, 5);

        【讨论】:

        • 这种解决方案不会产生大量开销吗?我不确定创建整个表、操作该表以及删除系统会占用多少资源。动态生成的加权值数组会更简单、更不容易出错且占用资源更少吗?
        • 可以通过使用窗口函数得到很大的改进,如果 mysql 有的话。
        【解决方案7】:

        对于其他人在谷歌上搜索这个主题,我相信你也可以这样做:

        SELECT strategy_id
        FROM weighted_strategies AS t1 
        WHERE (
           SELECT SUM(weight) 
           FROM weighted_strategies AS t2 
           WHERE t2.strategy_id<=t1.strategy_id
        )>@RAND AND 
        weight>0
        LIMIT 1
        

        所有记录的权重总和必须为 n-1,@RAND 应为介于 0 和 n-1 之间的随机值。

        @RAND 可以在 SQL 中设置或作为整数值从调用代码中插入。

        子选择将汇总所有前面记录的权重,检查它是否超过提供的随机值。

        【讨论】:

          【解决方案8】:

          伪代码(rand(1, num) % rand(1, num)) 的结果将越来越接近 0 而越来越接近 num。将结果从 num 中减去得到相反的结果。

          所以如果我的应用程序语言是 PHP,它应该看起来像这样:

          $arr = mysql_fetch_array(mysql_query(
              'SELECT MAX(`Multiplier`) AS `max_mul` FROM tbl'
          ));
          $MaxMul = $arr['max_mul']; // Holds the maximum value of the Multiplier column
          
          $mul = $MaxMul - ( rand(1, $MaxMul) % rand(1, $MaxMul) );
          
          mysql_query("SELECT * FROM tbl WHERE Multiplier=$mul ORDER BY RAND() LIMIT 1");
          

          上面代码的解释:

          1. 获取乘数列中的最大值
          2. 计算一个随机乘数值(向乘数列中的最大值加权)
          3. 获取具有该乘数值的随机行

          仅使用 MySQL 也可以实现。

          证明伪代码(rand(1, num) % rand(1, num)) 的权重会趋于0: 执行以下 PHP 代码查看原因(在此示例中,16 是最高数字):

          $v = array();
          
          for($i=1; $i<=16; ++$i)
              for($k=1; $k<=16; ++$k)
                  isset($v[$i % $k]) ? ++$v[$i % $k] : ($v[$i % $k] = 1);
          
          foreach($v as $num => $times)
                  echo '<div style="margin-left:', $times  ,'px">
                        times: ',$times,' @ num = ', $num ,'</div>';
          

          【讨论】:

          • 我绞尽脑汁试图理解这段代码在做什么,但我在那里看到了一些我以前从未见过的东西。能通俗点解释一下吗?
          • 是的 :) 我编辑了我的帖子并解释了 PHP 代码。
          • 看起来不错,但大多数条目的乘数为 0,而且看起来这段代码不会选择它们。
          • 我不明白为什么不...您可以将( rand(1, $MaxMul) % rand(1, $MaxMul) )的值分配给$mul
          【解决方案9】:

          不要使用 0、1 和 2,而是使用 1、2 和 3。然后您可以将此值用作乘数:

          SELECT * FROM tablename ORDER BY (RAND() * Multiplier);
          

          【讨论】:

          • 或者只加1:SELECT * FROM tablename ORDER BY (RAND() * (Multiplier+1));
          • 我想过做这样的事情,但我看不出将随机数乘以另一个数字会导致任何加权。另外,它如何知道从哪个条目中获取乘数值?
          • @John: RAND() 给你一个介于 0 和 1 之间的随机数。更大的乘数让你有更大的机会得到最大的结果。对这个结果进行排序是有意义的。对大型数据集进行一些测试并查看结果。
          • 这实际上并没有给出正确的分布(正如我偶然发现的那样);豪华轿车的回答确实如此。
          • 这给出了一个可怕的偏态分布。假设有 98 行加权 1 和 1 行加权​​ 2。 RAND() 将产生一个介于 0 和 1 之间的数字,因此 50% 的时间将> 0.5。对于加权为 2 的行,(RAND() * 2) 将大于 1 50% 的时间。这比所有 (RAND() * 1) 结果都大,因此将至少有 50% 的时间选择权重为 2 的行。实际上应该选择 2% 的时间 (2/100)。
          【解决方案10】:

          好吧,我会把权重的逻辑放在 PHP 中:

          <?php
              $weight_array = array(0, 1, 1, 2, 2, 2);
              $multiplier = $weight_array[array_rand($weight_array)];
          ?>
          

          和查询:

          SELECT *
          FROM `table`
          WHERE Multiplier = $multiplier
          ORDER BY RAND()
          LIMIT 1
          

          我认为它会起作用:)

          【讨论】:

          • 有趣!理论上,乘数的可能值可以是任何值,但可能会高达 20。这不会使数组变得庞大吗?可以吗?
          • 好吧,你可以让 $weight_array 动态化,这样你就不必手动输入所有的数字了。不用担心资源 - 一千个 int 并不多。
          • @John,然后使用 for 循环动态创建权重数组,方法是在其中放置第二个 for 循环
          • 我不确定这段代码是否符合我的要求:假设我在表中有 100 个条目:98 的乘数为 0,1 的乘数为 1(算作2 个条目),并且 1 的乘数为 2(计为 3 个条目)。选择 0-multiplier 条目的机会应该是 98/103,1-multiplier 条目应该是 2/103,2-multiplier 条目应该是 3/103。但是,使用您的代码,机会将是 1/6、2/6、3/6。也许我需要将每个条目的ID放入一个数组中,多次输入加权条目,然后使用array_rand?
          • 您不必将每个条目 ID 放入一个数组中。您可以按重量计算:0 时为 98,1 时为 1,2 时为 1。将偏移位置放入数组并根据重量重复(再次将其添加到数组中)。所以数组将包含数字 1 到 98,每个出现一次,99 出现两次,100 出现 3 次。从数组中随机选择一个位置,按重量对数据进行排序,然后在所选位置取出项目。这将更适合更大的数据集。
          【解决方案11】:

          无论你做什么,都会很糟糕,因为它会涉及: * 将所有列的总“权重”作为一个数字(包括应用乘数)。 * 获取一个介于 0 和该总数之间的随机数。 * 获取所有条目并运行它们,从随机数中减去权重,并在项目用完时选择一个条目。

          平均而言,您会跑到一半的桌子上。性能 - 除非表很小,否则在内存中的 mySQL 之外执行 - 会很慢。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2020-12-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2021-04-08
            相关资源
            最近更新 更多