这可以在没有循环或游标的情况下完成

时间:2017-10-07 17:56:58

标签: tsql sql-server-2008-r2

我有一张表格列出了单个项目以及我们为它们收取的费用。我们收到的付款可能低于结算总额。我想按照原始账单金额的比例将该付款分配给每个项目。

这是棘手的部分。

  • 每个个人的付款金额不能有分数美分

  • 个人付款金额的总和仍然必须加到TotalPaid金额上。

设置数据:

declare  @t table
(
   id varchar(4) primary key, 
   Billed money not null
)


insert into @t
(
id,
billed
)
values
( 'A', 5),
( 'B', 3),
( 'C', 2)

 declare @TotalPaid money
 Set @TotalPaid = 3.33

这种方式不起作用

 SELECT
      ID, 
      Round(@TotalPaid * Billed / (Select Sum(Billed) from @t), 2)
 From
      @T

它将返回:

 A  1.67
 C  1
 D  0.67
    -----
    3.34 <--- Note the sum doesn't equal the Total Paid

我知道我可以通过游标或循环完成此操作,跟踪每一步的未分配金额,并确保在最后一项之后分配整个TotalPaid金额。

但是我希望有一种方法可以在没有循环或游标的情况下执行此操作。

这是我试图解决的问题的大大简化版本。实际数据超过100K行,光标方法非常慢。

3 个答案:

答案 0 :(得分:3)

认为这是一种可行的方法......

(将1作为ROUND的第三个参数传递,以确保舍入始终为向下,然后将构成余额的奇数0.01分配给舍入金额之差的那些理想的数量是最大的)

WITH t1
     AS (SELECT *,
                billed_adj = @TotalPaid * Billed / Sum(Billed) OVER(),
                billed_adj_trunc = ROUND(@TotalPaid * Billed / Sum(Billed) OVER(), 2, 1)
         FROM   @t)
SELECT id,
       billed,
       billed_adj_trunc + CASE
                            WHEN ROW_NUMBER() OVER (ORDER BY billed_adj - billed_adj_trunc DESC) 
                                           <= 100 * ( @TotalPaid - SUM(billed_adj_trunc) OVER() )
                              THEN 0.01
                            ELSE 0
                          END
FROM   t1
ORDER  BY id 

答案 1 :(得分:2)

这是一个使用递归common table expression

的(有些复杂)解决方案
;with cte as (
  select
      id
    , Paid = round(@TotalPaid * Billed / (Select Sum(Billed) from @t), 2,1)
    , Remainder =  @TotalPaid * Billed / (Select Sum(Billed) from @t)
           - round(@TotalPaid * Billed / (Select Sum(Billed) from @t), 2,1)
    , x.next_id
  from @t t
    outer apply (
      select top 1 next_id = i.id
      from @t as i
      where i.id > t.id
      order by i.id asc
    ) x
)
, r_cte as (
    --anchor row(s) / starting row(s)
  select 
      id
    , Paid
    , Remainder
    , next_id
  from cte t
  where not exists (
    select 1
    from cte as i
    where i.id < t.id
    )
  union all
  --recursion starts here
  select 
      c.id
    , c.Paid + round(c.Remainder + p.Remainder,2,1)
    , Remainder = c.Remainder + p.Remainder - round(c.Remainder + p.Remainder,2,1)
    , c.next_id
  from cte c
    inner join r_cte p
      on c.id = p.next_id
)
select id, paid 
from r_cte

rextester演示:http://rextester.com/MKLDX88496

返回:

+----+------+
| id | paid |
+----+------+
| A  | 1.66 |
| B  | 1.00 |
| C  | 0.67 |
+----+------+

答案 2 :(得分:0)

对于类似的内容,您无法应用完全分发;因为你已经显示了舍入结果总数超过收到的款项。

因此,你需要将“剩下的东西”分发给最终[收费],所以你需要做两件事......

  1. 确定当前行是否是该组中的最后一行。
  2. 确定已分配了多少付款。
  3. 您没有提供太多数据可供使用,因此以下情况并不理想,但这与您想要的一致......

    SELECT
        ID, 
        CASE WHEN lead(billed,1) OVER(ORDER BY (SELECT 1)) IS NULL THEN @TotalPaid - (sum(round(@TotalPaid * Billed / (Select Sum(Billed) from @t),2)) OVER(ORDER BY (SELECT 1) ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING))
            ELSE round(@TotalPaid * Billed / (Select Sum(Billed) from @t),2)
        END AS solution
    FROM
        @T;
    

    请注意,如果A,B,C具有更高的键,则这将组成“组”,因此您将相应地调整窗口函数。如果您可以提供更多样本数据和其他列等,我可以提出更优雅的解决方案。