SQL Server에서 누계 계산


170

다음 표 ( 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명령문이 올바른 순서로 행을 처리 한다고 반드시 보장 할 수는 없기 때문에이 문제가 있다고 들었습니다 . 아마도 우리는 그 문제에 대한 확실한 대답을 얻을 수있을 것입니다.

그러나 사람들이 제안 할 수있는 다른 방법이 있습니까?

편집 : 이제 설정 및 위의 '업데이트 트릭'예가 있는 SqlFiddle


blogs.msdn.com/sqltips/archive/2005/07/20/441053.aspx 업데이트에 주문 추가 ... 세트하면 보증을받습니다.
Simon D

그러나 Order by를 UPDATE 문에 적용 할 수는 없습니까?
codeulike 2009

특히 SQL Server 2012를 사용하는 경우 sqlperformance.com/2012/07/t-sql-queries/running-totals 도 참조하십시오 .
Aaron Bertrand

답변:


133

SQL Server 2012를 실행중인 경우 업데이트 : https://stackoverflow.com/a/10309947

문제는 Over 절의 SQL Server 구현이 다소 제한적이라는 것 입니다.

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

당신은 벤치 마크를 요청했습니다. 이것은 낮습니다.

이 작업을 수행하는 가장 빠른 SAFE 방법은 커서가되며 상호 결합의 하위 쿼리보다 상관성이 훨씬 빠릅니다.

가장 빠른 방법은 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

1
방금 CTE @Martin을 테스트했지만 업데이트 트릭에 가까운 것은 없습니다. 다음은 프로파일 러 트레이스입니다. i.stack.imgur.com/BbZq3.png
Sam Saffron


1
이 답변에 넣은 모든 작업에 대해 +1-업데이트 옵션이 마음에 듭니다. 이 업데이트 스크립트에 파티션을 만들 수 있습니까? 예를 들어 "Car Colour"라는 추가 필드가있는 경우이 스크립트가 각 "Car Colour"파티션 내에서 누적 합계를 반환 할 수 있습니까?
whytheq

2
초기 (Oracle (및 ANSI-SQL)) 답변은 이제 SQL Server 2017에서 작동합니다. 감사합니다.
DaniDev


40

Sam Saffron은 훌륭한 작업을 수행했지만 이 문제에 대한 재귀 공통 테이블 표현식 코드는 제공하지 않았습니다 . 그리고 Denali가 아닌 SQL Server 2008 R2를 사용하는 우리에게는 여전히 총 ​​실행 속도가 가장 빠르며 작업 컴퓨터의 커서보다 10 만 줄 더 빠르며 인라인 쿼리입니다.
따라서 여기에 있습니다 ( 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) 업데이트는 항상 선형 방식으로 진행됩니다 (0으로 나누지 않음). 즉, 프로덕션 코드에서 사용할 것인지 결정하는 것은 당신에게 달려 있습니다 :)

업데이트 2 이 답변을 연결하고 있는데, 기발한 업데이트 -nvarchar 연결 / 인덱스 / nvarchar (max) 설명 할 수없는 동작의 신뢰성에 대한 유용한 정보가 포함되어 있습니다 .


6
이 답변은 더 많은 인정을받을 가치가 있습니다 (또는 아마 보지 못하는 결함이 있습니까?)
user1068352

ord = ord + 1에 참여할 수 있도록 순차적 인 숫자가 있어야하며 때로는 더 많은 작업이 필요합니다. 그러나 어쨌든 SQL 2008 R2에서는이 솔루션을 사용하고 있습니다.
Roman Pekar

+1 SQLServer2008R2에서는 재귀 적 CTE를 사용하는 접근 방식을 선호합니다. 참고로, 테이블의 값을 찾기 위해 간격을 허용하는 상관 하위 쿼리를 사용합니다. 이 쿼리에 두 개의 추가 찾는 작업을 추가 sqlfiddle.com/#!3/d41d8/18967
알렉산드르 Fedorenko에게

2
데이터에 대한 서 수가 이미 있고 SQL 2008 R2에서 간결한 (커서가 아닌) 세트 기반 솔루션을 찾고있는 경우에는 완벽한 것으로 보입니다.
Nick.McDermaid

1
모든 실행중인 총 쿼리에 연속 서수 필드가있는 것은 아닙니다. 때때로 날짜 / 시간 필드가 있거나 정렬 도중에 레코드가 삭제 된 경우가 있습니다. 그것이 더 자주 사용되지 않는 이유 일 수 있습니다.
Reuben

28

SQL 2005 이상의 APPLY 연산자는 다음과 같이 작동합니다.

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

5
소규모 데이터 세트에 매우 적합합니다. 단점은 내부 및 외부 쿼리에서 where 절이 동일해야한다는 것입니다.
Sire

내 날짜 중 일부가 정확히 똑같 았기 때문에 (초 단위로) 내외부 테이블에 row_number () over (txndate 순서)와 몇 가지 복합 인덱스를 추가해야했습니다. 매끄러운 / 간단한 솔루션. BTW, 하위 쿼리에 대해 교차 테스트 적용 ... 약간 빠릅니다.
pghcpa

이것은 매우 깨끗하며 작은 데이터 세트에서 잘 작동합니다. 빠른 재귀 CTE 이상
jtate

이 방법은 (소규모 데이터 세트의 경우) 훌륭한 솔루션이지만 열이 고유하다는 것을 의미해야합니다
Roman

11
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 서버에서 이것을하는 확실한 방법이 없습니다
Sam Saffron

