Рассчитать промежуточную сумму в SQL Server - PullRequest
155 голосов
/ 14 мая 2009

Представьте себе следующую таблицу (называемую TestTable):

id     somedate    somevalue
--     --------    ---------
45     01/Jan/09   3
23     08/Jan/09   5
12     02/Feb/09   0
77     14/Feb/09   7
39     20/Feb/09   34
33     02/Mar/09   6

Я бы хотел запрос, который возвращает промежуточную сумму в порядке дат, например:

id     somedate    somevalue  runningtotal
--     --------    ---------  ------------
45     01/Jan/09   3          3
23     08/Jan/09   5          8
12     02/Feb/09   0          8
77     14/Feb/09   7          15  
39     20/Feb/09   34         49
33     02/Mar/09   6          55

Я знаю, что есть различных способов сделать это в SQL Server 2000/2005/2008.

Меня особенно интересует метод такого типа, который использует трюк с агрегирующим набором операторов:

INSERT INTO @AnotherTbl(id, somedate, somevalue, runningtotal) 
   SELECT id, somedate, somevalue, null
   FROM TestTable
   ORDER BY somedate

DECLARE @RunningTotal int
SET @RunningTotal = 0

UPDATE @AnotherTbl
SET @RunningTotal = runningtotal = @RunningTotal + somevalue
FROM @AnotherTbl

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

Но, может быть, есть другие способы, которые люди могут предложить?

edit: теперь с SqlFiddle с настройкой и приведенным выше примером «трюка обновления»

Ответы [ 15 ]

121 голосов
/ 14 мая 2009

Обновление , если вы работаете с SQL Server 2012, см .: https://stackoverflow.com/a/10309947

Проблема в том, что реализация SQL Server предложения Over имеет несколько ограниченный .

Oracle (и ANSI-SQL) позволяют вам делать такие вещи, как:

 SELECT somedate, somevalue,
  SUM(somevalue) OVER(ORDER BY somedate 
     ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) 
          AS RunningTotal
  FROM Table

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

Трюк с обновлением удобен, но я чувствую его довольно хрупким. Кажется, что если вы обновляете полную таблицу, то она будет действовать в порядке первичного ключа. Поэтому, если вы установите дату в качестве первичного ключа по возрастанию, вы probably будете в безопасности. Но вы полагаетесь на недокументированные детали реализации SQL Server (также, если запрос завершается выполнением двумя процессами, интересно, что произойдет, см .: MAXDOP):

Полный рабочий образец:

drop table #t 
create table #t ( ord int primary key, total int, running_total int)

insert #t(ord,total)  values (2,20)
-- notice the malicious re-ordering 
insert #t(ord,total) values (1,10)
insert #t(ord,total)  values (3,10)
insert #t(ord,total)  values (4,1)

declare @total int 
set @total = 0
update #t set running_total = @total, @total = @total + total 

select * from #t
order by ord 

ord         total       running_total
----------- ----------- -------------
1           10          10
2           20          30
3           10          40
4           1           41

Вы просили эталонный тест, это - низкий уровень.

Самый быстрый БЕЗОПАСНЫЙ способ сделать это - Курсор, он на порядок быстрее, чем коррелированный подзапрос перекрестного соединения.

Абсолютно быстрый способ - трюк ОБНОВЛЕНИЯ. Меня беспокоит только то, что я не уверен, что при любых обстоятельствах обновление будет происходить линейным образом. В запросе нет ничего, что прямо говорит об этом.

Итог, для производственного кода я бы пошел с курсором.

Данные испытаний:

create table #t ( ord int primary key, total int, running_total int)

set nocount on 
declare @i int
set @i = 0 
begin tran
while @i < 10000
begin
   insert #t (ord, total) values (@i,  rand() * 100) 
    set @i = @i +1
end
commit

Тест 1:

SELECT ord,total, 
    (SELECT SUM(total) 
        FROM #t b 
        WHERE b.ord <= a.ord) AS b 
FROM #t a

-- CPU 11731, Reads 154934, Duration 11135 

Тест 2:

SELECT a.ord, a.total, SUM(b.total) AS RunningTotal 
FROM #t a CROSS JOIN #t b 
WHERE (b.ord <= a.ord) 
GROUP BY a.ord,a.total 
ORDER BY a.ord

-- CPU 16053, Reads 154935, Duration 4647

Тест 3:

DECLARE @TotalTable table(ord int primary key, total int, running_total int)

DECLARE forward_cursor CURSOR FAST_FORWARD 
FOR 
SELECT ord, total
FROM #t 
ORDER BY ord


OPEN forward_cursor 

DECLARE @running_total int, 
    @ord int, 
    @total int
SET @running_total = 0

FETCH NEXT FROM forward_cursor INTO @ord, @total 
WHILE (@@FETCH_STATUS = 0)
BEGIN
     SET @running_total = @running_total + @total
     INSERT @TotalTable VALUES(@ord, @total, @running_total)
     FETCH NEXT FROM forward_cursor INTO @ord, @total 
END

CLOSE forward_cursor
DEALLOCATE forward_cursor

SELECT * FROM @TotalTable

-- CPU 359, Reads 30392, Duration 496

Тест 4:

declare @total int 
set @total = 0
update #t set running_total = @total, @total = @total + total 

select * from #t

-- CPU 0, Reads 58, Duration 139
109 голосов
/ 25 апреля 2012

В SQL Server 2012 можно использовать SUM () с предложением OVER () .

select id,
       somedate,
       somevalue,
       sum(somevalue) over(order by somedate rows unbounded preceding) as runningtotal
from TestTable

SQL Fiddle

40 голосов
/ 06 декабря 2012

