카운트와 총계?


34

제목에서 알 수 있듯이 T-SQL에서 누적 합계를 얻는 데 도움이 필요합니다. 문제는 내가해야 할 합계가 카운트의 합계라는 것입니다.

sum(count (distinct (customers))) 

카운트를 단독으로 실행하면 결과는 다음과 같습니다.

Day | CountCustomers
----------------------
5/1  |      1
5/2  |      0
5/3  |      5

합계가있는 출력이 필요합니다.

Day | RunningTotalCustomers
----------------------
5/1  |      1
5/2  |      1
5/3  |      6

coalesce방법을 사용하기 전에 총계를 실행 했지만 카운트는 없습니다. 카운트가 있으므로 지금 어떻게 해야할지 모르겠습니다.


2
SQL Server의 버전은 무엇입니까? 데이터의 범위를 공유 할 수 있습니까? 약 1000 행, 백만, 십억에 대해 이야기하고 있습니까? 실제로이 두 열입니까, 아니면 스키마를 단순화 했습니까? 마지막으로, Day열쇠이며 값이 연속적입니까?
Aaron Bertrand

나는 총 (커서 대 하이브리드 재귀 CTE 대 기발한 업데이트를) 실행에 대한 포괄적 인 블로그를 만들어 : ienablemuch.com/2012/05/...은 내가 사용하는 순수 세트 기반의 접근 방식은 성능이 될 것이 없다는 누적 합계를 포함하지 않았다 원하는 : sqlblog.com/blogs/adam_machanic/archive/2006/07/12/...
마이클 부엔

답변:


53

비교할 수있는 몇 가지 방법이 있습니다. 먼저 더미 데이터가있는 테이블을 설정해 봅시다. 나는 이것을 sys.all_columns의 무작위 데이터로 채 웁니다. 글쎄, 그것은 임의의 종류입니다-나는 날짜가 연속적임을 확신하고 있습니다 (정답 중 하나에 만 중요합니다).

CREATE TABLE dbo.Hits(Day SMALLDATETIME, CustomerID INT);

CREATE CLUSTERED INDEX x ON dbo.Hits([Day]);

INSERT dbo.Hits SELECT TOP (5000) DATEADD(DAY, r, '20120501'),
  COALESCE(ASCII(SUBSTRING(name, s, 1)), 86)
FROM (SELECT name, r = ROW_NUMBER() OVER (ORDER BY name)/10,
       s = CONVERT(INT, RIGHT(CONVERT(VARCHAR(20), [object_id]), 1))
FROM sys.all_columns) AS x;

SELECT 
  Earliest_Day   = MIN([Day]), 
  Latest_Day     = MAX([Day]), 
  Unique_Days    = DATEDIFF(DAY, MIN([Day]), MAX([Day])) + 1, 
  Total_Rows     = COUNT(*)
FROM dbo.Hits;

결과 :

Earliest_Day         Latest_Day           Unique_Days  Total_Days
-------------------  -------------------  -----------  ----------
2012-05-01 00:00:00  2013-09-13 00:00:00  501          5000

데이터는 다음과 같습니다 (5000 행). 버전 및 빌드 번호에 따라 시스템에서 약간 다르게 보일 것입니다.

Day                  CustomerID
-------------------  ---
2012-05-01 00:00:00  95
2012-05-01 00:00:00  97
2012-05-01 00:00:00  97
2012-05-01 00:00:00  117
2012-05-01 00:00:00  100
...
2012-05-02 00:00:00  110
2012-05-02 00:00:00  110
2012-05-02 00:00:00  95
...

누적 합계 결과는 다음과 같습니다 (501 행).

Day                  c   rt
-------------------  --  --
2012-05-01 00:00:00  6   6
2012-05-02 00:00:00  5   11
2012-05-03 00:00:00  4   15
2012-05-04 00:00:00  7   22
2012-05-05 00:00:00  6   28
...

따라서 비교할 방법은 다음과 같습니다.

  • "self-join"-세트 기반 순수 주의자 접근
  • "날짜가있는 재귀 CTE"-연속 날짜에 의존합니다 (격차 없음).
  • "row_number의 재귀 CTE"-위와 비슷하지만 느리며 ROW_NUMBER에 의존
  • "#temp 테이블이있는 재귀 CTE"-제안 된 Mikael의 답변에서 도난
  • 지원되지 않고 유망한 정의 된 동작이지만 매우 인기있는 것처럼 보이는 "기발한 업데이트"
  • "커서"
  • 새로운 윈도우 기능을 사용하는 SQL Server 2012

자기 조인

이것은 "세트 기반이 항상 빠르기"때문에 사람들이 커서에서 멀리 떨어지라고 경고 할 때 지시하는 방법입니다. 최근의 일부 실험에서 커서 가이 솔루션을 능가하는 것으로 나타났습니다.

