【问题标题】:select 30 random rows where sum amount = x选择 30 个随机行,其中总和 = x
【发布时间】:2018-03-10 14:53:11
【问题描述】:

我有一张桌子

items
id int unsigned auto_increment primary key,
name varchar(255)
price DECIMAL(6,2)

我想从这张表中随机获得至少 30 件商品,总价格为 500,完成此任务的最佳方法是什么?

我见过这个解决方案,它看起来有类似的问题MySQL Select 3 random rows where sum of three rows is less than value

我想知道是否还有其他更容易实施和/或更高效的解决方案

【问题讨论】:

  • 每隔几天就会添加新商品,但价格几乎从未改变
  • 金额如何选择?由用户?有什么限制吗?还是最流行的价值观?
  • 我还没有最流行的实现,现在我只是想获得 30 个随机项目,我给查询一个固定的总和(我将来可能会给用户这个能力)
  • 我认为没有一种高效的方法可以实现这一目标。如果总和始终相同,您可以提前生成组合,然后随机选择其中一个。
  • 数据库应该只是列表的存储库; SQL 应该用于算法。这将花费太长时间。

标签: php mysql


【解决方案1】:

我能提供的最接近的答案是这个

set @cnt = 0;
set @cursum = 0;
set @cntchanged = 0;
set @uqid = 1;
set @maxsumid = 1;
set @maxsum = 0;
select 
    t.id,
    t.name,
    t.cnt
from (
    select 
        id + 0 * if(@cnt = 30, (if(@cursum > @maxsum, (@maxsum := @cursum) + (@maxsumid := @uqid), 0)) + (@cnt := 0) + (@cursum := 0) + (@uqid := @uqid + 1), 0) id, 
        name,  
        @uqid uniq_id,
        @cursum := if(@cursum + price <= 500, @cursum + price + 0 * (@cntchanged := 1) + 0 * (@cnt := @cnt + 1), @cursum + 0 * (@cntchanged := 0)) as cursum, if(@cntchanged, @cnt, 0) as cnt  
    from (select id, name, price from items order by rand() limit 10000) as orig
) as t

where t.cnt > 0 and t.uniq_id = @maxsumid
;

那么它是如何工作的呢?首先,我们从项目中选择 10k 个随机排序的行。之后,我们对商品的价格求和,直到总和小于 500 的 30 件商品。当我们找到 30 件商品时,我们重复该过程,直到遍历所有 10k 个选定商品。在找到这 30 个项目时,我们会保存最大找到的总和。所以最后我们选择总和最大的 30 个项目(即最接近目标 500)。 不确定这是否是您最初想要的,但是找到 500 的确切总和需要在数据库方面付出太多努力。

