비슷한 문제를 살펴본 결과 데이터를 한 번만 통과하는 윈도우 함수 솔루션을 찾지 못했습니다. 나는 그것이 가능하지 않다고 생각합니다. 창 함수는 열의 모든 값에 적용 할 수 있어야합니다. 한 번의 재설정으로 다음 모든 값의 값이 변경되므로 이와 같은 재설정 계산이 매우 어렵습니다.
문제를 생각하는 한 가지 방법은 올바른 이전 행에서 누적 합계를 빼는 한 기본 누적 합계를 계산하면 원하는 최종 결과를 얻을 수 있다는 것입니다. 예를 들어, 표본 데이터에서 id
4 의 값 은 running total of row 4 - the running total of row 3
입니다. id
6 의 값 running total of row 6 - the running total of row 3
은 재설정이 아직 발생하지 않았기 때문입니다. id
7 의 값은 running total of row 7 - the running total of row 6
등등입니다.
루프에서 T-SQL로 이것에 접근합니다. 나는 조금 나아졌고 완전한 해결책이 있다고 생각합니다. 3 백만 행과 500 개 그룹의 경우 코드가 데스크톱에서 24 초 안에 완료되었습니다. 6 vCPU가있는 SQL Server 2016 Developer 버전으로 테스트하고 있습니다. 병렬 삽입 및 병렬 실행을 일반적으로 활용하고 있으므로 이전 버전이거나 DOP 제한이있는 경우 코드를 변경해야 할 수도 있습니다.
데이터를 생성하는 데 사용한 코드 아래. 의 범위 VAL
및 RESET_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;