;WITH g AS 
(
  SELECT [Day], c = COUNT(DISTINCT CustomerID) 
    FROM dbo.Hits
    GROUP BY [Day]
)
SELECT g.[Day], g.c, rt = SUM(g2.c)
  FROM g INNER JOIN g AS g2
  ON g.[Day] >= g2.[Day]
GROUP BY g.[Day], g.c
ORDER BY g.[Day];

날짜가 포함 된 재귀 적 cte

알림-연속 날짜 (간격 없음), 최대 10000 수준의 재귀에 의존하며 원하는 범위의 시작 날짜를 알고 있습니다 (앵커 설정). 물론 하위 쿼리를 사용하여 앵커를 동적으로 설정할 수는 있지만 간단하게 유지하고 싶었습니다.

;WITH g AS 
(
  SELECT [Day], c = COUNT(DISTINCT CustomerID) 
    FROM dbo.Hits
    GROUP BY [Day]
), x AS
(
    SELECT [Day], c, rt = c
        FROM g
        WHERE [Day] = '20120501'
    UNION ALL
    SELECT g.[Day], g.c, x.rt + g.c
        FROM x INNER JOIN g
        ON g.[Day] = DATEADD(DAY, 1, x.[Day])
)
SELECT [Day], c, rt
    FROM x
    ORDER BY [Day]
    OPTION (MAXRECURSION 10000);

row_number의 재귀 적 cte

Row_number 계산은 여기에서 약간 비쌉니다. 다시 말하지만 최대 재귀 수준 인 10000을 지원하지만 앵커를 할당 할 필요는 없습니다.

;WITH g AS 
(
  SELECT [Day], rn = ROW_NUMBER() OVER (ORDER BY DAY), 
    c = COUNT(DISTINCT CustomerID) 
    FROM dbo.Hits
    GROUP BY [Day]
), x AS
(
    SELECT [Day], rn, c, rt = c
        FROM g
        WHERE rn = 1
    UNION ALL
    SELECT g.[Day], g.rn, g.c, x.rt + g.c
        FROM x INNER JOIN g
        ON g.rn = x.rn + 1
)
SELECT [Day], c, rt
    FROM x
    ORDER BY [Day]
    OPTION (MAXRECURSION 10000);

임시 테이블이있는 재귀 cte

제안에 따라 Mikael의 답변을 훔쳐서 테스트에 포함시킵니다.

CREATE TABLE #Hits
(
  rn INT PRIMARY KEY,
  c INT,
  [Day] SMALLDATETIME
);

INSERT INTO #Hits (rn, c, Day)
SELECT ROW_NUMBER() OVER (ORDER BY DAY),
       COUNT(DISTINCT CustomerID),
       [Day]
FROM dbo.Hits
GROUP BY [Day];

WITH x AS
(
    SELECT [Day], rn, c, rt = c
        FROM #Hits as c
        WHERE rn = 1
    UNION ALL
    SELECT g.[Day], g.rn, g.c, x.rt + g.c
        FROM x INNER JOIN #Hits as g
        ON g.rn = x.rn + 1
)
SELECT [Day], c, rt
    FROM x
    ORDER BY [Day]
    OPTION (MAXRECURSION 10000);

DROP TABLE #Hits;

기발한 업데이트

다시 한 번 나는 이것을 완전성을 위해 포함시키고있다. 다른 답변에서 언급 했듯이이 방법은 전혀 작동하지 않으며 향후 버전의 SQL Server에서 완전히 중단 될 수 있기 때문에 개인적 으로이 솔루션에 의존하지 않습니다. (인덱스 선택에 대한 힌트를 사용하여 원하는 순서대로 SQL Server를 강제 실행하기 위해 최선을 다하고 있습니다.)

CREATE TABLE #x([Day] SMALLDATETIME, c INT, rt INT);
CREATE UNIQUE CLUSTERED INDEX x ON #x([Day]);

INSERT #x([Day], c) 
    SELECT [Day], c = COUNT(DISTINCT CustomerID) 
    FROM dbo.Hits
    GROUP BY [Day]
    ORDER BY [Day];

DECLARE @rt1 INT;
SET @rt1 = 0;

UPDATE #x
SET @rt1 = rt = @rt1 + c
FROM #x WITH (INDEX = x);

SELECT [Day], c, rt FROM #x ORDER BY [Day];

DROP TABLE #x;

커서

"여기에 커서가 있습니다! 커서는 악합니다! 커서는 항상 피해야합니다!" 아니, 그건 내가 말하는 게 아니라, 내가 많이 듣는 것 뿐이야 대중의 의견과 달리 커서가 적합한 경우가 있습니다.

CREATE TABLE #x2([Day] SMALLDATETIME, c INT, rt INT);
CREATE UNIQUE CLUSTERED INDEX x ON #x2([Day]);

