Đặt lại Chạy Tổng số dựa trên cột khác


10

Đang cố gắng để tính tổng chạy. Nhưng nó nên được đặt lại khi tổng số tiền lớn hơn giá trị cột khác

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

Chi tiết chỉ số:

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

dữ liệu mẫu

+----+-----+-----------+-----+
| 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   |
+----+-----+-----------+-----+ 

Kết quả mong đợi

+----+-----+-----------------+-------------+
| 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
+----+-----+-----------------+-------------+

Truy vấn:

Tôi đã nhận được kết quả bằng cách sử dụng Recursive CTE. Câu hỏi ban đầu có tại đây /programming/42085404/reset-ricky-total-basing-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 

Có sự thay thế nào tốt hơn trong T-SQLkhi không sử dụng CLR.?


Làm thế nào tốt hơn? Liệu truy vấn này thể hiện hiệu suất kém? Sử dụng số liệu gì?
Aaron Bertrand

@AaronBertrand - Để hiểu rõ hơn tôi đã đăng dữ liệu mẫu cho chỉ một nhóm. Tôi phải làm tương tự cho 50000các nhóm xung quanh với 60 Id . vì vậy tổng số hồ sơ sẽ được xung quanh 3000000. Tôi chắc chắn Recursive CTEsẽ không mở rộng quy mô tốt 3000000. Sẽ cập nhật các số liệu khi tôi trở lại văn phòng. Chúng tôi có thể đạt được điều này bằng cách sử dụng sum()Over(Order by)như bạn đã sử dụng trong bài viết này sqlperformance.com/2012/07/t-sql-queries/rucky-totals
P

Một con trỏ có thể làm tốt hơn CTE đệ quy
paparazzo

Câu trả lời:


6

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 id4 là running total of row 4 - the running total of row 3. Giá trị cho id6 là running total of row 6 - the running total of row 3vì chưa thiết lập lại. Giá trị cho id7 là running total of row 7 - the running total of row 6và 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 VALRESET_VALphả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ó IDnhỏ hơn hoặc bằng với IDbả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 OUTPUTvà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_resultsgiữ 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_resultschứ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 IDcầ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_donecộ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 JOINgợ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;

đơn giản là tuyệt vời tôi sẽ trao thưởng cho bạn bằng tiền thưởng
P

Trong máy chủ của chúng tôi, trong 50000 grp và 60 id của bạn mất 1 phút 10 giây. Recursive CTEmất 2 phút và 15 giây
P

Tôi đã thử nghiệm cả hai mã với cùng một dữ liệu. Bạn thật tuyệt vời. Nó có thể được cải thiện hơn nữa?
P

Ý tôi là, tôi đã chạy mã của bạn trên dữ liệu thực của chúng tôi và kiểm tra nó. Tính toán được xử lý trong các bảng tạm thời trong quy trình thực tế của tôi, rất có thể nó phải được đóng gói chặt chẽ. Sẽ tốt hơn nếu có thể giảm xuống bất cứ thứ gì trong khoảng 30 giây
P

@Prdp Đã thử một cách tiếp cận nhanh chóng sử dụng bản cập nhật nhưng có vẻ tệ hơn. Sẽ không thể xem xét điều này nhiều hơn một chút. Hãy thử đăng nhập mỗi lần thao tác mất bao lâu để bạn có thể tìm ra phần nào đang chạy chậm nhất trên máy chủ của mình. Chắc chắn có thể có một cách để tăng tốc mã này hoặc một thuật toán tốt hơn nói chung.
Joe Obbish

4

Sử dụng một HIỆN TẠI:

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      |
+----+-----+-----------+-------------+

Kiểm tra tại đây: http://rextester.com/WSPLO95303


3

Không có cửa sổ, nhưng phiên bản SQL thuần túy:

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
;

Tôi không phải là chuyên gia về phương ngữ của SQL Server. Đây là phiên bản ban đầu cho PostrgreSQL (nếu tôi hiểu chính xác, tôi không thể sử dụng LIMIT 1 / TOP 1 trong phần đệ quy trong SQL Server):

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;

@JoeObish thành thật mà nói, điều đó không hoàn toàn rõ ràng từ câu hỏi. Các kết quả dự kiến, ví dụ, không hiển thị grpcột.
ypercubeᵀᴹ

@JoeObish đó là những gì tôi hiểu là tốt. Tuy nhiên, câu hỏi có thể được hưởng lợi từ một tuyên bố rõ ràng về điều đó. Mã trong câu hỏi (với CTE) cũng không sử dụng nó (và thậm chí nó có các cột được đặt tên khác nhau). Bất cứ ai đọc câu hỏi sẽ rõ ràng - họ sẽ không - và không nên - phải đọc các câu trả lời hoặc nhận xét khác.
ypercubeᵀᴹ

@ ypercubeᵀᴹ Đã thêm thông tin cần thiết vào câu hỏi.
P

1

Có vẻ như bạn có một số truy vấn / phương pháp để tấn công vấn đề nhưng bạn đã không cung cấp cho chúng tôi - hoặc thậm chí xem xét? - các chỉ số trên bảng.

Những chỉ số nào có trong bảng? Nó là một đống hay nó có một chỉ mục cụm?

Tôi sẽ thử các giải pháp khác nhau được đề xuất sau khi thêm chỉ số này:

(grp, id) INCLUDE (val, reset_val)

Hoặc chỉ cần thay đổi (hoặc thực hiện) chỉ mục được nhóm (grp, id).

Có một chỉ mục nhắm mục tiêu truy vấn cụ thể sẽ cải thiện hiệu quả - hầu hết nếu không phải tất cả các phương pháp.


Đã thêm thông tin cần thiết vào câu hỏi.
P
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.