2009-05-13 3 views
128

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

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 

I знаете, что в SQL Server 2000/2005/2008 есть various ways of doing this.

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

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 оператор будет обрабатывать строки в правильном порядке. Возможно, мы сможем получить окончательные ответы на этот вопрос.

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

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

+0

http://blogs.msdn.com/sqltips/archive/2005/07/20/441053.aspx Добавить заказ к вашему обновлению ... установить и получить гарантию. –

+0

Но заказ не может быть применен к заявлению UPDATE ... не так ли? – codeulike

+0

Правильно, моя ошибка. –

ответ

103

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

Проблема заключается в том, что реализация SQL Server клаузулы Over является somewhat limited.

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 

You попросил бенчмарк, это низкая оценка.

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

Абсолютным самым быстрым способом является трюк UPDATE. Меня беспокоит только то, что я не уверен, что при любых обстоятельствах обновление будет происходить линейным образом. В запросе явно ничего не говорится.

Нижняя линия, для производственного кода Я бы пошел с помощью курсора.

Тестовые данные:

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 
+1

Спасибо. Таким образом, ваш образец кода должен продемонстрировать, что он будет суммировать в порядке первичного ключа, я полагаю. Было бы интересно узнать, являются ли курсоры еще более эффективными, чем объединения больших наборов данных. – codeulike

+0

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

+0

@ sambo99 - Спасибо за все дополнительные подробности – codeulike

9
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.

+1

Это действительно неэффективно ... но опять же нет реального чистый способ сделать это на сервере sql –

+0

Абсолютно он неэффективен - но он выполняет эту работу, и нет сомнений в том, что-то для выполнения в правильном или неправильном порядке. –

+0

спасибо, полезно иметь альтернативные ответы, а также полезно иметь эффективную критику – codeulike

3

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

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

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

EDIT: ну, по-видимому, SQL Server не разрешает спецификацию окна («OVER (...)») без указания «PARTITION BY» (деление результата на группы, но не агрегирование в полном объеме GROUP BY делает). Раздражающее - ссылка на синтаксис MSDN предполагает, что он необязательный, но на данный момент у меня только экземпляры SqlServer 2000.

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

+2

Использование OVER с SUM не будет работать в этом случае, чтобы дать общее количество. Предложение OVER не принимает ORDER BY при использовании с SUM. Вы должны использовать PARTITION BY, который не будет работать для выполнения итогов. –

+0

спасибо, его действительно полезно услышать, почему это не работает. araqnid, может быть, вы можете отредактировать свой ответ, чтобы объяснить, почему его не вариант – codeulike

+0

[Похоже, что в SQL Server 2011] (http://twitter.com/#!/AdamMachanic/status/70868079046963200) –

2

Ниже приводятся требуемые результаты.

SELECT a.SomeDate, 
     a.SomeValue, 
     SUM(b.SomeValue) AS RunningTotal 
FROM TestTable a 
CROSS JOIN TestTable b 
WHERE (b.SomeDate <= a.SomeDate) 
GROUP BY a.SomeDate,a.SomeValue 
ORDER BY a.SomeDate,a.SomeValue 

Наличие кластерного индекса на SomeDate значительно улучшит производительность.

+0

@Dave Я думаю, что этот вопрос пытается найти эффективный способ сделать это, кросс-соединение будет очень медленным для больших наборов –

+0

спасибо, полезно иметь альтернативные ответы, а также полезно иметь эффективную критику – codeulike

24

Применить оператор в 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 
+4

Очень хорошо работает для меньших наборов данных. Недостатком является то, что вам придется иметь одинаковые предложения о внутреннем и внешнем запросах. – Sire

+0

Поскольку некоторые из моих дат были точно такими же (вплоть до доли секунды), мне пришлось добавить: row_number() over (order by txndate) во внутреннюю и внешнюю таблицу и несколько составных индексов, чтобы заставить ее работать. Легкое/простое решение. BTW, проверенный крест применим к подзапросу ... он немного быстрее. – pghcpa

+0

это очень чистый и хорошо работает с небольшими наборами данных; быстрее, чем рекурсивный CTE – jtate

1

Я считаю, что общая сумма может быть достигнута с помощью простого INNER JOI N ниже.

SELECT 
    ROW_NUMBER() OVER (ORDER BY SomeDate) AS OrderID 
    ,rt.* 
INTO 
    #tmp 
FROM 
    (
     SELECT 45 AS ID, CAST('01-01-2009' AS DATETIME) AS SomeDate, 3 AS SomeValue 
     UNION ALL 
     SELECT 23, CAST('01-08-2009' AS DATETIME), 5 
     UNION ALL 
     SELECT 12, CAST('02-02-2009' AS DATETIME), 0 
     UNION ALL 
     SELECT 77, CAST('02-14-2009' AS DATETIME), 7 
     UNION ALL 
     SELECT 39, CAST('02-20-2009' AS DATETIME), 34 
     UNION ALL 
     SELECT 33, CAST('03-02-2009' AS DATETIME), 6 
    ) rt 

SELECT 
    t1.ID 
    ,t1.SomeDate 
    ,t1.SomeValue 
    ,SUM(t2.SomeValue) AS RunningTotal 
FROM 
    #tmp t1 
    JOIN #tmp t2 
     ON t2.OrderID <= t1.OrderID 
GROUP BY 
    t1.OrderID 
    ,t1.ID 
    ,t1.SomeDate 
    ,t1.SomeValue 
ORDER BY 
    t1.OrderID 

DROP TABLE #tmp 
+0

Да, я думаю, что это эквивалентно «Тесту 3» в Sam Saffron's ответ. – codeulike

96

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

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

SQL Fiddle

36

В то время как Сэм Шафран сделал большую работу на нем, он до сих пор не предоставил Рекурсивного общие табличного выражение кода для этой проблемы.А для нас, работающих с 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) обновление будет происходить линейным образом все время (никогда не встречающееся деление на ноль). Тем не менее, вам решать, хотите ли вы использовать его в производственном коде :)

