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

Tsql 这可以在没有循环或光标的情况下完成吗,tsql,sql-server-2008-r2,Tsql,Sql Server 2008 R2,我有一个表格,列出了各个项目以及我们为它们支付的金额。我们收到的付款可能少于账单总金额。我想按照原始账单金额的比例将付款分配给每个项目 这是棘手的部分 每个个人支付的金额不能有零分 个人支付金额的总和仍必须等于总支付金额 设置数据: 这样不行 它将返回: A 1.67 C 1 D 0.67 ----- 3.34 <--- Note the sum doesn't equal the Total Paid 我知道我可以通过游标或循环来实现这一点,在每一步跟踪未分

我有一个表格,列出了各个项目以及我们为它们支付的金额。我们收到的付款可能少于账单总金额。我想按照原始账单金额的比例将付款分配给每个项目

这是棘手的部分

每个个人支付的金额不能有零分

个人支付金额的总和仍必须等于总支付金额

设置数据:

这样不行

它将返回:

 A  1.67
 C  1
 D  0.67
    -----
    3.34 <--- Note the sum doesn't equal the Total Paid
我知道我可以通过游标或循环来实现这一点,在每一步跟踪未分配的金额,并确保在最后一项之后分配全部已支付金额

然而,我希望有一种不用循环或游标的方法来实现这一点


这是我试图解决的问题的一个大大简化的版本。实际数据有超过10万行,而游标方法非常慢

对于这种情况,您无法应用精确分布;正如您已经显示的那样,四舍五入的结果超过了收到的付款总额

因此,您需要将剩余的内容分发给最终[账单],因此您需要做两件事

确定当前行是否为该组中的最后一行。 确定已经分配了多少付款。 在这里,您没有提供太多的数据来处理,因此下面的内容并不理想,但是这与您想要的内容大致相同

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具有更高的键,这将构成组,因此您可以相应地调整窗口功能。如果你能提供更多的带有附加列的示例数据等,我可能会想出一个更优雅的解决方案。

这里是一个使用递归的有点复杂的解决方案

我认为这是一个可行的方法

将1作为第三个参数传递给舍入,以确保舍入始终向下,然后将构成余额的奇数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.665到1.67。我需要将每个分配四舍五入为一便士。我不能在一次分配中有零分,2008-r2Ah当然没有领先优势,直到2012年才引入;很抱歉错过了2008R2,在这里,我认为我对递归cte很聪明。不错!
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;
;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
+----+------+
| id | paid |
+----+------+
| A  | 1.66 |
| B  | 1.00 |
| C  | 0.67 |
+----+------+
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