【讨论】:

    【解决方案2】:

    如果你想高效停止浪费你的时间,去争取最终的一致性。创建控制台脚本,通过任何必要的方式执行您想要完成的任务,然后偶尔在 CRON 或任何调度软件中运行此脚本。

    如果有 100、1000 个访问者,您是否希望每次都执行您的查询?这是耗费时间和资源的。 DBMS 也不能缓存随机排序的查询。追求最终一致性:创建一个表来保存记录并每次清除它,锁定写入,然后加载新集,例如每 5 分钟一次。

    至少这是我在重负载应用程序中的做法。在代码中运行普通的SELECT 查询很重要。

    【讨论】:

      【解决方案3】:

      如果您的产品列表满足以下假设,则有一个解决方案:

      您的产品价格在 0.00 到 500.00 之间。例如。 0.01、0.02 等至 499.99。或者可能是 0.05、0.10 等到 499.95。

      该算法基于以下内容:

      在 n 个总和为 S 的正数的集合中,至少有一个小于 S 除以 n (S/n)

      在这种情况下,步骤是:

      1. 随机选择价格
      2. 随机选择价格
      3. 随机选择价格

      重复 29 次,得到 29 个产品。对于最后一种产品,选择 价格 = 剩余价格。 (或价格

      对于表格项:

      获取随机产品最高价格:

      CREATE PROCEDURE getRandomProduct (IN maxPrice INT, OUT productId INT, productPrice DECIMAL(8,2))
      BEGIN
         DECLARE productId INT;
         SET productId = 0;
             SELECT id, price INTO productId, productPrice
             FROM items
             WHERE price < maxPrice
             ORDER BY RAND()
             LIMIT 1;
      END
      

      随机获得 29 件商品:

      CREATE PROCEDURE get29products(OUT str, OUT remainingPrice DECIMAL(8,2))
      BEGIN
        DECLARE x INT;
        DECLARE id INT;
        DECLARE price DECIMAL(8,2);
        SET x = 30;
        SET str = '';
        SET remainingPrice = 500.00;
      
        REPEAT
          CALL getRandomProduct(remainingPrice/x, @id, @price);
          SET str = CONCAT(str,',', @id);
          SET x = x - 1;
          SET remainingPrice = remainingPrice - @price;
          UNTIL x <= 1
        END REPEAT;
      END
      

      调用过程:

      CALL `get29products`(@p0, @p1); SELECT @p0 AS `str`, @p1 AS `remainingPrice`;
      

      最后尝试找到最后一个达到 500 的产品。

      或者,您可以选择 28 并使用您提供的链接问题的解决方案来获得总和为剩余价格的几个产品。

      请注意,重复产品是允许的。为避免重复,您可以扩展 getRandomProduct,为已找到的产品添加一个额外的 IN 参数,并添加条件 NOT IN 以排除它们。

      更新:您可以克服上述限制,这样您就总能找到总和为 500 的集合,方法是使用所述的 cron 进程在下面的第 2 部分。

      第二部分:使用 cron 进程

      根据@Michael Zukowski 的建议,您可以

      • 创建一个表来保存找到的集合
      • 定义一个运行上述算法多次(例如 10 次)的 cron 进程,例如。每 5 分钟一次
      • 如果找到与总和匹配的集合,则将其添加到新表中

      通过这种方式,您可以找到总和恰好为 500 的集合。当用户提出请求时,您可以从新表中随机选择一个集合。

      即使匹配率为 20%,在 24 小时内每 5 分钟运行 10 次算法的 cron 进程也可以收集超过 500 个。

      在我看来,使用 cron 进程有以下优点和缺点:

      优势

      • 查找完全匹配项
      • 没有处理客户端请求
      • 即使匹配率很低,您也可以找到多个合集

      缺点

      • 如果价格数据经常更新,您可能会得到不一致的结果,可能使用 cron 进程是行不通的。
      • 必须丢弃或过滤旧集合
      • 每个客户端可能不是随机的,因为不同的客户端可能会看到相同的集合。

      【讨论】:

        【解决方案4】:

        我很惊讶没有人建议,记录在案,蛮力解决方案:

        SELECT 
            i1.id, 
            i2.id, 
            ..., 
            i30.id, 
            i1.price + i2.price + ... + i30.price
        FROM items i1 
        INNER JOIN items i2 ON i2.id NOT IN (i1.id)
        ...
        INNER JOIN items i30 ON i30.id NOT IN (i1.id, i2.id, ..., i29.id)
        ORDER BY ABS(x - (i1.price + i2.price + ... + i30.price))
        

        这样的请求可能由程序生成以避免错误。这几乎是个笑话,因为时间是 O(n^30)(通用 https://en.wikipedia.org/wiki/Subset_sum_problem 是 NP 完备的,但如果你固定子集的大小,它不是。 ),但它是可能的,并且可能对预计算有意义。当价格集不变时,使用预先计算的价格集并找到具有这些价​​格的随机商品。

        有一个动态编程解决方案(请参阅 Wikipedia),但它可能需要很长时间才能满足您的需求。还有一个多项式时间近似算法,但在 queries 中,最简单的实现是 O(n)(我没有搜索其他实现)。

        我提出了另一种可能性,没有 Jannes Botis 的假设。原理是贪婪的“爬山”,有一些撤退,因为贪婪的方法并不适合所有情况。

        首先,总结一下:取30个最便宜的物品的总数,然后通过将便宜的物品替换为昂贵的物品,以尽可能快的速度前进到x(贪婪);如果您超出 x,则尽可能后退一步并继续攀登,除非您完成或累了。

        现在,细节(应该使用 PHP + MySQL,而不仅仅是 MySQL):

        令 N = 30

        第一步:初始化

        按价格升序排序并选择前N个

        • 如果总价为 x,您就完成了。
        • 如果总价大于 x,请放弃:您不能产生等于 x 的总价。
        • 否则继续购买最便宜的 N 件商品。

        有了价格的 B-tree 索引,应该很快

        第 2 步:攀登

        因此,x - total > 0,我们希望差值最接近 0。

        选择每对项目(带有连接),其中:

        1. 第一个项目 i1 在 N 个选定项目中
        2. 第二项i2不在N个选中项中,
        3. i1 的价格高于 i2 的价格:p1 - p2 > 0。
        4. (x - 总数) - (p1 - p2) >= 0

        将结果按升序排列 (x - total) - (p1 - p2)。

        • 如果没有匹配的行,有两种情况(所以如果你允许 N 增长,可以使用两个查询):

          1. 没有项目,因此 p1 - p2 > 0:增加 N 并添加价格最低的项目。如果 N == n,则无法达到 x,否则转到第 2 步。
          2. 没有项目使得 (x - total) - (p1 - p2) >= 0:您将超出限制 x。转到第 3 步。
        • 否则取第一行(最接近峰值)并将项目中的 i1 替换为 i2:新的总数是总计 - p1 + p2,现在 x - 总计 >= 0,您更接近于 0。

          • 如果它为零,那么我们就完成了。
          • 否则循环到第 2 步。

        *连接需要一些 O(n):N 项 i1 * [(n-N) 项 i2 减去 p2 > p1]*

        第三步:撤退

        有很多方法可以撤退。这是一个。

        • 如果你刚刚撤退,那就放弃吧:你被卡住了。
        • 如果你已经撤退了n次以上,或者你已经足够接近0,你可以放弃。这样可以避免无限循环。
        • 其他: 删除列表中价格最高的项目,并用不在列表中的最低价格的项目替换它(最大值和最小值,以确保你走得足够低)。然后更新总计并返回第 2 步。

        有了价格的 B-tree 索引,应该很快

        我希望这很清楚。你可以调整它来决定你什么时候做的足够多,并使用预先计算的 30 个项目的集合,总价格为 x。我相信时间复杂度在平均情况下是 O(n)。我做了一些测试(python + sqlite),包含 200 个项目,随机价格在 0 到 1000 之间,没有退路。在 1000 次测试中,22 次未达到 5000(0.44%),3 次尝试成功 708 次,4 次尝试成功 139 次,3 次尝试成功 126 次,5 次尝试成功 4 次,1 次尝试成功 1 次(“尝试”是尝试与 30 个最便宜的项目不同的一组项目:k 次尝试意味着乘以步骤 2)的查询。这将取决于商品的数量、价格……

        您还可以进行变化,例如从一组随机项目开始,尝试缩小 x,围绕 x 振荡而不是后退,...

        【讨论】:

          【解决方案5】:

          根据平均价格和价格分布,您可以尝试以下方法:

          1. 随机选择少于您想要的项目总数(例如 25 个)。重试直到它们的总量小于 x。

          2. 然后使用问题中链接的概念来找到提供剩余金额的组合。

          【讨论】:

          • 我在帖子中尝试了 5 个项目的解决方案,根据给定的总量,它需要 2 到 5 秒以上的时间来执行
          • 你确实有很多项目...你用EXPLAIN分析过你的查询吗?
          • 是的,我做到了......它说使用索引作为表的第一次出现然后使用where,使用索引,使用连接缓冲区......可以满足查询的组合数量太高以使其快速返回
          【解决方案6】:
          1. 首先选择 sum = 500 的所有值
          2. 使用mysql_query

          然后执行以下代码

          $arr = array();
          $num = 0;
          while($row = mysqli_fetch_array($result))
          {
              array_push($arr,$row['id']);
          }
          $arr2= array();
          while(count($arr2!=30)
          {
              $cnt = random(0,count($arr));
              if(in_array($arr[$cnt],$arr2);
              {
                  array_push($arr2,$arr[$cnt]);
              }
          }
          print_r($arr2);
          

          这里 $arr2 是需要的数组

          【讨论】:

            【解决方案7】:

            如果您阅读 MySQL 手册,您可能已经看到 ORDER BY RAND() 来随机化行。

            如果您仅在假设 1000 行时,此示例运行良好且速度很快。一旦有 10000 行,对行进行排序的开销就变得很重要。不要忘记:我们只是将几乎所有的行都扔掉。

            一个很棒的post 处理多种情况,从简单到有间隙,再到有间隙的不均匀。

            这里是你如何完美地做到这一点:

            SELECT id, name, price
             FROM `items` AS i1 JOIN
                (SELECT CEIL(RAND() *
                             (SELECT MAX(id)
                                FROM `items`)) AS id) AS i2
             WHERE i1.id >= i2.id AND i1.price = 500
             ORDER BY i1.id ASC
            LIMIT 30;
            

            【讨论】:

            • 按 rand 排序不是问题,我想要 30 行总计 500 个而不是 30 个价格为 500 的商品
            猜你喜欢
            • 2012-09-22
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2014-02-07
            • 2020-07-16
            • 2015-10-26
            • 2012-10-04
            相关资源
            最近更新 更多