Вычислить общее число выполняемых операций в 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 2012 можно использовать SUM () С OVER () предложения.

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

SQL Fiddle


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

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


оператор 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

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

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

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


предполагая, что 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