INSERT #x2([Day], c) 
    SELECT [Day], COUNT(DISTINCT CustomerID) 
    FROM dbo.Hits
    GROUP BY [Day]
    ORDER BY [Day];

DECLARE @rt2 INT, @d SMALLDATETIME, @c INT;
SET @rt2 = 0;

DECLARE c CURSOR LOCAL STATIC READ_ONLY FORWARD_ONLY
  FOR SELECT [Day], c FROM #x2 ORDER BY [Day];

OPEN c;

FETCH NEXT FROM c INTO @d, @c;

WHILE @@FETCH_STATUS = 0
BEGIN
  SET @rt2 = @rt2 + @c;
  UPDATE #x2 SET rt = @rt2 WHERE [Day] = @d;
  FETCH NEXT FROM c INTO @d, @c;
END

SELECT [Day], c, rt FROM #x2 ORDER BY [Day];

DROP TABLE #x2;

SQL Server 2012

최신 버전의 SQL Server를 사용하는 경우 창 기능 향상으로 인해 자체 가입 비용 (SUM이 한 번에 계산 됨)없이 CTE의 복잡성 (요구 사항 포함)없이 누적 합계를 쉽게 계산할 수 있습니다. 더 나은 성능의 CTE를위한 연속 행), 지원되지 않는 기발한 업데이트 및 금지 된 커서. 사용 RANGE과의 ROWS지정 의 차이에주의 하거나 전혀 지정하지 않으면 ROWS온 디스크 스풀 만 피할 수 있습니다. 그렇지 않으면 성능이 크게 저하 될 수 있습니다.

;WITH g AS 
(
  SELECT [Day], c = COUNT(DISTINCT CustomerID) 
    FROM dbo.Hits
    GROUP BY [Day]
)
SELECT g.[Day], c, 
  rt = SUM(c) OVER (ORDER BY [Day] ROWS UNBOUNDED PRECEDING)
FROM g
ORDER BY g.[Day];

성능 비교

나는 각 접근법을 취하여 다음을 사용하여 배치를 감쌌다.

SELECT SYSUTCDATETIME();
GO
DBCC DROPCLEANBUFFERS;DBCC FREEPROCCACHE;
-- query here
GO 10
SELECT SYSUTCDATETIME();

다음은 총 지속 시간의 결과입니다 (밀리 초) (이는 매번 DBCC 명령도 포함합니다).

method                          run 1     run 2
-----------------------------   --------  --------
self-join                        1296 ms   1357 ms -- "supported" non-SQL 2012 winner
recursive cte with dates         1655 ms   1516 ms
recursive cte with row_number   19747 ms  19630 ms
recursive cte with #temp table   1624 ms   1329 ms
quirky update                     880 ms   1030 ms -- non-SQL 2012 winner
cursor                           1962 ms   1850 ms
SQL Server 2012                   847 ms    917 ms -- winner if SQL 2012 available

그리고 DBCC 명령없이 다시했습니다.

method                          run 1     run 2
-----------------------------   --------  --------
self-join                        1272 ms   1309 ms -- "supported" non-SQL 2012 winner
recursive cte with dates         1247 ms   1593 ms
recursive cte with row_number   18646 ms  18803 ms
recursive cte with #temp table   1340 ms   1564 ms
quirky update                    1024 ms   1116 ms -- non-SQL 2012 winner
cursor                           1969 ms   1835 ms
SQL Server 2012                   600 ms    569 ms -- winner if SQL 2012 available

하나의 원시 반복 만 측정하여 DBCC와 루프를 모두 제거하십시오.

method                          run 1     run 2
-----------------------------   --------  --------
self-join                         313 ms    242 ms
recursive cte with dates          217 ms    217 ms
recursive cte with row_number    2114 ms   1976 ms
recursive cte with #temp table     83 ms    116 ms -- "supported" non-SQL 2012 winner
quirky update                      86 ms     85 ms -- non-SQL 2012 winner
cursor                           1060 ms    983 ms
SQL Server 2012                    68 ms     40 ms -- winner if SQL 2012 available

마지막으로 소스 테이블의 행 수에 10을 곱했습니다 (최고를 50000으로 변경하고 다른 테이블을 크로스 조인으로 추가). 이것의 결과, DBCC 명령이없는 하나의 단일 반복 (단순히 이익을 위해) :

method                           run 1      run 2
-----------------------------    --------   --------
self-join                         2401 ms    2520 ms
recursive cte with dates           442 ms     473 ms
recursive cte with row_number   144548 ms  147716 ms
recursive cte with #temp table     245 ms     236 ms -- "supported" non-SQL 2012 winner
quirky update                      150 ms     148 ms -- non-SQL 2012 winner
cursor                            1453 ms    1395 ms
SQL Server 2012                    131 ms     133 ms -- winner

