다른 열을 기준으로 누적 합계 재설정


10

누계를 계산하려고합니다. 그러나 누적 합계가 다른 열 값보다 큰 경우 재설정해야합니다.

create table #reset_runn_total
(
id int identity(1,1),
val int, 
reset_val int,
grp int
)

insert into #reset_runn_total
values 
(1,10,1),
(8,12,1),(6,14,1),(5,10,1),(6,13,1),(3,11,1),(9,8,1),(10,12,1)


SELECT Row_number()OVER(partition BY grp ORDER BY id)AS rn,*
INTO   #test
FROM   #reset_runn_total

색인 세부 사항 :

CREATE UNIQUE CLUSTERED INDEX ix_load_reset_runn_total
  ON #test(rn, grp) 

샘플 데이터

+----+-----+-----------+-----+
| id | val | reset_val | Grp |
+----+-----+-----------+-----+
|  1 |   1 |        10 | 1   |
|  2 |   8 |        12 | 1   |
|  3 |   6 |        14 | 1   |
|  4 |   5 |        10 | 1   |
|  5 |   6 |        13 | 1   |
|  6 |   3 |        11 | 1   |
|  7 |   9 |         8 | 1   |
|  8 |  10 |        12 | 1   |
+----+-----+-----------+-----+ 

예상 결과

+----+-----+-----------------+-------------+
| id | val |    reset_val    | Running_tot |
+----+-----+-----------------+-------------+
|  1 |   1 | 10              |       1     |  
|  2 |   8 | 12              |       9     |  --1+8
|  3 |   6 | 14              |       15    |  --1+8+6 -- greater than reset val
|  4 |   5 | 10              |       5     |  --reset 
|  5 |   6 | 13              |       11    |  --5+6
|  6 |   3 | 11              |       14    |  --5+6+3 -- greater than reset val
|  7 |   9 | 8               |       9     |  --reset -- greater than reset val 
|  8 |  10 | 12              |      10     |  --reset
+----+-----+-----------------+-------------+

질문:

을 사용하여 결과를 얻었습니다 Recursive CTE. 원래 질문은 https : //.com/questions/42085404/reset-running-total-based-on-another-column

;WITH cte
     AS (SELECT rn,id,
                val,
                reset_val,
                grp,
                val                   AS running_total,
                Iif (val > reset_val, 1, 0) AS flag
         FROM   #test
         WHERE  rn = 1
         UNION ALL
         SELECT r.*,
                Iif(c.flag = 1, r.val, c.running_total + r.val),
                Iif(Iif(c.flag = 1, r.val, c.running_total + r.val) > r.reset_val, 1, 0)
         FROM   cte c
                JOIN #test r
                  ON r.grp = c.grp
                     AND r.rn = c.rn + 1)
SELECT *
FROM   cte 

. T-SQL를 사용하지 않고 더 나은 대안이 CLR있습니까?


더 나은 방법? 이 쿼리는 성능이 좋지 않습니까? 어떤 측정 항목을 사용합니까?
Aaron Bertrand

@AaronBertrand-이해를 돕기 위해 한 그룹의 샘플 데이터를 게시했습니다. Id50000 와 그룹 주변에서 동일한 작업을 수행해야합니다 . 그래서 총 레코드 수는 주변에있을 것 입니다. 확실히 확장되지 않을 것입니다 . 사무실로 돌아 오면 통계를 업데이트합니다. 우리는이 사용을 달성 할 수 이 문서에서 사용한 것처럼 sqlperformance.com/2012/07/t-sql-queries/running-totals60 3000000Recursive CTE3000000sum()Over(Order by)
P ரதீப்에게

커서가 재귀 CTE보다 더 잘 할 수
파파라치

답변:


6

비슷한 문제를 살펴본 결과 데이터를 한 번만 통과하는 윈도우 함수 솔루션을 찾지 못했습니다. 나는 그것이 가능하지 않다고 생각합니다. 창 함수는 열의 모든 값에 적용 할 수 있어야합니다. 한 번의 재설정으로 다음 모든 값의 값이 변경되므로 이와 같은 재설정 계산이 매우 어렵습니다.

