2012-03-30 4 views
6

Моя программа должна передать сумму в запрос для выполнения такого расчета, но в моем случае она циклически перебирается по строке за строкой и вычитает правильную сумму, я знаю это не является эффективным способом реализации. поэтому я здесь, чтобы искать лучший способ.Лучший способ выполнить такую ​​логику вычислений в T-SQL

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 

Ваша помощь должна оценить. thank

+2

+1 для подачи полный образец кода. (Хотя 'while @total_value_to_deduct <= 0' не имеет никакого смысла, так как оно начинается с 0) – Tomalak

+0

второстепенная точка:' close' и 'deallocate' также являются неправильными. – Jamiec

+0

извините за это .. я уже изменил соответственно – skywills

ответ

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; 
+0

+1 - намного лучше, чем мое решение, пытался сделать рекурсивный CTE, но не смог заставить его работать. – Bridge

+0

Вы уверены, что в последовательном столбце нет пробелов? –

+0

Нет, я думаю, нет - я смотрел только на данные образца.Я также вижу, что он переименовал его, поэтому я предполагаю, что схема не исправлена! Я редактировал свой предыдущий комментарий. – Bridge

0

Создайте новый столбец в таблице с предыдущим балансом для каждой строки, затем вы можете использовать триггер на INSERT/UPDATE, чтобы создать баланс для вновь вставленной строки.

1

Я уверен, что этот запрос не будет работать в любом случае, так как «индекс» является ключевым словом и поэтому должен быть заключен в квадратные скобки для указания иначе.

В целом, это нехорошо делать что-либо поэтапно для производительности.

Если я правильно его читаю, вы устанавливаете каждую колонку баланса в столбец суммы минус переменная @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 

Edit: ОК спасибо за правку вопроса это более ясно. Вы пытаетесь взять общую сумму по всем учетным записям последовательно. Я посмотрю, могу ли я придумать сценарий для этого и отредактировать свой ответ дальше.

Редактировать # 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 
+0

извините за это. я переписал код, чтобы сделать его более полным и разумным. – skywills

+0

Спасибо, я думаю, теперь понимаю. Я попробую еще раз. – Bridge

+0

да..и пытаюсь взять общую сумму по всем счетам последовательно. спасибо за ваши попытки! – skywills

1

Вот один из способов сделать это. Он находит первую текущую сумму, большую или равную запрашиваемой сумме, а затем обновляет все записи, участвующие в этой сумме. Вероятно, это должно быть написано по-разному в том смысле, что нужно ввести столбец «toDeduct» и изначально иметь значение суммы. Это позволит этому обновлению работать над ранее используемыми наборами данных, потому что toDeduct = 0 означает, что из этой строки ничего нельзя вычесть. Кроме того, индекс на toDeduct, idx позволит быстро удалить Deduct <> 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 

И вы можете переписать цикл выборки, чтобы избежать повторения выборки заявления:

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 

Делает изменение выбора части курсора немного проще.

+0

Извините за мое плохое понимание умения, не возражаете, чтобы опубликовать полный код для части курсора с помощью «...» для моего лучшего понимания. – skywills

+0

Я расширил цикл выборки. Но - обновление, которое я разместил, выполняет ту же работу, что и курсор. Вы можете проверить его немного, чтобы увидеть, есть ли прирост производительности. –

1

Если индексы не имеют пробелов, самое простое решение было бы

  • Создать рекурсивную CTE, начиная с величиной, чтобы вычитать и декремент его в рекурсивной части.
  • Используйте результаты CTE обновить ваш фактический стол

SQL Statement

;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 
Смежные вопросы