SQL:选择总和超过0.7的最少行

时间:2013-11-30 12:54:30

标签: sql postgresql

原始数据表是

+--------+--------+
|     id |  value |
+--------+--------+
|      1 |    0.1 |
|      1 |    0.2 | 
|      1 |    0.3 |
|      1 |    0.2 |
|      1 |    0.2 |
|      2 |    0.4 |
|      2 |    0.5 |
|      2 |    0.1 |
|      3 |    0.5 |
|      3 |    0.5 |
+--------+--------+

对于每个id,其值总和为1.我想选择每个id的最少的行,其值和大于或等于0.7,如

+--------+--------+
|     id |  value |
+--------+--------+
|      1 |    0.3 |
|      1 |    0.2 |
|      1 |    0.2 |
|      2 |    0.5 |
|      2 |    0.4 |
|      3 |    0.5 |
|      3 |    0.5 |
+--------+--------+

如何解决这个问题?

3 个答案:

答案 0 :(得分:1)

它既不漂亮也不高效,但它是我能想到的最好的。

免责声明:我确定这会对任何真实数据集执行可怕

with recursive calc (id, row_list, value_list, total_value) as (
   select id, array[ctid], array[value]::numeric(6,2)[], value::numeric(6,2) as total_value
   from data
   union all
   select c.id, p.row_list||c.ctid, (p.value_list||c.value)::numeric(6,2)[], (p.total_value + c.value)::numeric(6,2)
   from data as c
     join calc as p on p.id = c.id and c.ctid <> all(p.row_list)
)
select id, unnest(min(value_list)) as value
from (
  select id, 
         value_list,
         array_length(row_list,1) num_values,
         min(array_length(row_list,1)) over (partition by id) as min_num_values
  from calc
  where total_value >= 0.7
) as result
where num_values = min_num_values  
group by id

SQLFiddle示例:http://sqlfiddle.com/#!15/8966b/1

这是如何工作的?

递归CTE(thew with recursive)部分创建表中所有可能的值组合。为了确保相同的值不被计算两次,我收集已经处理成数组的每一行的CTID s(每行的Postgres内部唯一标识符)。然后,递归连接条件(p.id = c.id and c.ctid <> all(p.row_list))确保仅添加相同id的值,并且仅添加尚未处理的值。

然后将CTE的结果缩减为总和(列total_value)为>= 0.7的所有行。

然后将最终外部选择(别名result)过滤到构成总和的值的数量最小的那些。然后distinct和不再将数组转换回适当的&#34;表&#34;。区别是必要的,因为CTE收集所有组合,以便例如id = 3 value_list数组将包含{0.40,0.50}{0.50,0.40}。如果没有明确的,那么不需要的将返回两个组合,使id=3总共四行。

答案 1 :(得分:1)

这也不是那么漂亮,但我认为它更有效(并且在RDBMS之间可以更好地转移)

with unique_data as (
select id
     , value
     , row_number() over ( partition by id order by value desc ) as rn
  from my_table
       )
, cumulative_sum as (
select id
     , value
     , sum(value) over ( partition by id order by rn ) as csum
  from unique_data
       )
, first_over_the_mark as (
select id
     , value
     , csum
     , lag(csum) over ( partition by id order by csum ) as prev_value
  from cumulative_sum
       )
select *
  from first_over_the_mark
 where coalesce(prev_value, 0) < 0.7

SQL Fiddle

我已经用CTE完成了它,以便更容易看到发生了什么但是没有必要使用它们。

它使用累积和,第一个CTE使数据唯一,因为没有它0.2是相同的值,因此所有0.2的行总和在一起。第二个计算出运行总和。第三个然后计算出之前的值。如果以前严格小于0.7就拿起一切。这个想法是,如果先前的累积和小于0.7,则当前值更多(或等于)该数字。

值得注意的是,如果表中的任何行的值为0,则会出现故障。

答案 2 :(得分:0)

这是Ben的方法的变体,但实现起来更简单。你只需要一个累计和,按反向排序,然后取累积和小于0.7加上第一个超过该值的所有东西。

select t.*
from (select t.*,
             sum(value) over (partition by id order by value desc) as csum
      from t
     ) t
where csum - value < 0.7;

表达式csum - value是累计总和减去当前值(您也可以使用类似rows between unbounded preceding and 1 preceding的内容来获取此值)。您的条件是该值小于某个阈值。

编辑:

Ben的评论是关于重复值的。他的解决方案很好。这是另一种解决方案:

select t.*
from (select t.*,
             sum(value) over (partition by id order by value desc, random()) as csum
      from t
     ) t
where csum - value < 0.7;