Tôi đã xem xét các vấn đề tương tự và chưa bao giờ có thể tìm thấy một giải pháp chức năng cửa sổ nào vượt qua dữ liệu. Tôi không nghĩ rằng nó có thể. Các chức năng của cửa sổ cần có thể được áp dụng cho tất cả các giá trị trong một cột. Điều đó làm cho việc tính toán thiết lập lại như thế này rất khó khăn, bởi vì một thiết lập lại thay đổi giá trị cho tất cả các giá trị sau.
Một cách để suy nghĩ về vấn đề là bạn có thể nhận được kết quả cuối cùng mà bạn muốn nếu bạn tính tổng tổng chạy cơ bản miễn là bạn có thể trừ tổng chạy từ hàng trước đó. Ví dụ: trong dữ liệu mẫu của bạn, giá trị cho id
4 là running total of row 4 - the running total of row 3
. Giá trị cho id
6 là running total of row 6 - the running total of row 3
vì chưa thiết lập lại. Giá trị cho id
7 là running total of row 7 - the running total of row 6
và như vậy.
Tôi sẽ tiếp cận điều này với T-SQL trong một vòng lặp. Tôi đã mang đi một chút và nghĩ rằng tôi có một giải pháp đầy đủ. Đối với 3 triệu hàng và 500 nhóm, mã hoàn thành sau 24 giây trên máy tính để bàn của tôi. Tôi đang thử nghiệm với phiên bản SQL Server 2016 Developer với 6 vCPU. Tôi đang tận dụng các chèn song song và thực thi song song nói chung để bạn có thể cần thay đổi mã nếu bạn đang ở phiên bản cũ hơn hoặc có các giới hạn DOP.
Bên dưới mã mà tôi đã sử dụng để tạo dữ liệu. Phạm vi trên VAL
và RESET_VAL
phải tương tự với dữ liệu mẫu của bạn.
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;
Thuật toán như sau:
1) Bắt đầu bằng cách chèn tất cả các hàng có tổng số chạy chuẩn vào bảng tạm thời.
2) Trong một vòng lặp:
2a) Đối với mỗi nhóm, hãy tính hàng đầu tiên có tổng chạy trên reset_value còn lại trong bảng và lưu trữ id, tổng chạy quá lớn và tổng chạy trước đó quá lớn trong bảng tạm thời.
2b) Xóa các hàng từ bảng tạm thời đầu tiên vào bảng tạm thời kết quả có ID
nhỏ hơn hoặc bằng với ID
bảng tạm thời thứ hai. Sử dụng các cột khác để điều chỉnh tổng số chạy khi cần thiết.
3) Sau khi xóa, không còn xử lý các hàng chạy bổ sung DELETE OUTPUT
vào bảng kết quả. Điều này là cho các hàng ở cuối nhóm không bao giờ vượt quá giá trị đặt lại.
Tôi sẽ thực hiện từng bước thực hiện thuật toán trên trong T-SQL.
Bắt đầu bằng cách tạo một vài bảng tạm thời. #initial_results
giữ dữ liệu gốc với tổng chạy tiêu chuẩn, #group_bookkeeping
được cập nhật mỗi vòng lặp để tìm ra hàng nào có thể được di chuyển và #final_results
chứa kết quả với tổng chạy được điều chỉnh cho đặt lại.
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;
Tôi tạo chỉ mục được nhóm trên bảng tạm thời sau đó để việc chèn và xây dựng chỉ mục có thể được thực hiện song song. Tạo sự khác biệt lớn trên máy của tôi nhưng có thể không phải trên máy của bạn. Tạo một chỉ mục trên bảng nguồn dường như không giúp được gì nhưng điều đó có thể giúp ích cho máy của bạn.
Mã dưới đây chạy trong vòng lặp và cập nhật bảng kế toán. Đối với mỗi nhóm, chúng ta cần tìm mức tối đa ID
cần chuyển vào bảng kết quả. Chúng ta cần tổng số chạy từ hàng đó để có thể trừ nó khỏi tổng số chạy ban đầu. Các grp_done
cột được thiết lập để 1 khi không có công việc nào hơn để làm cho một 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);
Thực sự không phải là một fan hâm mộ của LOOP JOIN
gợi ý nói chung, nhưng đây là một truy vấn đơn giản và đó là cách nhanh nhất để có được những gì tôi muốn. Để thực sự tối ưu hóa cho thời gian đáp ứng, tôi muốn tham gia vòng lặp lồng nhau song song thay vì tham gia hợp nhất DOP 1.
Mã dưới đây chạy trong vòng lặp và di chuyển dữ liệu từ bảng ban đầu vào bảng kết quả cuối cùng. Lưu ý điều chỉnh tổng số chạy ban đầu.
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;
Để thuận tiện cho bạn dưới đây là mã đầy đủ:
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;