Вычислить общее число выполняемых операций в SQL Server
Представьте следующую таблицу (называется 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 - ...
меня особенно интересует такой метод, который использует трюк агрегирования-set-statement:
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 с настройкой и примером "трюк обновления" выше
14 ответов
обновление, если вы используете 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
вы попросили ориентир это lowdown.
самый быстрый безопасный способ сделать это был бы курсор, это на порядок быстрее, чем коррелированный подзапрос перекрестного соединения.
самый быстрый способ-это трюк обновления. Меня беспокоит только то, что я не уверен, что при всех обстоятельствах обновление будет продолжаться линейным образом. В запросе нет ничего, что прямо так и говорит.
нижняя строка, для производственного кода я бы пошел с курсором.
тестовые данные:
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
хотя Сэм Шафран проделал большую работу над этим, он все еще не предоставил рекурсивное общее табличное выражение код для этой проблемы. И для нас, кто работает с SQL Server 2008 R2, а не Denali, это все еще самый быстрый способ запустить total, это примерно в 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
обновление
Мне также было любопытно об этом обновление с переменной или ушлый обновление. Обычно это работает нормально, но как мы можем быть уверены, что это работает каждый раз? Ну, вот небольшой трюк (нашел его здесь - 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
) обновление будет продолжаться линейным способом все время (никогда не встречалось деление на ноль). Тем не менее, это до вас, чтобы решить, если вы хотите использовать его в производственном коде:)
оператор 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
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.
использовать коррелированный подзапрос. Очень просто, вот так:
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
код может быть не совсем правильно, но я уверен, что идея.
группа 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
вы также можете денормализовать-хранить текущие итоги в той же таблице:
выбирает работу намного быстрее, чем любые другие решения, но модификации могут быть медленнее
предполагая, что windowing работает на 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-beta. Так что скажите MS догнать;)
ниже приведены необходимые результаты.
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 значительно улучшит производительность.
Если вы используете Sql server 2008 R2 выше. Тогда это будет самый короткий путь;
Select id
,somedate
,somevalue,
LAG(runningtotal) OVER (ORDER BY somedate) + somevalue AS runningtotal
From TestTable
ЛАГ используется для получения предыдущего значения строки. Вы можете сделать Google для получения дополнительной информации.
[1]:
Я считаю, что бегущая сумма может быть достигнута с помощью простой операции внутреннего соединения ниже.
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
использование join Другой вариант-использовать join. Теперь запрос может выглядеть так:
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
хотя лучший способ сделать это-использовать функцию окна, это также можно сделать с помощью простого коррелированный подзапрос.
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;
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