Chọn dữ liệu được chia thành các nhóm phân bổ đều theo giá trị


8

Tôi muốn chọn thành 4 nhóm dữ liệu từ một bảng có tổng các giá trị trong các nhóm được phân bổ đều có thể. Tôi chắc chắn rằng tôi không giải thích nó đủ rõ ràng vì vậy tôi sẽ cố gắng đưa ra một ví dụ.

Ở đây tôi sử dụng NTILE (4) để tạo 4 nhóm:

SELECT Time, NTILE(4) OVER (ORDER BY Time DESC) AS N FROM TableX

Time -  N
-------------
10  -   1
 9  -   2
 8  -   3
 7  -   4
 6  -   1
 5  -   2
 4  -   3
 3  -   4
 2  -   1
 1  -   2

Trong truy vấn và kết quả ở trên, các cột khác đã bị bỏ qua cho ngắn gọn.

Vì vậy, bạn có thể thấy các nhóm cũng như sau:

  1    2    3    4
---  ---  ---  ---
 10    9    8    7
  6    5    4    3
  2    1    
---  ---  ---  ---
 18   15   12   10  Sum Totals of Time

Lưu ý rằng Tổng số thời gian sử dụng NTile không thực sự cân bằng giữa các nhóm. Một phân phối tốt hơn của các giá trị Thời gian sẽ là ví dụ:

  1    2    3    4
---  ---  ---  ---
 10    9    8    7
  3    5    4    6
  1         2
---  ---  ---  ---
 14   14   14   13  Sum Totals of Time

Ở đây, Tổng số thời gian được phân bổ đều hơn trên 4 nhóm.

Làm cách nào tôi có thể thực hiện việc này thông qua báo cáo TSQL?

Hơn nữa, tôi phải nói rằng tôi đang sử dụng SQL Server 2012. Nếu bạn có thứ gì đó có thể giúp tôi, hãy cho tôi biết.

Chúc các bạn một ngày vui vẻ.

Stan


Giá trị của bạn luôn luôn là số nguyên? Nếu vậy, họ trong một chuỗi liên tục hoặc có thể có những khoảng trống? Giá trị duy nhất?
Daniel Hutmacher

Xin chào, vâng, chúng là số nguyên và không có chúng không liên tục, một số có thể tăng gấp đôi và khoảng cách chắc chắn nằm giữa chúng. Hãy tưởng tượng họ rằng đó là thời gian cần thiết để thực hiện một thao tác cho mục cụ thể đó (mục cụ thể là một cột bị bỏ qua).
iStan

Câu trả lời:


14

Đây là một cú đâm vào một thuật toán. Nó không hoàn hảo, và tùy thuộc vào thời gian bạn muốn dành cho việc tinh chỉnh nó, có thể có một số lợi ích nhỏ hơn nữa sẽ được thực hiện.

Giả sử bạn có một bảng các nhiệm vụ được thực hiện bởi bốn hàng đợi. Bạn biết khối lượng công việc liên quan đến việc thực hiện từng nhiệm vụ và bạn muốn cả bốn hàng đợi có được khối lượng công việc gần như bằng nhau, vì vậy tất cả các hàng đợi sẽ hoàn thành cùng một lúc.

Trước hết, tôi phân vùng các tác vụ bằng cách sử dụng một cách hợp lý, được sắp xếp theo kích thước của chúng, từ nhỏ đến lớn.

SELECT [time], ROW_NUMBER() OVER (ORDER BY [time])%4 AS grp, 0

Các ROW_NUMBER()đơn hàng mỗi hàng theo kích thước, sau đó gán một số hàng, bắt đầu từ 1. Số hàng này được gán một "nhóm" ( grpcột) trên cơ sở luân chuyển vòng. Hàng đầu tiên là nhóm 1, hàng thứ hai là nhóm 2, sau đó 3, hàng thứ tư là nhóm 0, v.v.

time ROW_NUMBER() grp
---- ------------ ---
   1            1   1
  10            2   2
  12            3   3
  15            4   0
  19            5   1
  22            6   2
...

Để dễ sử dụng, tôi đang lưu trữ các cột timegrpcột trong một biến bảng được gọi @work.

Bây giờ, chúng ta có thể thực hiện một vài tính toán trên dữ liệu này:

WITH cte AS (
    SELECT *, SUM([time]) OVER (PARTITION BY grp)
             -SUM([time]) OVER (PARTITION BY (SELECT NULL))/4 AS _grpoffset
    FROM @work)
...

Cột _grpoffsetlà tổng số timetrên mỗi grpkhác với trung bình "lý tưởng". Nếu tổng số timecủa tất cả các nhiệm vụ là 1000 và có bốn nhóm, lý tưởng nhất là có tổng số 250 trong mỗi nhóm. Nếu một nhóm chứa tổng cộng 268, thì nhóm đó _grpoffset=18.