문제를 생각하는 한 가지 방법은 올바른 이전 행에서 누적 합계를 빼는 한 기본 누적 합계를 계산하면 원하는 최종 결과를 얻을 수 있다는 것입니다. 예를 들어, 표본 데이터에서 id4 의 값 은 running total of row 4 - the running total of row 3입니다. id6 의 값 running total of row 6 - the running total of row 3은 재설정이 아직 발생하지 않았기 때문입니다. id7 의 값은 running total of row 7 - the running total of row 6등등입니다.

루프에서 T-SQL로 이것에 접근합니다. 나는 조금 나아졌고 완전한 해결책이 있다고 생각합니다. 3 백만 행과 500 개 그룹의 경우 코드가 데스크톱에서 24 초 안에 완료되었습니다. 6 vCPU가있는 SQL Server 2016 Developer 버전으로 테스트하고 있습니다. 병렬 삽입 및 병렬 실행을 일반적으로 활용하고 있으므로 이전 버전이거나 DOP 제한이있는 경우 코드를 변경해야 할 수도 있습니다.

데이터를 생성하는 데 사용한 코드 아래. 의 범위 VALRESET_VAL샘플 데이터 유사해야합니다.

drop table if exists reset_runn_total;

create table reset_runn_total
(
id int identity(1,1),
val int, 
reset_val int,
grp int
);

DECLARE 
@group_num INT,
@row_num INT;
BEGIN
    SET NOCOUNT ON;
    BEGIN TRANSACTION;

    SET @group_num = 1;
    WHILE @group_num <= 50000 
    BEGIN
        SET @row_num = 1;
        WHILE @row_num <= 60
        BEGIN
            INSERT INTO reset_runn_total WITH (TABLOCK)
            SELECT 1 + ABS(CHECKSUM(NewId())) % 10, 8 + ABS(CHECKSUM(NewId())) % 8, @group_num;

            SET @row_num = @row_num + 1;
        END;
        SET @group_num = @group_num + 1;
    END;
    COMMIT TRANSACTION;
END;

알고리즘은 다음과 같습니다.

1) 표준 누적 합계가있는 모든 행을 임시 테이블에 삽입하여 시작하십시오.

2) 루프에서 :

2a) 각 그룹에 대해 테이블에 남아있는 reset_value보다 높은 누적 합계로 첫 번째 행을 계산하고 id, 너무 큰 누적 합계 및 임시 테이블에서 너무 큰 이전 누적 합계를 저장하십시오.

2b) 첫 번째 임시 테이블의 행을 두 번째 임시 테이블의 값 ID이하인 결과 임시 테이블로 삭제 ID하십시오. 다른 열을 사용하여 필요에 따라 누적 합계를 조정하십시오.

3) 삭제가 더 이상 처리하지 않으면 행 DELETE OUTPUT이 결과 테이블에 추가 로 실행 됩니다. 재설정 값을 초과하지 않는 그룹 끝의 행에 해당됩니다.

위의 알고리즘을 한 단계 씩 T-SQL에서 구현해 보겠습니다.

임시 테이블을 몇 개 작성하여 시작하십시오. #initial_results는 표준 누적 합계로 원본 데이터를 유지하고 #group_bookkeeping이동할 수있는 행을 파악하기 위해 각 루프를 업데이트 #final_results하며 재설정에 대해 누적 합계가 조정 된 결과를 포함합니다.

CREATE TABLE #initial_results (
id int,
val int, 
reset_val int,
grp int,
initial_running_total int
);

CREATE TABLE #group_bookkeeping (
grp int,
max_id_to_move int,
running_total_to_subtract_this_loop int,
running_total_to_subtract_next_loop int,
grp_done bit, 
PRIMARY KEY (grp)
);

CREATE TABLE #final_results (
id int,
val int, 
reset_val int,
grp int,
running_total int
);

INSERT INTO #initial_results WITH (TABLOCK)
SELECT ID, VAL, RESET_VAL, GRP, SUM(VAL) OVER (PARTITION BY GRP ORDER BY ID) RUNNING_TOTAL
FROM reset_runn_total;

CREATE CLUSTERED INDEX i1 ON #initial_results (grp, id);

INSERT INTO #group_bookkeeping WITH (TABLOCK)
SELECT DISTINCT GRP, 0, 0, 0, 0
FROM reset_runn_total;

삽입 및 인덱스 빌드를 병렬로 수행 할 수 있도록 임시 테이블에 클러스터형 인덱스를 만듭니다. 내 컴퓨터에는 큰 차이가 있지만 당신에게는 그렇지 않을 수 있습니다. 소스 테이블에서 인덱스를 작성하는 것이 도움이되지 않았지만 시스템에 도움이 될 수 있습니다.

