根据组和最大总和分配组

Posted

技术标签:

【中文标题】根据组和最大总和分配组【英文标题】:Assign groups based on a group and a maximum sum 【发布时间】:2019-10-31 16:02:52 【问题描述】:

我需要按某些列和运行总和对行进行分组,直到达到阈值。我得到的最接近的是基于this answer 的查询,但这个解决方案并不像我需要的那样精确,因为总和必须在达到阈值时重置并重新启动。

这是我对一些样本数据和阈值 100 的尝试:

drop table #table

create table #table (
     Id int not null,
     GroupId int not null,
     Code nvarchar(14) not null,
     Total int not null
)

insert into #table values
( 1, 1, '1111', 20),( 2, 1, '1111', 75),( 3, 1, '1111', 40),( 4, 1, '1111', 20),
( 5, 1, '1111', 20),( 6, 1, '1111', 25),( 7, 1, '2222', 20),( 8, 1, '2222', 20),
( 9, 1, '2222', 20),(10, 1, '2222', 20),(11, 2, '3333', 10),(12, 2, '3333', 90),
(13, 2, '3333', 90),(14, 2, '3333', 90),(15, 2, '3333', 90),(16, 2, '3333', 10),
(17, 2, '3333', 10),(18, 2, '3333', 10),(19, 2, '3333', 10),(20, 2, '3333', 10),
(21, 2, '3333', 10),(22, 2, '3333', 10),(23, 2, '3333', 10),(24, 2, '3333', 10),
(25, 2, '3333', 10),(26, 2, '3333', 10),(27, 2, '3333', 10),(28, 2, '3333', 10),
(29, 2, '3333', 10),(30, 2, '3333', 10),(31, 2, '3333', 10),(32, 2, '3333', 10),
(33, 2, '3333', 10),(34, 2, '3333', 10),(35, 2, '3333', 10)

;with cte as (
     select
         Id, GroupId, Code, Total,
         cast(sum(Total) OVER (ORDER BY Code, id) as int) / 100 AS Limit
     from #table
)
select
     *,
     dense_rank() over(ORDER BY Code, Limit) as Groups
from cte order by id

这就是我得到的,我手动添加了一个“GroupsExpected”列来显示我实际需要的组。

| Id | GroupId | Code | Total | Limit | Groups | GroupsExpected |
|----|---------|------|-------|-------|--------|----------------|
|  1 |       1 | 1111 |    20 |     0 |      1 |              1 |
|  2 |       1 | 1111 |    75 |     0 |      1 |              1 |
|  3 |       1 | 1111 |    40 |     1 |      2 |              2 |
|  4 |       1 | 1111 |    20 |     1 |      2 |              2 |
|  5 |       1 | 1111 |    20 |     1 |      2 |              2 |
|  6 |       1 | 1111 |    25 |     2 |      3 |              3 |
|  7 |       1 | 2222 |    20 |     2 |      4 |              4 |
|  8 |       1 | 2222 |    20 |     2 |      4 |              4 |
|  9 |       1 | 2222 |    20 |     2 |      4 |              4 |
| 10 |       1 | 2222 |    20 |     2 |      4 |              4 |
| 11 |       2 | 3333 |    10 |     2 |      5 |              5 |
| 12 |       2 | 3333 |    90 |     3 |      6 |              6 |
| 13 |       2 | 3333 |    90 |     4 |      7 |              7 |
| 14 |       2 | 3333 |    90 |     5 |      8 |              8 |
| 15 |       2 | 3333 |    90 |     6 |      9 |              9 |
| 16 |       2 | 3333 |    10 |     6 |      9 |             10 |
| 17 |       2 | 3333 |    10 |     6 |      9 |             10 |
| 18 |       2 | 3333 |    10 |     6 |      9 |             10 |
| 19 |       2 | 3333 |    10 |     6 |      9 |             10 |
| 20 |       2 | 3333 |    10 |     7 |     10 |             10 |
| 21 |       2 | 3333 |    10 |     7 |     10 |             10 |
| 22 |       2 | 3333 |    10 |     7 |     10 |             10 |
| 23 |       2 | 3333 |    10 |     7 |     10 |             10 |
| 24 |       2 | 3333 |    10 |     7 |     10 |             10 |
| 25 |       2 | 3333 |    10 |     7 |     10 |             11 |
| 26 |       2 | 3333 |    10 |     7 |     10 |             11 |
| 27 |       2 | 3333 |    10 |     7 |     10 |             11 |
| 28 |       2 | 3333 |    10 |     7 |     10 |             11 |
| 29 |       2 | 3333 |    10 |     7 |     10 |             11 |
| 30 |       2 | 3333 |    10 |     8 |     11 |             11 |
| 31 |       2 | 3333 |    10 |     8 |     11 |             11 |
| 32 |       2 | 3333 |    10 |     8 |     11 |             11 |
| 33 |       2 | 3333 |    10 |     8 |     11 |             11 |
| 34 |       2 | 3333 |    10 |     8 |     11 |             12 |
| 35 |       2 | 3333 |    10 |     8 |     11 |             12 |