당연히 비효율적이지만 작업을 수행하며 올바른 순서로 실행할 것인지 또는 잘못된 순서로 실행할 것인지에 대해서는 의문의 여지가 없습니다.
Sam Ax

efficienty 비판이 덕분에, 자사의 다른 답변이 유용하고, 또한 유용한
codeulike

7

상관 된 하위 쿼리를 사용하십시오. 매우 간단합니다. 여기 있습니다 :

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는 날짜가 두 번 이상 나타나는 경우 결과 집합에서 한 번만보고자합니다.

반복되는 날짜가 마음에 들지 않거나 원래 값과 ID를 보려면 다음이 필요합니다.

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

고마워 ... 간단했다. 성능을 높이기 위해 추가 할 색인이 있었지만 데이터베이스 엔진 튜닝 관리자의 권장 사항 중 하나를 수행하는 것만으로도 간단 해 보였습니다.
Doug_Ivison


4

다른 곳에서와 마찬가지로 SQL Server 2008에서 윈도우가 작동한다고 가정하면 다음과 같이하십시오.

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

MSDN 은 SQL Server 2008 (및 아마도 2005도 가능)에서 사용할 수 있다고 말하지만 직접 사용해 볼 인스턴스가 없습니다.

편집 : 글쎄, 분명히 SQL Server는 "PARTITION BY"를 지정하지 않고 창 사양 ( "OVER (...)")을 허용하지 않습니다 (결과를 그룹으로 나누지 만 GROUP BY와 같은 방식으로 집계하지는 않음). 성가신-MSDN 구문 참조는 선택 사항이지만 현재 SqlServer 2000 인스턴스 만 있습니다.

내가 준 쿼리는 Oracle 10.2.0.3.0과 PostgreSQL 8.4-beta에서 모두 작동합니다. 따라서 MS에게 따라 잡으라고 말하십시오.)


2
이 경우 SUM과 함께 OVER를 사용하면 누적 합계를 얻을 수 없습니다. SUM과 함께 사용될 때 OVER 절은 ORDER BY를 허용하지 않습니다. 누적 합계에는 작동하지 않는 PARTITION BY를 사용해야합니다.
Sam Ax

고마워, 이것이 왜 효과가 없는지 듣는 것이 실제로 유용합니다. araqnid 당신은 왜 옵션이 아닌지 설명하기 위해 답을 편집 할 수 있습니다
codeulike


이것은 파티션을 만들어야하기 때문에 실제로 효과적입니다. 따라서 이것이 가장 인기있는 대답은 아니지만 SQL의 RT 문제에 대한 가장 쉬운 해결책입니다.
William MB

나는 MSSQL 2008을 가지고 있지 않지만 아마도 (null을 선택하여) 파티셔닝 문제를 해결할 수 있다고 생각합니다. 또는 그것으로 하위 선택 1 partitionme하고 파티션하십시오. 또한 보고서 작성시 실제 상황에서 파티션 기준이 필요할 수 있습니다.
nurettin

4

위의 Sql Server 2008 R2를 사용중인 경우 그러면 가장 짧은 방법 일 것입니다.

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

지연 는 이전 행 값을 얻는 데 사용됩니다. 더 많은 정보를 얻기 위해 구글을 할 수 있습니다.

[1]:


1
LAG 는 SQL Server 2012 이상 (2008 아님)에만 존재 한다고 생각 합니다.
AaA

1
LAG ()를 사용하면 SUM(somevalue) OVER(...) 나에게 훨씬 깨끗한 것처럼 개선되지 않습니다
Used_By_Already

2

아래의 간단한 INNER JOIN 작업을 사용하여 누적 합계를 얻을 수 있다고 생각합니다.

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

그렇습니다. Sam Saffron의 답변에서 'Test 3'과 같습니다.
codeulike

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에 클러스터 된 인덱스가 있으면 성능이 크게 향상됩니다.


@Dave 나는이 질문이 이것을하는 효율적인 방법을 찾으려고 노력하고 있다고 생각한다. 교차 결합은 큰 세트에 대해 실제로 느려질 것이다
Sam Saffron

efficienty 비판이 덕분에, 자사의 다른 답변이 유용하고, 또한 유용한
codeulike


2

가장 좋은 방법은 창 함수를 사용하는 것이지만 간단한 상관 하위 쿼리를 사용하여 수행 할 수도 있습니다 .

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;

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

여기서하고있는 일에 대한 정보를 제공하고이 특정 방법의 장점 / 단점에 주목해야합니다.
TT.

0

누계를 계산하는 간단한 두 가지 방법은 다음과 같습니다.

접근법 1 : DBMS가 분석 기능을 지원하는 경우 이러한 방식으로 작성할 수 있습니다.

SELECT     id
           ,somedate
           ,somevalue
           ,runningtotal = SUM(somevalue) OVER (ORDER BY somedate ASC)
FROM       TestTable

접근법 2 : 데이터베이스 버전 / DBMS 자체가 분석 기능을 지원하지 않는 경우 OUTER APPLY를 사용할 수 있습니다.

SELECT     T.id
           ,T.somedate
           ,T.somevalue
           ,runningtotal = OA.runningtotal
FROM       TestTable T
           OUTER APPLY (
                           SELECT   runningtotal = SUM(TI.somevalue)
                           FROM     TestTable TI
                           WHERE    TI.somedate <= S.somedate
                       ) OA;

참고 :-다른 파티션에 대한 누적 합계를 별도로 계산 해야하는 경우 여기에 게시 된대로 수행 할 수 있습니다. 행 전체의 누적 합계 계산 및 ID별로 그룹화

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.