在T-SQL中执行此类计算逻辑的最佳方法

时间:2012-03-30 07:39:55

标签: sql sql-server database tsql

我的程序需要将一定数量传入查询以执行此类计算,但在我的情况下,它逐行循环并扣除正确的数量,我知道这不是一种有效的实现方式。所以我在这里寻求更好的方式。

PS:这只是我的草稿代码,很抱歉由于某些原因我无法发布完整的源代码。现在我重新构建了我的代码,使其更加完整和合理。

   --- the amount column is just for reference.

    insert into tbl1 (idx,amount,balance) values (1, 50, 50)
    insert into tbl1 (idx,amount,balance) values (2, 30, 30)
    insert into tbl1 (idx,amount,balance) values (3, 20, 20)
    insert into tbl1 (idx,amount,balance) values (4, 50, 50)
    insert into tbl1 (idx,amount,balance) values (5, 60, 60)


declare @total_value_to_deduct int
declare @cs_index int, @cs_balance int, @deduct_amount int

set @total_value_to_deduct = 130

declare csDeduct Cursor for select idx, balance from tbl1 where balance > 0 
open csDeduct fetch next from csDeduct into @cs_index, @cs_balance

while @@FETCH_STATUS = 0 and @total_value_to_deduct > 0
begin

   if @cs_balance >= @total_value_to_deduct  
    set @deduct_amount = @total_value_to_deduct
   else
    set @deduct_amount = @cs_balance

    -- contine deduct row by row if the total_value_to_deduct is not 0
    set @total_value_to_deduct = @total_value_to_deduct - @deduct_amount

    update tbl1 set balance = balance - @deduct_amount  where idx = @cs_index
    fetch next from csDeduct into @cs_index, @cs_balance
end

close csDeduct
deallocate csDeduct

预期结果:

idx          amount          balance
1              50               0
2              30               0
3              20               0
4              50              20
5              60              60

你必须得到你的帮助。感谢

5 个答案:

答案 0 :(得分:4)

修订版1:我添加了第三个解决方案

1)第一个解决方案(SQL2005 +; online query

DECLARE @tbl1 TABLE
(
    idx INT IDENTITY(2,2) PRIMARY KEY,
    amount INT NOT NULL,
    balance INT NOT NULL
);

INSERT INTO @tbl1 (amount,balance) VALUES (50, 50);
INSERT INTO @tbl1 (amount,balance) VALUES (30, 30);
INSERT INTO @tbl1 (amount,balance) VALUES (20, 20);
INSERT INTO @tbl1 (amount,balance) VALUES (50, 50);
INSERT INTO @tbl1 (amount,balance) VALUES (60, 60);


DECLARE @total_value_to_deduct INT;
SET @total_value_to_deduct = 130;

WITH CteRowNumber
AS
(
    SELECT  *, ROW_NUMBER() OVER(ORDER BY idx) AS RowNum
    FROM    @tbl1 a
),  CteRecursive
AS
(
    SELECT  a.idx, 
            a.amount,
            a.amount AS running_total, 
            CASE 
                WHEN a.amount <= @total_value_to_deduct THEN 0 
                ELSE a.amount - @total_value_to_deduct 
            END AS new_balance,
            a.RowNum
    FROM    CteRowNumber a
    WHERE   a.RowNum = 1
    --AND       a.amount < @total_value_to_deduct 
    UNION ALL
    SELECT  crt.idx, 
            crt.amount, 
            crt.amount + prev.running_total AS running_total,
            CASE 
                WHEN crt.amount + prev.running_total <= @total_value_to_deduct THEN 0 
                WHEN prev.running_total < @total_value_to_deduct AND crt.amount + prev.running_total > @total_value_to_deduct THEN crt.amount + prev.running_total - @total_value_to_deduct
                ELSE crt.amount 
            END AS new_balance, 
            crt.RowNum
    FROM    CteRowNumber crt
    INNER JOIN CteRecursive prev ON crt.RowNum = prev.RowNum + 1
    --WHERE prev.running_total < @total_value_to_deduct 
)
UPDATE  @tbl1 
SET     balance = b.new_balance
FROM    @tbl1 a

2)第二个解决方案(SQL2012)