每个组的Total 列的总和不能达到 100,而组“9”和“10”达到了这个数量(它们的总和分别为 130 和 100)。

我还尝试使用来自 this answer 的递归 CTE,但在这种情况下,我无法按 Code 列分组。

我可以以编程方式执行此操作,但我觉得这可以通过单个查询更轻松地实现。

我正在使用 MSSQL 2016。

【问题讨论】:

也许是递归 cte?我不知道。我倾向于认为光标(yuk)。 您说“阈值 100”和“不能超过 100”。您指出第 10 组是不可接受的,因为它等于 100 - 但等于 100 不超过 100。由于您在预期结果中指出 100 的总数是不可接受的,我认为这意味着“运行总数必须小于 100",这意味着正好 100 是不可接受的。 您也没有说过如果个人总数为 100 或更多该怎么办。 【参考方案1】:

您可以使用递归 CTE 来做到这一点:

with tt as (
      select t.*, row_number() over (order by id) as seqnum
      from t
     ),
     cte as (
      select id, groupid, code, total, total as totaltotal, 1 as grp, tt.seqnum
      from tt
      where seqnum = 1
      union all
      select tt.id, tt.groupid, tt.code, tt.total,
             (case when cte.totaltotal + tt.total > 100 or cte.groupid <> tt.groupid or cte.code <> tt.code
                   then tt.total else totaltotal + tt.total
              end),
             (case when cte.totaltotal + tt.total > 100 or cte.groupid <> tt.groupid or cte.code <> tt.code
                   then cte.grp + 1 else cte.grp
              end),
             tt.seqnum
      from cte join
           tt
           on tt.seqnum = cte.seqnum + 1
     )
select *
from cte
order by id;

Here 是一个 dbfiddle。

不幸的是,没有“基于集合”的方法。

如果您接受重新启动每个 groupid/code 组合的号码,则可以加快速度。

【讨论】:

【参考方案2】:

这是一个递归 CTE。它要求 ID 是增量的,没有间隙,并且按照您想要的顺序。这在您的示例数据中是正确的。但是,如果不能保证在您的实际数据中是这样的,那么您必须使用带有 row_number 的子查询来按您想要的顺序获取序列号。

declare @table table (
     Id int not null,
     GroupId int not null,
     Code nvarchar(14) not null,
     Total int not null
)

