选择30个随机行,其中sum amount = x

时间:2018-03-10 14:53:12

标签: php mysql

我有一张桌子

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

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

7 个答案:

答案 0 :(得分:6)

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

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随机排序的行。在它之后,我们总结物品的价格,直到我们达到30项,总和少于500.当我们找到30项时,我们重复这个过程,直到我们遍历所有10k选定的项目。在找到这30个项目时,我们可以节省最多的总和。因此,最后我们选择30个具有最大总和的项目(意味着最接近目标500)。 不确定这是否是您最初想要的,但找到500的精确总和需要在DB端付出太多努力。

答案 1 :(得分:3)

如果您希望高效停止浪费您的时间并去最终的合作。创建控制台脚本,以任何必要的方式完成您想要完成的任务,然后在CRON中运行此脚本或偶尔使用任何计划软件。

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

至少这是我在负载很重的应用程序中的方式。在代码中,运行简单的SELECT查询。

答案 2 :(得分:1)

如果您的产品列表满足以下假设

,则有一种解决方案

您的产品价格介于0.00到500.00之间。例如。 0.01,0.02等到499.99。或者0.05,0.10等等到499.95。

该算法基于以下内容:

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

在这种情况下,步骤是:

  1. 随机选择价格&lt;三十〇分之五百。得到它的价格,让我们说X。
  2. 随机选择价格&lt; (500 - X)/ 29。得到它的价格,假设Y。
  3. 随机选择价格&lt; (500 - X - Y)/ 28。
  4. 重复29次,获得29件产品。对于最后一个产品,请选择价格=剩余价格的产品。 (或价格&lt; =剩余价格和按订单价格排序,希望你能够足够接近)。

    表格项目:

    随机产品最高价格:

    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并在您提供的链接问题上使用解决方案,以获得总计剩余价格的几种产品。

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

    更新:您可以克服上述限制,以便始终使用所述的cron流程查找总计为500 的集合在下面的第二部分。

    第二部分:使用cron进程

    基于@Michael Zukowski的建议,你可以

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

    通过这种方式,您可以找到总是精确到500 的集合。当用户发出请求时,您可以从新表中选择一个随机集合。

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

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

    <强>优点

    • 找到完全匹配
    • 客户请求没有流程
    • 即使匹配率较低,您也可以找到多个收藏品

    <强>缺点

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

答案 3 :(得分:0)

根据平均价格和价格分布,你可以尝试这样的事情:

  1. 随机选择少于您想要的项目(例如25)。重试,直到其总金额小于x。

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

答案 4 :(得分:0)

  1. 首先选择sum = 500
  2. 的所有值
  3. 使用mysql_query
  4. 然后执行以下代码

    $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是必需的数组

答案 5 :(得分:0)

令我感到惊讶的是,没有人建议,作为记录,蛮力解决方案:

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完整的,但是如果你修复子集的大小,则不是。 ),但它可能并且可能对预计算有意义。当价格集没有变化时,使用预先计算的价格集并找到价格过高的随机物品。

有一个动态编程解决方案(请参阅维基百科),但可能需要很长时间才能满足您的需求。还有一个多项式时间近似算法,但天真的实现将是查询中的O(n)(我没有搜索另一个实现)。

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

首先,摘要:取30个最便宜的物品的总和,然后通过用昂贵的物品替换廉价物品,尽可能快地进步到x(贪婪);如果你超越x,那么最大限度地退一步并恢复攀爬,除非你已经完成或累了。

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

设N = 30

第1步:初始化

按升价对商品进行排序,然后选择前N个

  • 总价是x,你完成了。
  • 如果总价格大于x,则放弃:您不能产生等于x的总数。
  • 继续使用N个最便宜的物品。

对于价格的B树索引,它应该是快速的

第2步:攀登

因此,x - 总&gt; 0,我们希望差异最接近0.

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

  1. 第一项i1在N个选定项目中
  2. 第二项i2不在N个选定项目中
  3. i1的价格高于i2的价格:p1 - p2&gt; 0
  4. (x-total) - (p1-p2)> = 0
  5. 按升序排序(x - 总) - (p1 - p2)。

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

      1. 没有项目,以便p1-p2&gt; 0:增加N并添加价格最低的项目。如果N == n,则无法到达x,否则请转到步骤2.
      2. 没有项目,以便(x - 总) - (p1 - p2)&gt; = 0:您将超出限制x。转到第3步。
    • 否则取第一行(最接近峰值)并在项目中用i2替换i1:新总数为总数 - p1 + p2,现在x - 总数> = 0且你离得更近了到0。

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

    *连接将采用一些O(n):N项目i1 * [(n-N)项目i2减去具有p2&gt;的项目; P1] *

    第3步:撤退

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

    • 如果你刚刚退缩,就放弃:你被困住了。
    • 如果你已经退回n次以上或者你已经接近0,你可能会放弃。这避免了无限循环。
    • 否则: 使用列表的最高价格删除项目,并将其替换为列表中没有最低价格的项目(最大值和最小值以确保您足够下降)。然后更新总数并返回步骤2.

    对于价格的B树索引,它应该是快速的

    我希望这很清楚。您可以调整它以决定何时做得足够多并使用预先计算的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振荡而不是后退,......

答案 6 :(得分:-1)

如果你阅读了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;