나는 단지 지속 시간만을 측정했다-나는 데이터에 대한 이러한 접근법을 비교하고 중요하거나 다른 스키마 / 데이터에 따라 다를 수있는 다른 메트릭을 비교하기 위해 독자에게 연습으로 남겨 두겠다. 이 답변에서 결론을 도출하기 전에 데이터와 스키마에 대해 테스트하는 것은 전적으로 귀하의 몫입니다.이 결과는 행 수가 높아질수록 거의 확실하게 변경됩니다.


데모

sqlfiddle을 추가했습니다 . 결과 :

여기에 이미지 설명을 입력하십시오


결론

내 테스트에서 선택은 다음과 같습니다.

  1. SQL Server 2012를 사용할 수있는 경우 SQL Server 2012 방법
  2. SQL Server 2012를 사용할 수없고 내 날짜가 연속적이면 재귀 적 cte with dates 방법을 사용합니다.
  3. 1과 2 모두 적용 할 수없는 경우, 동작이 문서화되고 보장 되었기 때문에 성능이 거의 없더라도 기발한 업데이트를 통해 자체 조인을 수행합니다. 기발한 업데이트가 중단되면 이미 모든 코드를 1로 변환 한 후에 발생하기 때문에 향후 호환성에 대해 걱정하지 않습니다. :-)

그러나 다시 스키마와 데이터에 대해 테스트해야합니다. 이것은 상대적으로 적은 행 수를 가진 고안된 테스트이기 때문에 바람에 방귀가 될 수 있습니다. 다른 스키마와 행 수로 다른 테스트를 수행했으며 성능 휴리스틱이 상당히 달랐습니다. 그래서 원래 질문에 대한 후속 질문을 많이했습니다.


최신 정보

나는 이것에 대해 더 많이 블로그에 올렸다.

총계 실행을위한 최선의 방법 – SQL Server 2012 용으로 업데이트


1

이것은 분명히 최적의 솔루션입니다

DECLARE @dailyCustomers TABLE (day smalldatetime, CountCustomers int, RunningTotal int)

DECLARE @RunningTotal int

SET @RunningTotal = 0

INSERT INTO @dailyCustomers 
SELECT day, CountCustomers, null
FROM Sales
ORDER BY day

UPDATE @dailyCustomers
SET @RunningTotal = RunningTotal = @RunningTotal + CountCustomers
FROM @dailyCustomers

SELECT * FROM @dailyCustomers

임시 테이블을 구현하지 않은 아이디어 (내 proc은 이미 여러 임시 테이블을 통해 값을 강제하고 있으므로 다른 임시 테이블을 사용하지 않는 방법을 찾으려고 노력하고 있습니까?) 그렇지 않은 경우이 방법을 사용합니다. 나는 그것이 작동 할 것이라고 생각한다

자체 조인 또는 중첩 된 하위 쿼리를 사용하여 수행 할 수도 있지만 이러한 옵션은 거의 수행되지 않습니다. 또한 스풀링이나 작업 테이블이있는 이러한 대안으로 tempdb를 칠 가능성이 있습니다.

3
이 "기발한 업데이트"방법은 작동하지 않을 수 있습니다.이 구문은 지원되지 않으며 동작이 정의되지 않으며 향후 버전, 핫픽스 또는 서비스 팩에서 중단 될 수 있습니다. 따라서 지원되는 일부 대안보다 빠르지 만 향후 호환성 비용이 발생할 수 있습니다.
Aaron Bertrand

6
Jeff Moden이 작성한 어딘가에이 접근법에 대한 많은 경고가 있습니다. day예를 들어 클러스터 된 인덱스가 있어야합니다 .
Martin Smith

2
@MartinSmith sqlservercentral.com 의 매우 큰 기사입니다 (작성자 페이지로 이동하여 기발한 업데이트에 대한 기사를 찾으십시오).
Fabricio Araujo 2016 년

-2

다른 방법으로 비용이 많이 들지만 버전 독립적입니다. 임시 테이블이나 변수를 사용하지 않습니다.

select T.dday, T.CustomersByDay + 
    (select count(A.customer) from NewCustomersByDate A 
      where A.dday < T.dday) as TotalCustomerTillNow 
from (select dday, count(customer) as CustomersByDay 
        from NewCustomersByDate group by dday) T 

2
그것은 좋지 않습니다, 그것은 매우 느립니다. 심지어 100 행만 가지고도 5,050 번 테이블 사이에서 탁구 읽기를 수행합니다. 200 행은 20,100 배입니다. 만 1,000 행으로, 그것은 기하 급수적으로 500,500에 점프 읽고 sqlblog.com/blogs/adam_machanic/archive/2006/07/12/...을
마이클 부엔

이 게시물을 게시 한 후 블로그 링크를 보았습니다. 이제는 매우 나쁜 생각입니다. 감사합니다!
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.