insert into @table values
( 1, 1, '1111', 20),( 2, 1, '1111', 75),( 3, 1, '1111', 40),( 4, 1, '1111', 20),
( 5, 1, '1111', 20),( 6, 1, '1111', 25),( 7, 1, '2222', 20),( 8, 1, '2222', 20),
( 9, 1, '2222', 20),(10, 1, '2222', 20),(11, 2, '3333', 10),(12, 2, '3333', 90),
(13, 2, '3333', 90),(14, 2, '3333', 90),(15, 2, '3333', 90),(16, 2, '3333', 10),
(17, 2, '3333', 10),(18, 2, '3333', 10),(19, 2, '3333', 10),(20, 2, '3333', 10),
(21, 2, '3333', 10),(22, 2, '3333', 10),(23, 2, '3333', 10),(24, 2, '3333', 10),
(25, 2, '3333', 10),(26, 2, '3333', 10),(27, 2, '3333', 10),(28, 2, '3333', 10),
(29, 2, '3333', 10),(30, 2, '3333', 10),(31, 2, '3333', 10),(32, 2, '3333', 10),
(33, 2, '3333', 10),(34, 2, '3333', 10),(35, 2, '3333', 10)

;with rcte as (
    select id, groupid, code, total, total as runtotal, 1 as groups
    from @table
    where id=1

    union all

    select t.id, t.groupid, t.code, t.total,
       case when r.runtotal + t.total >= 100 or r.code <> t.code
            then t.total
            else r.runtotal + t.total
       end as runtotal,
       case when r.runtotal + t.total >= 100 or r.code <> t.code
            then groups + 1
            else groups
       end as groups
    from rcte r
    inner join @table t on t.id = r.id + 1
)
select id, groupid, code, total, groups
from rcte
order by id

【讨论】:

【参考方案3】:

这里有一个光标。

declare @table table (
     Id int not null,
     GroupId int not null,
     Code nvarchar(14) not null,
     Total int not null
)

insert into @table values
( 1, 1, '1111', 20),( 2, 1, '1111', 75),( 3, 1, '1111', 40),( 4, 1, '1111', 20),
( 5, 1, '1111', 20),( 6, 1, '1111', 25),( 7, 1, '2222', 20),( 8, 1, '2222', 20),
( 9, 1, '2222', 20),(10, 1, '2222', 20),(11, 2, '3333', 10),(12, 2, '3333', 90),
(13, 2, '3333', 90),(14, 2, '3333', 90),(15, 2, '3333', 90),(16, 2, '3333', 10),
(17, 2, '3333', 10),(18, 2, '3333', 10),(19, 2, '3333', 10),(20, 2, '3333', 10),
(21, 2, '3333', 10),(22, 2, '3333', 10),(23, 2, '3333', 10),(24, 2, '3333', 10),
(25, 2, '3333', 10),(26, 2, '3333', 10),(27, 2, '3333', 10),(28, 2, '3333', 10),
(29, 2, '3333', 10),(30, 2, '3333', 10),(31, 2, '3333', 10),(32, 2, '3333', 10),
(33, 2, '3333', 10),(34, 2, '3333', 10),(35, 2, '3333', 10)

select *
from @table
order by code,id


declare @runtotal int = 0
declare @groups int = 0
declare @code nvarchar(14)
declare @currentcode nvarchar(14) = ''
declare @total int
declare @id int

declare @output table (
     Id int not null,
     Groups int not null
)

declare cursor_table cursor
    for select id, code, total
        from @table
        order by code,id

open cursor_table
fetch next from cursor_table into @id, @code, @total

while @@fetch_status = 0
    begin
        set @runtotal += @total
        if @runtotal >= 100 or @code <> @currentcode
            begin
                set @runtotal = @total
                set @groups += 1
                set @currentcode = @code
            end
        insert into @output
        select @id,@groups
        fetch next from cursor_table into @id, @code, @total
    end

select t.*,groups
from @table t
inner join @output o on o.id=t.id

close cursor_table
deallocate cursor_table

【讨论】:

以上是关于根据组和最大总和分配组的主要内容,如果未能解决你的问题,请参考以下文章

如何根据火花数据框中的值的累积总和为每一行分配一个类别?

KVM资源划分分配技巧

将整数值分解为保持总和的整数数组

[题解]机器分配

多重分配/矩阵最大化

python中元组和列表有啥区别