UPDATE  @tbl1 
SET     balance = b.new_balance
FROM    @tbl1 a
INNER JOIN 
(
    SELECT  x.idx,
            SUM(x.amount) OVER(ORDER BY x.idx) AS running_total,
            CASE 
                WHEN SUM(x.amount) OVER(ORDER BY x.idx) <= @total_value_to_deduct THEN 0
                WHEN SUM(x.amount) OVER(ORDER BY x.idx) - x.amount < @total_value_to_deduct --prev_running_total < @total_value_to_deduct
                AND  SUM(x.amount) OVER(ORDER BY x.idx) > @total_value_to_deduct THEN SUM(x.amount) OVER(ORDER BY x.idx) - @total_value_to_deduct
                ELSE x.amount
            END AS new_balance
    FROM    @tbl1 x
)  b ON a.idx = b.idx;

3)第三个解决方案(SQ2000 +)使用triangular join

UPDATE  @tbl1 
SET     balance = d.new_balance
FROM    @tbl1 e
INNER JOIN
(
    SELECT  c.idx,
            CASE 
                WHEN c.running_total <= @total_value_to_deduct THEN 0
                WHEN c.running_total - c.amount < @total_value_to_deduct --prev_running_total < @total_value_to_deduct
                AND  c.running_total > @total_value_to_deduct THEN c.running_total - @total_value_to_deduct
                ELSE c.amount
            END AS new_balance
    FROM
    (
        SELECT  a.idx, 
                a.amount,
                (SELECT SUM(b.amount) FROM @tbl1 b WHERE b.idx <= a.idx) AS running_total
        FROM    @tbl1 a
    ) c
)d ON d.idx = e.idx;

答案 1 :(得分:1)

我很确定这个查询无论如何都不会起作用,因为&#34; index&#34;是一个关键字,所以应该用方括号括起来表示不是。

一般情况下,逐行执行任何操作并不是一个好主意。

如果我正确阅读,您可以将每个余额列设置为金额列减去@total_value_to_deduct变量,或者如果扣除金额会导致负值,则将其设置为0。如果这是真的那么为什么不直接对其进行计算呢?如果没有你发布任何预期的结果,我不能仔细检查我的逻辑,但如果我错了,请纠正我,并且它比这更复杂。

UPDATE tbl1
SET    balance = CASE
                   WHEN amount < @total_value_to_deduct THEN 0
                   ELSE amount - @total_value_to_deduct
                 END

编辑:好的,感谢编辑问题,现在更清楚了。您正在尝试按顺序获取所有帐户的总金额。我会看看我是否可以提出一个脚本来执行此操作并进一步编辑我的答案。

编辑#2:好的,我无法在不通过所有行进行交互的情况下找到一种方法(我尝试了递归CTE,但无法获得它工作)所以我用你原来的while循环完成了它。它有效地每行进行3次数据访问 - 我试图将其降为2但又没有运气。无论如何我都会发布它,以防它比你现在的速度快。这应该是您需要的所有代码(除了表创建/填充)。

DECLARE @id INT
SELECT @id = Min([index])
FROM   tbl1

WHILE @id IS NOT NULL
  BEGIN
      UPDATE tbl1
      SET    balance = CASE
                         WHEN amount < @total_value_to_deduct THEN 0
                         ELSE amount - @total_value_to_deduct
                       END
      FROM   tbl1
      WHERE  [index] = @id

      SELECT @total_value_to_deduct = CASE
                                        WHEN @total_value_to_deduct < amount THEN 0
                                        ELSE @total_value_to_deduct - amount
                                      END
      FROM   tbl1
      WHERE  [index] = @id

      SELECT @id = Min([index])
      FROM   tbl1
      WHERE  [index] > @id
 END

答案 2 :(得分:1)

以下是其中一种方法。它找到大于或等于请求数量的第一个运行总和,然后更新参与此总和的所有记录。在应该引入列“toDeduct”并且最初具有amount值的意义上,这可能应该以不同的方式编写。这将允许此更新适用于以前使用的数据集,因为toDeduct = 0意味着不能从此行中扣除任何内容。此外,toDeduct,idx上的索引将允许快速toDeduct&lt;&gt;您将使用0过滤器来减少无意义的搜索/更新次数。

declare @total_value_to_deduct int
set @total_value_to_deduct = 130

update tbl1
set balance = case when balance.idx = tbl1.idx 
           then balance.sumamount - @total_value_to_deduct
               else 0
         end