Хотя Сэм Саффрон проделал большую работу над этим, он все еще не предоставил рекурсивный общий табличный выражение код для этой проблемы. А для нас, которые работают с SQL Server 2008 R2, а не с Denali, это все еще самый быстрый способ подвести итоги, он примерно в 10 раз быстрее, чем курсор на моем рабочем компьютере для 100000 строк, и это также встроенный запрос.
Итак, вот оно (я предполагаю, что в таблице есть столбец ord и его порядковый номер без пробелов, для быстрой обработки также должно быть уникальное ограничение на это число):

;with 
CTE_RunningTotal
as
(
    select T.ord, T.total, T.total as running_total
    from #t as T
    where T.ord = 0
    union all
    select T.ord, T.total, T.total + C.running_total as running_total
    from CTE_RunningTotal as C
        inner join #t as T on T.ord = C.ord + 1
)
select C.ord, C.total, C.running_total
from CTE_RunningTotal as C
option (maxrecursion 0)

-- CPU 140, Reads 110014, Duration 132

sql fiddle demo

обновление Мне также было интересно узнать об этом обновлении с переменной или необычном обновлении . Обычно это работает нормально, но как мы можем быть уверены, что это работает каждый раз? хорошо, вот небольшой трюк (нашел его здесь - http://www.sqlservercentral.com/Forums/Topic802558-203-21.aspx#bm981258) - вы просто проверяете текущий и предыдущий ord и используете 1/0 назначение в случае, если они отличаются от того, что вы ожидали:

declare @total int, @ord int

select @total = 0, @ord = -1

update #t set
    @total = @total + total,
    @ord = case when ord <> @ord + 1 then 1/0 else ord end,
    ------------------------
    running_total = @total

select * from #t

-- CPU 0, Reads 58, Duration 139

Из того, что я видел, если у вас есть правильный кластеризованный индекс / первичный ключ в вашей таблице (в нашем случае это будет индекс на ord_id), обновление будет происходить линейно все время (никогда не встречалось деление на ноль). ). Тем не менее, вам решать, хотите ли вы использовать его в рабочем коде:)

28 голосов
/ 05 июня 2009

Оператор APPLY в SQL 2005 и выше работает для этого:

select
    t.id ,
    t.somedate ,
    t.somevalue ,
    rt.runningTotal
from TestTable t
 cross apply (select sum(somevalue) as runningTotal
                from TestTable
                where somedate <= t.somedate
            ) as rt
order by t.somedate
11 голосов
/ 14 мая 2009
SELECT TOP 25   amount, 
    (SELECT SUM(amount) 
    FROM time_detail b 
    WHERE b.time_detail_id <= a.time_detail_id) AS Total FROM time_detail a

Вы также можете использовать функцию ROW_NUMBER () и временную таблицу для создания произвольного столбца, который будет использоваться при сравнении внутреннего оператора SELECT.

6 голосов
/ 13 сентября 2014

Использовать коррелированный подзапрос. Очень просто, вот и все:

SELECT 
somedate, 
(SELECT SUM(somevalue) FROM TestTable t2 WHERE t2.somedate<=t1.somedate) AS running_total
FROM TestTable t1
GROUP BY somedate
ORDER BY somedate

Код может быть не совсем правильным, но я уверен, что идея такова.

GROUP BY - в случае, если дата появляется более одного раза, вы бы хотели видеть ее только один раз в наборе результатов.

Если вы не против увидеть повторяющиеся даты или хотите увидеть исходное значение и идентификатор, то вам нужно следующее:

SELECT 
id,
somedate, 
somevalue,
(SELECT SUM(somevalue) FROM TestTable t2 WHERE t2.somedate<=t1.somedate) AS running_total
FROM TestTable t1
ORDER BY somedate
5 голосов
/ 05 июня 2009

Вы также можете денормализовать - сохранить промежуточные итоги в той же таблице:

http://sqlblog.com/blogs/alexander_kuznetsov/archive/2009/01/23/denormalizing-to-enforce-business-rules-running-totals.aspx

Выбирает работу намного быстрее, чем любые другие решения, но изменения могут быть медленнее

4 голосов
/ 30 августа 2017

Если вы используете Sql server 2008 R2 выше. Тогда это был бы самый короткий способ сделать;

Select id
    ,somedate
    ,somevalue,
LAG(runningtotal) OVER (ORDER BY somedate) + somevalue AS runningtotal
From TestTable 

LAG используется для получения значения предыдущей строки. Вы можете сделать Google для получения дополнительной информации.

[1]:

4 голосов
/ 14 мая 2009

Предполагая, что управление окнами работает на SQL Server 2008 так же, как и в других местах (которые я пробовал), попробуйте:

select testtable.*, sum(somevalue) over(order by somedate)
from testtable
order by somedate;

MSDN говорит, что он доступен в SQL Server 2008 (а может быть, и в 2005 году?), Но у меня нет экземпляра, чтобы попробовать его.

РЕДАКТИРОВАТЬ: ну, очевидно, SQL Server не позволяет спецификацию окна ("OVER (...)") без указания "PARTITION BY" (разделение результата на группы, но без агрегирования, как это делает GROUP BY ). Раздражает - ссылка на синтаксис MSDN предполагает, что это необязательно, но в данный момент у меня есть только экземпляры SqlServer 2000.

Запрос, который я дал, работает как в Oracle 10.2.0.3.0, так и в PostgreSQL 8.4-beta. Так скажи MS наверстать;)

2 голосов
/ 25 февраля 2018

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

Select id, someday, somevalue, (select sum(somevalue) 
                                from testtable as t2
                                where t2.id = t1.id
                                and t2.someday <= t1.someday) as runningtotal
from testtable as t1
order by id,someday;
...