+6

Этот ответ заслуживает большего признания (или, может быть, у него есть некоторые недостатки, которые я не вижу?) – user1068352

+0

должно быть последовательное число, чтобы вы могли присоединиться к ord = ord + 1, и иногда ему нужно немного больше работы. Но в любом случае, на SQL 2008 R2 я использую это решение –

+0

+1 На SQLServer2008R2 я также предпочитаю подход с рекурсивным CTE. FYI, чтобы найти значение для таблиц, которые допускают пробелы, я использую коррелированный подзапрос. Он добавляет две дополнительные операции поиска к запросу http://sqlfiddle.com/#!3/d41d8/18967 –

4

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

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

Спасибо ... просто было здорово. Был добавлен индекс для повышения производительности, но это было sim (взяв одну из рекомендаций от Advisor Engine Tuning Advisor;), а затем она запускается как выстрел. –

1

Использование присоединиться Другим вариантом является использование соединения. Теперь запрос может выглядеть следующим образом:

SELECT a.id, a.value, SUM(b.Value)FROM RunTotalTestData a, 
    RunTotalTestData b 
    WHERE b.id <= a.id 
    GROUP BY a.id, a.value 
    ORDER BY a.id; 

для больше вы можете аут этой ссылки http://askme.indianyouth.info/details/calculating-simple-running-totals-in-sql-server-12

0
BEGIN TRAN 
CREATE TABLE #Table (_Id INT IDENTITY(1,1) ,id INT , somedate VARCHAR(100) , somevalue INT) 


INSERT INTO #Table (id , somedate , somevalue ) 
SELECT 45 , '01/Jan/09', 3 UNION ALL 
SELECT 23 , '08/Jan/09', 5 UNION ALL 
SELECT 12 , '02/Feb/09', 0 UNION ALL 
SELECT 77 , '14/Feb/09', 7 UNION ALL 
SELECT 39 , '20/Feb/09', 34 UNION ALL 
SELECT 33 , '02/Mar/09', 6 

;WITH CTE (_Id, id , _somedate , _somevalue ,_totvalue) AS 
(

SELECT _Id , id , somedate , somevalue ,somevalue 
FROM #Table WHERE _id = 1 
UNION ALL 
SELECT #Table._Id , #Table.id , somedate , somevalue , somevalue + _totvalue 
FROM #Table,CTE 
WHERE #Table._id > 1 AND CTE._Id = (#Table._id-1) 
) 

SELECT * FROM CTE 

ROLLBACK TRAN 
+0

Возможно, вам следует дать некоторую информацию о том, что вы здесь делаете, и отметить любые преимущества/недостатки этого конкретного метода. –

1

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

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

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

[1]:

+0

Я считаю, что [LAG] (https://docs.microsoft.com/en-us/sql/t-sql/functions/lag-transact-sql) существует только в SQL Server 2012 и выше (не в 2008 году) – AaA

+0

Использование LAG() не улучшается на 'SUM (somevalue) OVER (...)', который кажется мне намного более чистым –

0

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

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