from tbl1 inner join
(
    select top 1 *
    from
    (
          select idx, (select sum (a.amount) 
                 from tbl1 a 
                where a.idx <= tbl1.idx) sumAmount
          from tbl1
    ) balance
      where balance.sumamount >= @total_value_to_deduct
      order by sumamount
) balance
  on tbl1.idx <= balance.idx

现在开始你的光标。只需声明游标fast_forward即可获得性能:

declare csDeduct Cursor local fast_forward
    for select idx, balance 
          from tbl1 
         where balance > 0 
         order by idx

你可能会重写fetch循环以避免重复fetch语句:

open csDeduct 
while 1 = 1
begin
   fetch next from csDeduct into @cs_index, @cs_balance
   if @@fetch_status <> 0
      break

    if @cs_balance >= @total_value_to_deduct  
       set @deduct_amount = @total_value_to_deduct
    else
       set @deduct_amount = @cs_balance

    -- contine deduct row by row if the total_value_to_deduct is not 0
    set @total_value_to_deduct = @total_value_to_deduct - @deduct_amount

    update tbl1 set balance = balance - @deduct_amount  where idx = @cs_index

end
close csDeduct 
deallocate csDeduct 

更改光标的选择部分更容易。

答案 3 :(得分:1)

如果您的索引没有差距,最简单的解决方案是

  • 创建一个递归CTE,从递归部分中扣除和递减它的值开始。
  • 使用CTE的结果更新实际表格

SQL声明

;WITH q AS (
  SELECT  idx, amount, balance, 130 AS Deduct
  FROM    tbl1
  WHERE   idx = 1
  UNION ALL
  SELECT  t.idx, t.amount, t.balance, q.Deduct - q.balance
  FROM    q
          INNER JOIN @tbl1 t ON t.idx = q.idx + 1  
  WHERE   q.Deduct - q.balance > 0 
)
UPDATE  @tbl1
SET     Balance = CASE WHEN q.Balance - q.Deduct > 0 THEN q.Balance - q.Deduct ELSE 0 END
FROM    q
        INNER JOIN tbl1 t ON t.idx = q.idx   

使用ROW_NUMBER可以缓解差距问题,但这会使查询复杂化。

;WITH r AS (
  SELECT  idx, amount, balance, rn = ROW_NUMBER() OVER (ORDER BY idx)
  FROM    tbl1
), q AS (
  SELECT  rn, amount, balance, 130 AS Deduct, idx
  FROM    r
  WHERE   rn = 1
  UNION ALL
  SELECT  r.rn, r.amount, r.balance, q.Deduct - q.balance, r.idx
  FROM    q
          INNER JOIN r ON r.rn = q.rn + 1  
  WHERE   q.Deduct - q.balance > 0 
)
UPDATE  tbl1
SET     Balance = CASE WHEN q.Balance - q.Deduct > 0 THEN q.Balance - q.Deduct ELSE 0 END
FROM    q
        INNER JOIN @tbl1 t ON t.idx = q.idx

测试脚本

DECLARE @tbl1 TABLE (idx INTEGER, Amount INTEGER, Balance INTEGER)
INSERT INTO @tbl1 (idx,amount,balance) VALUES (1, 50, 50)
INSERT INTO @tbl1 (idx,amount,balance) VALUES (2, 30, 30)
INSERT INTO @tbl1 (idx,amount,balance) VALUES (3, 20, 20)
INSERT INTO @tbl1 (idx,amount,balance) VALUES (4, 50, 50)
INSERT INTO @tbl1 (idx,amount,balance) VALUES (5, 60, 60)

;WITH q AS (
  SELECT  idx, amount, balance, 130 AS Deduct
  FROM    @tbl1
  WHERE   idx = 1
  UNION ALL
  SELECT  t.idx, t.amount, t.balance, q.Deduct - q.balance
  FROM    q
          INNER JOIN @tbl1 t ON t.idx = q.idx + 1  
  WHERE   q.Deduct - q.balance > 0 
)
UPDATE  @tbl1
SET     Balance = CASE WHEN q.Balance - q.Deduct > 0 THEN q.Balance - q.Deduct ELSE 0 END
FROM    q
        INNER JOIN @tbl1 t ON t.idx = q.idx

SELECT  *
FROM    @tbl1

<强>输出

idx Amount  Balance
1   50      0
2   30      0
3   20      0
4   50      20
5   60      60

答案 4 :(得分:0)

在表格中创建一个新列,每行包含前一个余额,然后您可以在INSERT / UPDATE上使用触发器为新插入的行创建余额。