Postgres:由sum函数的结果限制

时间:2014-03-25 00:16:05

标签: sql postgresql aggregate-functions plpgsql common-table-expression

CREATE TABLE inventory_box (
 box_id varchar(10),
 value   integer
);

INSERT INTO inventory_box VALUES ('1', 10), ('2', 15), ('3', 20);

我准备了一个sql fiddle的架构。

我想选择一个合并值大于20的库存盒列表

possible result 1. box 1 + box 2 (10 + 15 >= 20) 

这就是我现在正在做的事情:

SELECT * FROM inventory_box LIMIT 1 OFFSET 0;
-- count on the client side and see if I got enough
-- got 10
SELECT * FROM inventory_box LIMIT 1 OFFSET 1;
-- count on the client side and see if I got enough
-- got 15, add it to the first query which returned 10
-- total is 25, ok, got enough, return answer

我正在寻找一种解决方案,扫描一旦达到目标值就会停止

2 个答案:

答案 0 :(得分:3)

一种可能的方法是以box_id顺序扫描表,直到总数大于30,然后返回所有先前的行加上超过限制的总和的行。请注意,当达到总和时,扫描不会停止,它将整个表格合计,然后返回结果以选择结果。

http://sqlfiddle.com/#!15/1c502/4

SELECT
  array_agg(box_id ORDER BY box_id) AS box_ids,
  max(boxsum) AS boxsum
FROM
(
  SELECT
    box_id,
    sum(value) OVER (ORDER BY box_id) AS boxsum,
    sum(value) OVER (ORDER BY box_id ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING) AS prevboxsum
  FROM 
    inventory_box
) x
WHERE prevboxsum < 30 OR prevboxsum IS NULL;

但实际上,在SQL中(或者根本不是)以一般和可靠的方式做这将是非常可怕的。

如果您愿意,可以ORDER BY value ASC代替ORDER BY box_id;这将添加从最小到最大的框。但是,如果您从池中删除所有小盒子并再次运行它,那么这将会灾难性地失败,并重复。很快就会把两个大盒子混在一起效率低下。

为了解决这个问题,一般情况下,找到最小的组合是一个很难的优化问题,可能会受益于不精确的基于样本和概率的方法。

要按顺序扫描表,直到总和到达目标,然后锁定表,然后使用PL / PgSQL从游标中读取行,这些行返回value顺序加array_agg(box_id) OVER (ORDER BY value)和{sum(value) OVER (order by value)行的行{1}}。达到所需总和后,返回当前行的数组。这不会产生最佳解决方案,但它会生成 a 解决方案,如果有合适的索引,我认为如果没有全表扫描,它就会这样做的地方。

答案 1 :(得分:1)

您的问题更新澄清了您的实际要求比成熟"subset sum problem" suspected by @GhostGambler简单得多: 只需获取行,直到总和足够大。

我按box_id排序以获得确定性结果。您甚至可以完全放弃ORDER BY以更快地获得任何有效结果。

慢:Recursive CTE

WITH RECURSIVE i AS (
   SELECT *, row_number() OVER (ORDER BY box_id) AS rn
   FROM   inventory_box
   )
, r AS (
   SELECT box_id, val, val AS total, 2 AS rn
   FROM   i
   WHERE  rn = 1

   UNION ALL
   SELECT i.box_id, i.val, r.total + i.val, r.rn + 1
   FROM   r
   JOIN   i USING (rn)
   WHERE  r.total < 20
   )
SELECT box_id, val, total
FROM   r
ORDER  BY box_id;

快速:PL/pgSQL function with FOR loop

使用sum()作为窗口聚合函数(这种方式最便宜)。

CREATE OR REPLACE FUNCTION f_shop_for(_total int)
  RETURNS TABLE (box_id text, val int, total int) AS
$func$
BEGIN

total := 0;

FOR box_id, val, total IN
    SELECT i.box_id, i.val
          , sum(i.val) OVER (ORDER BY i.box_id) AS total
    FROM inventory_box i
LOOP
    RETURN NEXT;
    EXIT WHEN total >= _total;
END LOOP; 

END
$func$ LANGUAGE plpgsql STABLE;

SELECT * FROM f_shop_for(35);

我用100万行的大表测试了两者。该函数仅从索引和表中读取必要的行。 CTE非常慢,似乎扫描整个桌子......

SQL Fiddle for both.

除此之外:按包含数字数据的varchar列(box_id)排序会产生可疑结果。也许这应该是一个数字类型,真的吗?

相关问题