Ý tưởng là xác định hai hàng tốt nhất, một trong nhóm "tích cực" (có quá nhiều công việc) và một trong nhóm "tiêu cực" (với quá ít công việc). Nếu chúng ta có thể hoán đổi các nhóm trên hai hàng đó, chúng ta có thể giảm tuyệt đối _grpoffsetcủa cả hai nhóm.

Thí dụ:

time grp total _grpoffset
---- --- ----- ----------
   3   1   222         40
  46   1   222         40
  73   1   222         40
 100   1   222         40
   6   2   134        -48
  52   2   134        -48
  76   2   134        -48
  11   3   163        -21
  66   3   163        -21
  86   3   163        -21
  45   0   208         24
  71   0   208         24
  92   0   208         24
----
=727

Với tổng cộng 727, mỗi nhóm nên có số điểm khoảng 182 để phân phối trở nên hoàn hảo. Sự khác biệt giữa điểm số của nhóm và 182 là những gì chúng tôi đưa vào _grpoffsetcột.

Như bạn có thể thấy bây giờ, trong thế giới tốt nhất, chúng ta nên di chuyển khoảng 40 điểm giá trị của các hàng từ nhóm 1 sang nhóm 2 và khoảng 24 điểm từ nhóm 3 sang nhóm 0.

Đây là mã để xác định các hàng ứng cử viên đó:

    SELECT TOP 1 pos._row AS _pos_row, pos.grp AS _pos_grp,
                 neg._row AS _neg_row, neg.grp AS _neg_grp
    FROM cte AS pos
    INNER JOIN cte AS neg ON
        pos._grpoffset>0 AND
        neg._grpoffset<0 AND
        --- To prevent infinite recursion:
        pos.moved<4 AND
        neg.moved<4
    WHERE --- must improve positive side's offset:
          ABS(pos._grpoffset-pos.[time]+neg.[time])<=pos._grpoffset AND
          --- must improve negative side's offset:
          ABS(neg._grpoffset-neg.[time]+pos.[time])<=ABS(neg._grpoffset)
    --- Largest changes first:
    ORDER BY ABS(pos.[time]-neg.[time]) DESC
    ) AS x ON w._row IN (x._pos_row, x._neg_row);

Tôi tự tham gia biểu thức bảng chung mà chúng ta đã tạo trước đây cte: Một mặt, các nhóm có mặt tích cực _grpoffset, mặt khác là các nhóm âm. Để tiếp tục lọc ra những hàng nào được cho là khớp với nhau, sự hoán đổi của các mặt tích cực và tiêu cực phải được cải thiện _grpoffset, tức là làm cho nó gần hơn với 0.

Các TOP 1ORDER BYlựa chọn phù hợp "tốt nhất" để trao đổi đầu tiên.

Bây giờ, tất cả những gì chúng ta cần là thêm UPDATEvà lặp lại cho đến khi không còn tìm thấy tối ưu hóa nữa.

TL; DR - đây là truy vấn

Đây là mã hoàn chỉnh:

DECLARE @work TABLE (
    _row    int IDENTITY(1, 1) NOT NULL,
    [time]  int NOT NULL,
    grp     int NOT NULL,
    moved   tinyint NOT NULL,
    PRIMARY KEY CLUSTERED ([time], _row)
);

WITH cte AS (
    SELECT 0 AS n, CAST(1+100*RAND(CHECKSUM(NEWID())) AS int) AS [time]
    UNION ALL
    SELECT n+1,    CAST(1+100*RAND(CHECKSUM(NEWID())) AS int) AS [time]
    FROM cte WHERE n<100)

INSERT INTO @work ([time], grp, moved)
SELECT [time], ROW_NUMBER() OVER (ORDER BY [time])%4 AS grp, 0
FROM cte;



WHILE (@@ROWCOUNT!=0)
    WITH cte AS (
        SELECT *, SUM([time]) OVER (PARTITION BY grp)
                 -SUM([time]) OVER (PARTITION BY (SELECT NULL))/4 AS _grpoffset
        FROM @work)

    UPDATE w
    SET w.grp=(CASE w._row
               WHEN x._pos_row THEN x._neg_grp
               ELSE x._pos_grp END),
        w.moved=w.moved+1
    FROM @work AS w
    INNER JOIN (
        SELECT TOP 1 pos._row AS _pos_row, pos.grp AS _pos_grp,
                     neg._row AS _neg_row, neg.grp AS _neg_grp
        FROM cte AS pos
        INNER JOIN cte AS neg ON
            pos._grpoffset>0 AND
            neg._grpoffset<0 AND
            --- To prevent infinite recursion:
            pos.moved<4 AND
            neg.moved<4
        WHERE --- must improve positive side's offset:
              ABS(pos._grpoffset-pos.[time]+neg.[time])<=pos._grpoffset AND
              --- must improve negative side's offset:
              ABS(neg._grpoffset-neg.[time]+pos.[time])<=ABS(neg._grpoffset)
        --- Largest changes first:
        ORDER BY ABS(pos.[time]-neg.[time]) DESC
        ) AS x ON w._row IN (x._pos_row, x._neg_row);
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.