Лучший способ выполнить такую ​​логику вычислений в T-SQL - PullRequest
6 голосов
/ 30 марта 2012

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

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 ]

4 голосов
/ 30 марта 2012

Редакция 1: я добавил третье решение

1) Первое решение (SQL2005 +; онлайн-запрос )

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 +) использует треугольное соединение :

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 голос
/ 30 марта 2012

Если в ваших индексах нет пробелов, простейшим решением будет

  • Создать рекурсив 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
1 голос
/ 30 марта 2012

Я почти уверен, что этот запрос все равно не будет работать, так как "index" - это ключевое слово, поэтому его следует заключить в квадратные скобки, чтобы указать обратное.

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

Если я правильно понял, вы устанавливаете для каждого столбца баланса столбец суммы минус переменную @total_value_to_deduct или устанавливаете его в 0, есливычеты привели бы к отрицательной сумме.Если это правда, то почему бы просто не сделать расчеты по этому вопросу напрямую?Без того, что вы публикуете ожидаемые результаты, я не могу дважды проверить мою логику, но, пожалуйста, поправьте меня, если я ошибаюсь, и это более сложно, чем это.для редактирования вопроса теперь более понятно.Вы пытаетесь взять общую сумму по всем счетам последовательно.Я посмотрю, смогу ли я придумать сценарий для этого и отредактировать свой ответ дальше.

Edit # 2: ОК, я не могу найти способ сделать это безпросматривая все строки (я попробовал рекурсивный CTE, но не смог заставить его работать), поэтому я сделал это с циклом while, как вы делали изначально.Это эффективно делает 3 доступа к данным в строке, хотя - я попытался свалить это до 2, но снова не повезло.Я выкладываю это в любом случае, если это будет быстрее, чем у вас сейчас.Это должен быть весь необходимый код (кроме таблицы create / populate).

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
1 голос
/ 30 марта 2012

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...