아래 코드는 루프에서 실행되며 부기 테이블을 업데이트합니다. 각 그룹 ID에 대해 결과 테이블로 이동해야하는 최대 값 을 찾아야 합니다. 해당 행의 누적 합계가 필요하므로 초기 누적 합계에서 뺄 수 있습니다. 에 대한 grp_done작업이 더 이상 없으면 열이 1로 설정됩니다 grp.

WITH UPD_CTE AS (
        SELECT 
        #grp_bookkeeping.GRP
        , MIN(CASE WHEN initial_running_total - #group_bookkeeping.running_total_to_subtract_next_loop > RESET_VAL THEN ID ELSE NULL END) max_id_to_update
        , MIN(#group_bookkeeping.running_total_to_subtract_next_loop) running_total_to_subtract_this_loop
        , MIN(CASE WHEN initial_running_total - #group_bookkeeping.running_total_to_subtract_next_loop > RESET_VAL THEN initial_running_total ELSE NULL END) additional_value_next_loop
        , CASE WHEN MIN(CASE WHEN initial_running_total - #group_bookkeeping.running_total_to_subtract_next_loop > RESET_VAL THEN ID ELSE NULL END) IS NULL THEN 1 ELSE 0 END grp_done
        FROM #group_bookkeeping 
        INNER JOIN #initial_results IR ON #group_bookkeeping.grp = ir.grp
        WHERE #group_bookkeeping.grp_done = 0
        GROUP BY #group_bookkeeping.GRP
    )
    UPDATE #group_bookkeeping
    SET #group_bookkeeping.max_id_to_move = uv.max_id_to_update
    , #group_bookkeeping.running_total_to_subtract_this_loop = uv.running_total_to_subtract_this_loop
    , #group_bookkeeping.running_total_to_subtract_next_loop = uv.additional_value_next_loop
    , #group_bookkeeping.grp_done = uv.grp_done
    FROM UPD_CTE uv
    WHERE uv.GRP = #group_bookkeeping.grp
OPTION (LOOP JOIN);

LOOP JOIN일반적으로 힌트 의 팬은 아니지만 이것은 간단한 쿼리이며 원하는 것을 얻는 가장 빠른 방법이었습니다. 응답 시간을 실제로 최적화하기 위해 DOP 1 병합 조인 대신 병렬 중첩 루프 조인을 원했습니다.

아래 코드는 루프에서 실행되며 초기 테이블에서 최종 결과 테이블로 데이터를 이동합니다. 초기 누적 합계에 대한 조정을 확인하십시오.

DELETE ir
OUTPUT DELETED.id,  
    DELETED.VAL,  
    DELETED.RESET_VAL,  
    DELETED.GRP ,
    DELETED.initial_running_total - tb.running_total_to_subtract_this_loop
INTO #final_results
FROM #initial_results ir
INNER JOIN #group_bookkeeping tb ON ir.GRP = tb.GRP AND ir.ID <= tb.max_id_to_move
WHERE tb.grp_done = 0;

아래는 전체 코드입니다.

DECLARE @RC INT;
BEGIN
SET NOCOUNT ON;

CREATE TABLE #initial_results (
id int,
val int, 
reset_val int,
grp int,
initial_running_total int
);

CREATE TABLE #group_bookkeeping (
grp int,
max_id_to_move int,
running_total_to_subtract_this_loop int,
running_total_to_subtract_next_loop int,
grp_done bit, 
PRIMARY KEY (grp)
);

CREATE TABLE #final_results (
id int,
val int, 
reset_val int,
grp int,
running_total int
);

INSERT INTO #initial_results WITH (TABLOCK)
SELECT ID, VAL, RESET_VAL, GRP, SUM(VAL) OVER (PARTITION BY GRP ORDER BY ID) RUNNING_TOTAL
FROM reset_runn_total;

CREATE CLUSTERED INDEX i1 ON #initial_results (grp, id);

INSERT INTO #group_bookkeeping WITH (TABLOCK)
SELECT DISTINCT GRP, 0, 0, 0, 0
FROM reset_runn_total;

SET @RC = 1;
WHILE @RC > 0 
BEGIN
    WITH UPD_CTE AS (
        SELECT 
        #group_bookkeeping.GRP
        , MIN(CASE WHEN initial_running_total - #group_bookkeeping.running_total_to_subtract_next_loop > RESET_VAL THEN ID ELSE NULL END) max_id_to_move
        , MIN(#group_bookkeeping.running_total_to_subtract_next_loop) running_total_to_subtract_this_loop
        , MIN(CASE WHEN initial_running_total - #group_bookkeeping.running_total_to_subtract_next_loop > RESET_VAL THEN initial_running_total ELSE NULL END) additional_value_next_loop
        , CASE WHEN MIN(CASE WHEN initial_running_total - #group_bookkeeping.running_total_to_subtract_next_loop > RESET_VAL THEN ID ELSE NULL END) IS NULL THEN 1 ELSE 0 END grp_done
        FROM #group_bookkeeping 
        CROSS APPLY (SELECT ID, RESET_VAL, initial_running_total FROM #initial_results ir WHERE #group_bookkeeping.grp = ir.grp ) ir
        WHERE #group_bookkeeping.grp_done = 0
        GROUP BY #group_bookkeeping.GRP
    )
    UPDATE #group_bookkeeping
    SET #group_bookkeeping.max_id_to_move = uv.max_id_to_move
    , #group_bookkeeping.running_total_to_subtract_this_loop = uv.running_total_to_subtract_this_loop
    , #group_bookkeeping.running_total_to_subtract_next_loop = uv.additional_value_next_loop
    , #group_bookkeeping.grp_done = uv.grp_done
    FROM UPD_CTE uv
    WHERE uv.GRP = #group_bookkeeping.grp
    OPTION (LOOP JOIN);

    DELETE ir
    OUTPUT DELETED.id,  
        DELETED.VAL,  
        DELETED.RESET_VAL,  
        DELETED.GRP ,
        DELETED.initial_running_total - tb.running_total_to_subtract_this_loop
    INTO #final_results
    FROM #initial_results ir
    INNER JOIN #group_bookkeeping tb ON ir.GRP = tb.GRP AND ir.ID <= tb.max_id_to_move
    WHERE tb.grp_done = 0;

    SET @RC = @@ROWCOUNT;
END;

DELETE ir 
OUTPUT DELETED.id,  
    DELETED.VAL,  
    DELETED.RESET_VAL,  
    DELETED.GRP ,
    DELETED.initial_running_total - tb.running_total_to_subtract_this_loop
    INTO #final_results
FROM #initial_results ir
INNER JOIN #group_bookkeeping tb ON ir.GRP = tb.GRP;

CREATE CLUSTERED INDEX f1 ON #final_results (grp, id);

/* -- do something with the data
SELECT *
FROM #final_results
ORDER BY grp, id;
*/

DROP TABLE #final_results;
DROP TABLE #initial_results;
DROP TABLE #group_bookkeeping;

END;

단순히 멋진 나는 현상금으로 당신에게 상을 수여합니다
P ரதர்

우리 서버에서 50000 grp과 60 id는 1 분 10 초가 걸렸습니다. Recursive CTE2 분 15 초가 걸렸습니다
P ரதீப்

두 코드를 동일한 데이터로 테스트했습니다. 너 대단 했어 더 개선 될 수 있습니까?
P ரதீப்

실제 데이터에서 코드를 실행하고 테스트했습니다. 계산은 실제 절차에서 임시 테이블로 처리되므로 대부분 포장해야합니다. 30 초 정도
줄이면 좋을 것입니다.

@Prdp 업데이트를 사용한 빠른 접근 방식을 시도했지만 더 나빠진 것 같습니다. 잠시 동안 이것을 더 조사 할 수 없을 것입니다. 서버에서 가장 느리게 실행되는 부분을 파악할 수 있도록 각 작업에 걸리는 시간을 기록하십시오. 이 코드의 속도를 높이거나 일반적인 알고리즘을 개선 할 수있는 방법이있을 수 있습니다.
Joe Obbish

4

커서 사용하기 :

ALTER TABLE #reset_runn_total ADD RunningTotal int;

DECLARE @id int, @val int, @reset int, @acm int, @grp int, @last_grp int;
SET @acm = 0;

DECLARE curRes CURSOR FAST_FORWARD FOR 
SELECT id, val, reset_val, grp
FROM #reset_runn_total
ORDER BY grp, id;

OPEN curRes;
FETCH NEXT FROM curRes INTO @id, @val, @reset, @grp;
SET @last_grp = @grp;

WHILE @@FETCH_STATUS = 0  
BEGIN
    IF @grp <> @last_grp SET @acm = 0;
    SET @last_grp = @grp;
    SET @acm = @acm + @val;
    UPDATE #reset_runn_total
    SET RunningTotal = @acm
    WHERE id = @id;
    IF @acm > @reset SET @acm = 0;
    FETCH NEXT FROM curRes INTO @id, @val, @reset, @grp;
END

CLOSE curRes;
DEALLOCATE curRes;

+----+-----+-----------+-------------+
| id | val | reset_val | RunningTotal|
+----+-----+-----------+-------------+
| 1  | 1   | 10        |     1       |
+----+-----+-----------+-------------+
| 2  | 8   | 12        |     9       |
+----+-----+-----------+-------------+
| 3  | 6   | 14        |     15      |
+----+-----+-----------+-------------+
| 4  | 5   | 10        |     5       |
+----+-----+-----------+-------------+
| 5  | 6   | 13        |     11      |
+----+-----+-----------+-------------+
| 6  | 3   | 11        |     14      |
+----+-----+-----------+-------------+
| 7  | 9   | 8         |     9       |
+----+-----+-----------+-------------+
| 8  | 10  | 12        |     10      |
+----+-----+-----------+-------------+

여기를 확인하십시오 : http://rextester.com/WSPLO95303


3

윈도우는 아니지만 순수한 SQL 버전 :

WITH x AS (
    SELECT TOP 1 id,
           val,
           reset_val,
           val AS running_total,
           1 AS level 
      FROM reset_runn_total
    UNION ALL
    SELECT r.id,
           r.val,
           r.reset_val,
           CASE WHEN x.running_total < x.reset_val THEN x.running_total + r.val ELSE r.val END,
           level = level + 1
      FROM x JOIN reset_runn_total AS r ON (r.id > x.id)
) SELECT
  *
FROM x
WHERE NOT EXISTS (
        SELECT 1
        FROM x AS x2
        WHERE x2.id = x.id
        AND x2.level > x.level
    )
ORDER BY id, level DESC
;

저는 SQL Server의 방언 전문가가 아닙니다. 이것은 PostrgreSQL의 초기 버전입니다 (정확하게 이해하면 SQL Server의 재귀 부분에서 LIMIT 1 / TOP 1을 사용할 수 없습니다).

WITH RECURSIVE x AS (
    (SELECT id, val, reset_val, val AS running_total
       FROM reset_runn_total
      ORDER BY id
      LIMIT 1)
    UNION
    (SELECT r.id, r.val, r.reset_val,
            CASE WHEN x.running_total < x.reset_val THEN x.running_total + r.val ELSE r.val END
       FROM x JOIN reset_runn_total AS r ON (r.id > x.id)
      ORDER BY id
      LIMIT 1)
) SELECT * FROM x;

솔직히 @JoeObbish, 그것은 질문에서 완전히 명확하지 않습니다. 예를 들어 예상 결과는 grp열이 표시되지 않습니다 .
ypercubeᵀᴹ

@JoeObbish 그것이 내가 이해 한 것입니다. 그러나 그 질문은 그것에 대한 명백한 진술로부터 유익을 얻을 수 있습니다. 질문의 코드 (CTE 포함)는 코드를 사용하지 않으며 열 이름이 다릅니다. 질문을 읽는 사람에게는 분명 할 것입니다. 다른 답변이나 의견을 읽지 말아야 할 것입니다.
ypercubeᵀᴹ

@ ypercubeᵀᴹ 질문에 대한 필수 정보를 추가했습니다.
P ரதீப்

1

문제를 공격하기 위해 여러 가지 쿼리 / 메소드가있는 것 같지만 우리에게 제공하지 않았거나 고려하지 않았습니까? -테이블의 인덱스

테이블에 어떤 인덱스가 있습니까? 힙입니까 아니면 클러스터형 인덱스가 있습니까?

이 색인을 추가 한 후 제안 된 다양한 솔루션을 시도해보십시오.

(grp, id) INCLUDE (val, reset_val)

또는 클러스터 된 인덱스를로 변경 (또는 작성)하십시오 (grp, id).

특정 쿼리를 대상으로하는 인덱스가 있으면 모든 방법이 아니라 효율성을 향상시켜야합니다.


질문에 필요한 정보를 추가했습니다.
P ரதீப்
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.